feat(config): use specified gitdir when supplied

The user can specify a gitdir for a repo in the `git-next-server.toml` file.
When they do, then we should use that directory.

Closes kemitix/git-next#71
This commit is contained in:
Paul Campbell 2024-04-28 18:13:45 +01:00
parent bb67b7c66d
commit 31ef0c19fb
5 changed files with 90 additions and 58 deletions

View file

@ -21,7 +21,15 @@ pub struct RepoConfigUnknownError(pub network::StatusCode);
pub async fn load( pub async fn load(
details: &RepoDetails, details: &RepoDetails,
forge: &gitforge::Forge, forge: &gitforge::Forge,
) -> Result<RepoConfig, OneOf<(ForgeFileError, toml::de::Error, RepoConfigValidationErrors)>> { ) -> Result<
RepoConfig,
OneOf<(
ForgeFileError,
crate::server::config::Error,
toml::de::Error,
RepoConfigValidationErrors,
)>,
> {
let contents = forge let contents = forge
.file_contents_get(&details.branch, ".git-next.toml") .file_contents_get(&details.branch, ".git-next.toml")
.await .await

View file

@ -17,6 +17,9 @@ pub enum Error {
KxIoFs(kxio::fs::Error), KxIoFs(kxio::fs::Error),
TomlDe(toml::de::Error), TomlDe(toml::de::Error),
} }
impl std::error::Error for 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)] #[derive(Debug, PartialEq, Eq, Deserialize)]
@ -26,7 +29,7 @@ pub struct ServerConfig {
forge: HashMap<String, ForgeConfig>, forge: HashMap<String, ForgeConfig>,
} }
impl ServerConfig { impl ServerConfig {
pub(crate) fn load(fs: &FileSystem) -> Result<Self, Error> { pub(crate) fn load(fs: &FileSystem) -> Result<Self> {
let str = fs.file_read_to_string(&fs.base().join("git-next-server.toml"))?; let str = fs.file_read_to_string(&fs.base().join("git-next-server.toml"))?;
toml::from_str(&str).map_err(Into::into) toml::from_str(&str).map_err(Into::into)
} }
@ -90,8 +93,8 @@ impl RepoConfig {
Self { branches } Self { branches }
} }
// #[cfg(test)] // #[cfg(test)]
pub fn load(toml: &str) -> Result<Self, toml::de::Error> { pub fn load(toml: &str) -> Result<Self> {
toml::from_str(toml) toml::from_str(toml).map_err(Into::into)
} }
pub const fn branches(&self) -> &RepoBranches { pub const fn branches(&self) -> &RepoBranches {
@ -186,7 +189,7 @@ impl Display for ForgeConfig {
pub struct ServerRepoConfig { pub struct ServerRepoConfig {
repo: String, repo: String,
branch: String, branch: String,
gitdir: Option<PathBuf>, // TODO: (#71) use this when supplied gitdir: Option<PathBuf>,
main: Option<String>, main: Option<String>,
next: Option<String>, next: Option<String>,
dev: Option<String>, dev: Option<String>,
@ -352,18 +355,21 @@ impl RepoDetails {
forge_name: &ForgeName, forge_name: &ForgeName,
forge_config: &ForgeConfig, forge_config: &ForgeConfig,
server_storage: &ServerStorage, server_storage: &ServerStorage,
) -> Self { ) -> Result<Self> {
Self { let path_buf = server_repo_config.gitdir.clone().unwrap_or_else(|| {
server_storage
.path
.join(forge_name.to_string())
.join(name.to_string())
});
let path_buf = std::fs::canonicalize(path_buf)?;
let gitdir = GitDir(path_buf);
Ok(Self {
repo_alias: name.clone(), repo_alias: name.clone(),
repo_path: RepoPath(server_repo_config.repo.clone()), repo_path: RepoPath(server_repo_config.repo.clone()),
repo_config: server_repo_config.repo_config(), repo_config: server_repo_config.repo_config(),
branch: BranchName(server_repo_config.branch.clone()), branch: BranchName(server_repo_config.branch.clone()),
gitdir: GitDir( gitdir,
server_storage
.path
.join(forge_name.to_string())
.join(name.to_string()),
),
forge: ForgeDetails { forge: ForgeDetails {
forge_name: forge_name.clone(), forge_name: forge_name.clone(),
forge_type: forge_config.forge_type.clone(), forge_type: forge_config.forge_type.clone(),
@ -371,7 +377,7 @@ impl RepoDetails {
user: forge_config.user(), user: forge_config.user(),
token: forge_config.token(), token: forge_config.token(),
}, },
} })
} }
pub fn origin(&self) -> secrecy::Secret<String> { pub fn origin(&self) -> secrecy::Secret<String> {
let repo_details = self; let repo_details = self;
@ -386,11 +392,11 @@ impl RepoDetails {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn validate_repo(&self) -> Result<(), RepoValidationError> { pub fn validate_repo(&self) -> ValidationResult<()> {
self.gitdir.validate(self) self.gitdir.validate(self)
} }
pub fn find_default_push_remote(&self) -> Result<GitRemote, RepoValidationError> { pub fn find_default_push_remote(&self) -> ValidationResult<GitRemote> {
let repository = gix::open(self.gitdir.clone()) let repository = gix::open(self.gitdir.clone())
.map_err(|e| RepoValidationError::UnableToOpenRepo(e.to_string()))?; .map_err(|e| RepoValidationError::UnableToOpenRepo(e.to_string()))?;
let Some(Ok(remote)) = repository.find_default_remote(gix::remote::Direction::Push) else { let Some(Ok(remote)) = repository.find_default_remote(gix::remote::Direction::Push) else {
@ -431,6 +437,7 @@ impl Display for RepoDetails {
} }
} }
type ValidationResult<T> = core::result::Result<T, RepoValidationError>;
#[derive(Debug)] #[derive(Debug)]
pub enum RepoValidationError { pub enum RepoValidationError {
NoDefaultPushRemote, NoDefaultPushRemote,
@ -443,6 +450,7 @@ pub enum RepoValidationError {
configured: GitRemote, configured: GitRemote,
}, },
} }
impl std::error::Error for RepoValidationError {}
impl Display for RepoValidationError { impl Display for RepoValidationError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -484,12 +492,12 @@ impl GitDir {
pub(crate) fn new(pathbuf: &std::path::Path) -> Self { pub(crate) fn new(pathbuf: &std::path::Path) -> Self {
Self(pathbuf.to_path_buf()) Self(pathbuf.to_path_buf())
} }
#[allow(dead_code)] // TODO:
pub const fn pathbuf(&self) -> &PathBuf { pub const fn pathbuf(&self) -> &PathBuf {
&self.0 &self.0
} }
pub fn validate(&self, repo_details: &RepoDetails) -> Result<(), RepoValidationError> { pub fn validate(&self, repo_details: &RepoDetails) -> ValidationResult<()> {
let configured = repo_details.git_remote(); let configured = repo_details.git_remote();
let found = repo_details.find_default_push_remote()?; let found = repo_details.find_default_push_remote()?;
if configured != found { if configured != found {

View file

@ -7,8 +7,10 @@ use kxio::fs;
use super::*; use super::*;
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
#[test] #[test]
fn load_should_parse_server_config() -> Result<(), crate::server::config::Error> { fn load_should_parse_server_config() -> Result<()> {
let fs = fs::temp()?; let fs = fs::temp()?;
fs.file_write( fs.file_write(
&fs.base().join("git-next-server.toml"), &fs.base().join("git-next-server.toml"),
@ -111,7 +113,7 @@ fn load_should_parse_server_config() -> Result<(), crate::server::config::Error>
} }
#[test] #[test]
fn test_repo_config_load() -> Result<(), crate::server::config::Error> { fn test_repo_config_load() -> Result<()> {
let toml = r#" let toml = r#"
[branches] [branches]
main = "main" main = "main"
@ -150,7 +152,7 @@ fn gitdir_should_display_as_pathbuf() {
// NOTE: this test assumes it is being run in a cloned worktree from the project's home repo: // NOTE: this test assumes it is being run in a cloned worktree from the project's home repo:
// 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<(), RepoValidationError> { fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
let cwd = std::env::current_dir().map_err(RepoValidationError::Io)?; let cwd = std::env::current_dir().map_err(RepoValidationError::Io)?;
let mut repo_details = common::repo_details( let mut repo_details = common::repo_details(
1, 1,
@ -172,7 +174,7 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<(), Re
} }
#[test] #[test]
fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<(), RepoValidationError> { fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
let cwd = std::env::current_dir().map_err(RepoValidationError::Io)?; let cwd = std::env::current_dir().map_err(RepoValidationError::Io)?;
let mut repo_details = common::repo_details( let mut repo_details = common::repo_details(
1, 1,

View file

@ -102,13 +102,13 @@ impl super::ForgeLike for ForgeJoEnv {
fn repo_clone(&self, gitdir: GitDir) -> Result<(), RepoCloneError> { fn repo_clone(&self, gitdir: GitDir) -> Result<(), RepoCloneError> {
if gitdir.exists() { if gitdir.exists() {
info!(?gitdir, "Gitdir already exists - validating..."); info!(%gitdir, "Gitdir already exists - validating...");
gitdir gitdir
.validate(&self.repo_details) .validate(&self.repo_details)
.map_err(|e| RepoCloneError::Validation(e.to_string())) .map_err(|e| RepoCloneError::Validation(e.to_string()))
.inspect(|_| info!(?gitdir, "Validation - OK")) .inspect(|_| info!(%gitdir, "Validation - OK"))
} else { } else {
info!(?gitdir, "Gitdir doesn't exists - cloning..."); info!(%gitdir, "Gitdir doesn't exists - cloning...");
repo::clone(&self.repo_details, gitdir).inspect(|_| info!("Cloned - OK")) repo::clone(&self.repo_details, gitdir).inspect(|_| info!("Cloned - OK"))
} }
} }

View file

@ -27,7 +27,11 @@ pub enum Error {
FailedToCreateDataDirectory(kxio::fs::Error), FailedToCreateDataDirectory(kxio::fs::Error),
#[display("The forge data path is not a directory: {path:?}")] #[display("The forge data path is not a directory: {path:?}")]
ForgeDirIsNotDirectory { path: PathBuf }, ForgeDirIsNotDirectory {
path: PathBuf,
},
Config(crate::server::config::Error),
} }
type Result<T> = core::result::Result<T, Error>; type Result<T> = core::result::Result<T, Error>;
@ -81,14 +85,25 @@ pub async fn start(fs: FileSystem, net: Network) {
return; return;
} }
server_config for (forge_name, forge_config) in server_config.forges() {
.forges() if let Err(err) = create_forge_repos(
.flat_map(|(forge_name, forge_config)| { forge_config,
create_forge_repos(forge_config, forge_name, server_storage, webhook, &net) forge_name.clone(),
}) server_storage,
.map(start_actor) webhook,
.map(|(alias, addr)| webhook::AddWebhookRecipient(alias, addr.recipient())) &net,
.for_each(|msg| webhook_router.do_send(msg)); )
.map(|repos| {
repos
.into_iter()
.map(start_actor)
.map(|(alias, addr)| webhook::AddWebhookRecipient(alias, addr.recipient()))
.for_each(|msg| webhook_router.do_send(msg));
}) {
error!(?err, ?forge_name, "Failed to create forge repo actor");
return;
}
}
let webhook_server = webhook::WebhookActor::new(webhook_router.recipient()).start(); let webhook_server = webhook::WebhookActor::new(webhook_router.recipient()).start();
let _ = actix_rt::signal::ctrl_c().await; let _ = actix_rt::signal::ctrl_c().await;
info!("Ctrl-C received, shutting down..."); info!("Ctrl-C received, shutting down...");
@ -121,20 +136,22 @@ fn create_forge_repos(
server_storage: &ServerStorage, server_storage: &ServerStorage,
webhook: &Webhook, webhook: &Webhook,
net: &Network, net: &Network,
) -> Vec<(ForgeName, RepoAlias, RepoActor)> { ) -> Result<Vec<(ForgeName, RepoAlias, RepoActor)>> {
let span = tracing::info_span!("Forge", %forge_name, %forge_config); let span = tracing::info_span!("Forge", %forge_name, %forge_config);
let _guard = span.enter(); let _guard = span.enter();
info!("Creating Forge"); info!("Creating Forge");
forge_config let mut repos = vec![];
.repos() let creator = create_actor(
.map(create_actor( forge_name,
forge_name, forge_config.clone(),
forge_config.clone(), server_storage,
server_storage, webhook,
webhook, net,
net, );
)) for (repo_alias, server_repo_config) in forge_config.repos() {
.collect::<Vec<_>>() repos.push(creator((repo_alias, server_repo_config))?);
}
Ok(repos)
} }
fn create_actor( fn create_actor(
@ -143,7 +160,7 @@ fn create_actor(
server_storage: &ServerStorage, server_storage: &ServerStorage,
webhook: &Webhook, webhook: &Webhook,
net: &Network, net: &Network,
) -> impl Fn((RepoAlias, &ServerRepoConfig)) -> (ForgeName, RepoAlias, RepoActor) { ) -> impl Fn((RepoAlias, &ServerRepoConfig)) -> Result<(ForgeName, RepoAlias, RepoActor)> {
let server_storage = server_storage.clone(); let server_storage = server_storage.clone();
let webhook = webhook.clone(); let webhook = webhook.clone();
let net = net.clone(); let net = net.clone();
@ -151,19 +168,16 @@ fn create_actor(
let span = tracing::info_span!("Repo", %repo_name, %server_repo_config); let span = tracing::info_span!("Repo", %repo_name, %server_repo_config);
let _guard = span.enter(); let _guard = span.enter();
info!("Creating Repo"); info!("Creating Repo");
let actor = actors::repo::RepoActor::new( let repo_details = config::RepoDetails::new(
config::RepoDetails::new( &repo_name,
&repo_name, server_repo_config,
server_repo_config, &forge_name,
&forge_name, &forge_config,
&forge_config, &server_storage,
&server_storage, )?;
), let actor = actors::repo::RepoActor::new(repo_details, webhook.clone(), net.clone());
webhook.clone(),
net.clone(),
);
info!("Created Repo"); info!("Created Repo");
(forge_name.clone(), repo_name, actor) Ok((forge_name.clone(), repo_name, actor))
} }
} }