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(
details: &RepoDetails,
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
.file_contents_get(&details.branch, ".git-next.toml")
.await

View file

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

View file

@ -7,8 +7,10 @@ use kxio::fs;
use super::*;
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
#[test]
fn load_should_parse_server_config() -> Result<(), crate::server::config::Error> {
fn load_should_parse_server_config() -> Result<()> {
let fs = fs::temp()?;
fs.file_write(
&fs.base().join("git-next-server.toml"),
@ -111,7 +113,7 @@ fn load_should_parse_server_config() -> Result<(), crate::server::config::Error>
}
#[test]
fn test_repo_config_load() -> Result<(), crate::server::config::Error> {
fn test_repo_config_load() -> Result<()> {
let toml = r#"
[branches]
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:
// git.kemitix.net:kemitix/git-next
// 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 mut repo_details = common::repo_details(
1,
@ -172,7 +174,7 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<(), Re
}
#[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 mut repo_details = common::repo_details(
1,

View file

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

View file

@ -27,7 +27,11 @@ pub enum Error {
FailedToCreateDataDirectory(kxio::fs::Error),
#[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>;
@ -81,14 +85,25 @@ pub async fn start(fs: FileSystem, net: Network) {
return;
}
server_config
.forges()
.flat_map(|(forge_name, forge_config)| {
create_forge_repos(forge_config, forge_name, server_storage, webhook, &net)
})
for (forge_name, forge_config) in server_config.forges() {
if let Err(err) = create_forge_repos(
forge_config,
forge_name.clone(),
server_storage,
webhook,
&net,
)
.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 _ = actix_rt::signal::ctrl_c().await;
info!("Ctrl-C received, shutting down...");
@ -121,20 +136,22 @@ fn create_forge_repos(
server_storage: &ServerStorage,
webhook: &Webhook,
net: &Network,
) -> Vec<(ForgeName, RepoAlias, RepoActor)> {
) -> Result<Vec<(ForgeName, RepoAlias, RepoActor)>> {
let span = tracing::info_span!("Forge", %forge_name, %forge_config);
let _guard = span.enter();
info!("Creating Forge");
forge_config
.repos()
.map(create_actor(
let mut repos = vec![];
let creator = create_actor(
forge_name,
forge_config.clone(),
server_storage,
webhook,
net,
))
.collect::<Vec<_>>()
);
for (repo_alias, server_repo_config) in forge_config.repos() {
repos.push(creator((repo_alias, server_repo_config))?);
}
Ok(repos)
}
fn create_actor(
@ -143,7 +160,7 @@ fn create_actor(
server_storage: &ServerStorage,
webhook: &Webhook,
net: &Network,
) -> impl Fn((RepoAlias, &ServerRepoConfig)) -> (ForgeName, RepoAlias, RepoActor) {
) -> impl Fn((RepoAlias, &ServerRepoConfig)) -> Result<(ForgeName, RepoAlias, RepoActor)> {
let server_storage = server_storage.clone();
let webhook = webhook.clone();
let net = net.clone();
@ -151,19 +168,16 @@ fn create_actor(
let span = tracing::info_span!("Repo", %repo_name, %server_repo_config);
let _guard = span.enter();
info!("Creating Repo");
let actor = actors::repo::RepoActor::new(
config::RepoDetails::new(
let repo_details = config::RepoDetails::new(
&repo_name,
server_repo_config,
&forge_name,
&forge_config,
&server_storage,
),
webhook.clone(),
net.clone(),
);
)?;
let actor = actors::repo::RepoActor::new(repo_details, webhook.clone(), net.clone());
info!("Created Repo");
(forge_name.clone(), repo_name, actor)
Ok((forge_name.clone(), repo_name, actor))
}
}