diff --git a/Cargo.toml b/Cargo.toml index 56f88e1b..904d24e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ base64 = "0.22" # git # gix = "0.62" gix = { version = "0.63", features = [ + "dirwalk", "blocking-http-transport-reqwest-rust-tls", ] } async-trait = "0.1" diff --git a/crates/forge-forgejo/src/file.rs b/crates/forge-forgejo/src/file.rs deleted file mode 100644 index 1b5af3c2..00000000 --- a/crates/forge-forgejo/src/file.rs +++ /dev/null @@ -1,87 +0,0 @@ -use git_next_config as config; -use git_next_git as git; - -use kxio::network::{self, Network}; -use tracing::{error, warn}; - -pub async fn contents_get( - repo_details: &git::RepoDetails, - net: &Network, - branch: &config::BranchName, - file_path: &str, -) -> Result { - let hostname = &repo_details.forge.hostname(); - let repo_path = &repo_details.repo_path; - let api_token = &repo_details.forge.token(); - use secrecy::ExposeSecret; - let token = api_token.expose_secret(); - let url = network::NetUrl::new(format!( - "https://{hostname}/api/v1/repos/{repo_path}/contents/{file_path}?ref={branch}&token={token}" - )); - - // info!("Loading config"); - let request = network::NetRequest::new( - network::RequestMethod::Get, - url, - network::NetRequestHeaders::new(), - network::RequestBody::None, - network::ResponseType::Json, - None, - network::NetRequestLogging::None, - ); - let result = net.get::(request).await; - let response = result.map_err(|e| { - warn!(?e, ""); - git::file::Error::NotFound(file_path.to_string()) - })?; - let status = response.status_code(); - let contents = match response.response_body() { - Some(body) => { - // we need to decode (see encoding field) the value of 'content' from the response - match body.content_type { - ForgeContentsType::File => decode_config(body), - _ => Err(git::file::Error::NotFile(file_path.to_string())), - } - } - None => { - error!(%status, "Failed to fetch repo config file"); - Err(git::file::Error::Unknown(status.to_string())) - } - }?; - Ok(contents) -} - -fn decode_config(body: ForgeContentsResponse) -> Result { - use base64::Engine; - match body.encoding.as_str() { - "base64" => { - let decoded = base64::engine::general_purpose::STANDARD - .decode(body.content) - .map_err(|_| git::file::Error::DecodeFromBase64)?; - let decoded = - String::from_utf8(decoded).map_err(|_| git::file::Error::DecodeFromUtf8)?; - - Ok(decoded) - } - encoding => Err(git::file::Error::UnknownEncoding(encoding.to_string())), - } -} - -#[derive(Clone, Debug, serde::Deserialize)] -struct ForgeContentsResponse { - #[serde(rename = "type")] - pub content_type: ForgeContentsType, - pub content: String, - pub encoding: String, -} -#[derive(Clone, Debug, serde::Deserialize)] -enum ForgeContentsType { - #[serde(rename = "file")] - File, - #[serde(rename = "dir")] - Dir, - #[serde(rename = "symlink")] - Symlink, - #[serde(rename = "submodule")] - Submodule, -} diff --git a/crates/forge-forgejo/src/lib.rs b/crates/forge-forgejo/src/lib.rs index f6c7c9eb..e194aeab 100644 --- a/crates/forge-forgejo/src/lib.rs +++ b/crates/forge-forgejo/src/lib.rs @@ -1,5 +1,4 @@ pub mod branch; -mod file; #[cfg(test)] mod tests; @@ -30,14 +29,6 @@ impl git::ForgeLike for ForgeJo { branch::get_all(&self.repo_details, &self.net).await } - async fn file_contents_get( - &self, - branch: &config::BranchName, - file_path: &str, - ) -> Result { - file::contents_get(&self.repo_details, &self.net, branch, file_path).await - } - async fn commit_status(&self, commit: &git::Commit) -> git::commit::Status { let repo_details = &self.repo_details; let hostname = &repo_details.forge.hostname(); diff --git a/crates/forge/src/mock_forge.rs b/crates/forge/src/mock_forge.rs index 6063dd3b..35c853bf 100644 --- a/crates/forge/src/mock_forge.rs +++ b/crates/forge/src/mock_forge.rs @@ -22,14 +22,6 @@ impl git::ForgeLike for MockForgeEnv { todo!() } - async fn file_contents_get( - &self, - _branch: &config::BranchName, - _file_path: &str, - ) -> Result { - todo!() - } - async fn commit_status(&self, _commit: &git::Commit) -> git::commit::Status { todo!() } diff --git a/crates/git/src/file.rs b/crates/git/src/file.rs index 0c70561c..8b34aef2 100644 --- a/crates/git/src/file.rs +++ b/crates/git/src/file.rs @@ -1,18 +1,74 @@ +// +pub type Result = core::result::Result; + #[derive(Debug, derive_more::Display)] pub enum Error { + Lock, + #[display("File not found: {}", 0)] NotFound(String), + #[display("Unable to parse file contents")] ParseContent, + #[display("Unable to decode from base64")] DecodeFromBase64, - #[display("Unable to decoce from UTF-8")] + + #[display("Unable to decode from UTF-8")] DecodeFromUtf8, + #[display("Unknown file encoding: {}", 0)] UnknownEncoding(String), + #[display("Not a file: {}", 0)] NotFile(String), + #[display("Unknown error (status: {})", 0)] Unknown(String), + + CommitLog(crate::commit::log::Error), + + CommitNotFound, + + NoTreeInCommit(String), + + NoGitNextToml, + + FindReference(String), + + FindObject(String), + + NonUtf8Blob(String), + + TryId, } impl std::error::Error for Error {} + +impl From for Error { + fn from(value: crate::commit::log::Error) -> Self { + Self::CommitLog(value) + } +} +impl From for Error { + fn from(value: gix::reference::find::existing::Error) -> Self { + Self::FindReference(value.to_string()) + } +} + +impl From for Error { + fn from(value: gix::object::commit::Error) -> Self { + Self::NoTreeInCommit(value.to_string()) + } +} + +impl From for Error { + fn from(value: gix::object::find::existing::Error) -> Self { + Self::FindObject(value.to_string()) + } +} + +impl From for Error { + fn from(value: std::string::FromUtf8Error) -> Self { + Self::NonUtf8Blob(value.to_string()) + } +} diff --git a/crates/git/src/forge_like.rs b/crates/git/src/forge_like.rs index 062da652..a8844c70 100644 --- a/crates/git/src/forge_like.rs +++ b/crates/git/src/forge_like.rs @@ -8,13 +8,6 @@ pub trait ForgeLike { /// Returns a list of all branches in the repo. async fn branches_get_all(&self) -> Result, git::branch::Error>; - /// Returns the contents of the file. - async fn file_contents_get( - &self, - branch: &config::BranchName, - file_path: &str, - ) -> Result; - /// Checks the results of any (e.g. CI) status checks for the commit. async fn commit_status(&self, commit: &git::Commit) -> git::commit::Status; } diff --git a/crates/git/src/push.rs b/crates/git/src/push.rs index d7eb2834..080d52c3 100644 --- a/crates/git/src/push.rs +++ b/crates/git/src/push.rs @@ -22,4 +22,4 @@ pub enum Error { } impl std::error::Error for Error {} -pub type Result = core::result::Result<(), Error>; +pub type Result = core::result::Result; diff --git a/crates/git/src/repository/mock.rs b/crates/git/src/repository/mock.rs index fc266151..f4d4f887 100644 --- a/crates/git/src/repository/mock.rs +++ b/crates/git/src/repository/mock.rs @@ -143,6 +143,17 @@ impl OpenRepositoryLike for MockOpenRepository { .map(|inner| inner.commit_log(branch_name, find_commits)) .unwrap() } + + fn read_file( + &self, + branch_name: &git_next_config::BranchName, + file_name: &str, + ) -> git::file::Result { + self.inner + .lock() + .map(|inner| inner.read_file(branch_name, file_name)) + .unwrap() + } } impl OpenRepositoryLike for InnerMockOpenRepository { fn find_default_remote(&self, direction: Direction) -> Option { @@ -173,4 +184,12 @@ impl OpenRepositoryLike for InnerMockOpenRepository { ) -> core::result::Result, crate::commit::log::Error> { todo!() } + + fn read_file( + &self, + _branch_name: &git_next_config::BranchName, + _file_name: &str, + ) -> git::file::Result { + todo!() + } } diff --git a/crates/git/src/repository/open/mod.rs b/crates/git/src/repository/open/mod.rs index b2fcd65b..907b2479 100644 --- a/crates/git/src/repository/open/mod.rs +++ b/crates/git/src/repository/open/mod.rs @@ -36,14 +36,23 @@ pub trait OpenRepositoryLike { branch_name: config::BranchName, to_commit: git::GitRef, force: git::push::Force, - ) -> Result<(), git::push::Error>; + ) -> git::push::Result<()>; /// List of commits in a branch, optionally up-to any specified commit. fn commit_log( &self, branch_name: &config::BranchName, find_commits: &[git::Commit], - ) -> Result, git::commit::log::Error>; + ) -> git::commit::log::Result>; + + /// Read the contents of a file as a string. + /// + /// Only handles files in the root of the repo. + fn read_file( + &self, + branch_name: &config::BranchName, + file_name: &str, + ) -> git::file::Result; } impl std::ops::Deref for OpenRepository { type Target = dyn OpenRepositoryLike; diff --git a/crates/git/src/repository/open/oreal.rs b/crates/git/src/repository/open/oreal.rs index 08f91026..033af33e 100644 --- a/crates/git/src/repository/open/oreal.rs +++ b/crates/git/src/repository/open/oreal.rs @@ -1,9 +1,8 @@ // - use crate as git; use git_next_config as config; -use gix::bstr::BStr; +use gix::bstr::BStr; use std::sync::{Arc, Mutex}; use tracing::{info, warn}; @@ -152,6 +151,32 @@ impl super::OpenRepositoryLike for RealOpenRepository { Ok(commits) })? } + + #[tracing::instrument(skip_all, fields(%branch_name, %file_name))] + fn read_file( + &self, + branch_name: &config::BranchName, + file_name: &str, + ) -> git::file::Result { + self.0 + .lock() + .map_err(|_| git::file::Error::Lock) + .and_then(|repo| { + let fref = repo.find_reference(format!("origin/{}", branch_name).as_str())?; + let id = fref.try_id().ok_or(git::file::Error::TryId)?; + let oid = id.detach(); + let obj = repo.find_object(oid)?; + let commit = obj.into_commit(); + let tree = commit.tree()?; + let ent = tree + .find_entry(".git-next.toml") + .ok_or(git::file::Error::NoGitNextToml)?; + let fobj = ent.object()?; + let blob = fobj.into_blob().take_data(); + let content = String::from_utf8(blob)?; + Ok(content) + }) + } } impl From<&gix::Url> for git::GitRemote { diff --git a/crates/repo-actor/src/lib.rs b/crates/repo-actor/src/lib.rs index db6ab095..e604d3dd 100644 --- a/crates/repo-actor/src/lib.rs +++ b/crates/repo-actor/src/lib.rs @@ -122,7 +122,11 @@ impl Handler for RepoActor { let details = self.repo_details.clone(); let addr = ctx.address(); let forge = self.forge.clone(); - repo_actor::load::load_file(details, addr, forge) + let Some(open_repository) = self.open_repository.clone() else { + warn!("missing open repository - can't load configuration"); + return; + }; + repo_actor::load::load_file(details, addr, forge, open_repository) .in_current_span() .into_actor(self) .wait(ctx); diff --git a/crates/repo-actor/src/load.rs b/crates/repo-actor/src/load.rs index c7d1e3bb..c17412b0 100644 --- a/crates/repo-actor/src/load.rs +++ b/crates/repo-actor/src/load.rs @@ -11,9 +11,14 @@ use super::{LoadedConfig, RepoActor}; /// Loads the [RepoConfig] from the `.git-next.toml` file in the repository #[tracing::instrument(skip_all, fields(branch = %repo_details.branch))] -pub async fn load_file(repo_details: git::RepoDetails, addr: Addr, forge: forge::Forge) { +pub async fn load_file( + repo_details: git::RepoDetails, + addr: Addr, + forge: forge::Forge, + open_repository: git::OpenRepository, +) { info!("Loading .git-next.toml from repo"); - let repo_config = match load(&repo_details, &forge).await { + let repo_config = match load(&repo_details, &forge, open_repository).await { Ok(repo_config) => repo_config, Err(err) => { error!(?err, "Failed to load config"); @@ -27,10 +32,9 @@ pub async fn load_file(repo_details: git::RepoDetails, addr: Addr, fo async fn load( details: &git::RepoDetails, forge: &forge::Forge, + open_repository: git::OpenRepository, ) -> Result { - let contents = forge - .file_contents_get(&details.branch, ".git-next.toml") - .await?; + let contents = open_repository.read_file(&details.branch, ".git-next.toml")?; let config = config::RepoConfig::load(&contents)?; let config = validate(config, forge).await?; Ok(config)