From 91870055b0ec2ff487724adc1f48a23aa28e43c2 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Fri, 19 Apr 2024 18:56:42 +0100 Subject: [PATCH] feat(gitforge): Add ability to clone a repo Closes kemitix/git-next#56 --- src/server/config/mod.rs | 14 ++++++-- src/server/gitforge/errors.rs | 17 +++++++++ src/server/gitforge/forgejo/branch/reset.rs | 18 +++++----- src/server/gitforge/forgejo/mod.rs | 9 ++++- src/server/gitforge/forgejo/repo/clone.rs | 40 +++++++++++++++++++++ src/server/gitforge/forgejo/repo/mod.rs | 3 ++ src/server/gitforge/mock_forge.rs | 8 ++++- src/server/gitforge/mod.rs | 15 ++++++++ 8 files changed, 110 insertions(+), 14 deletions(-) create mode 100644 src/server/gitforge/forgejo/repo/clone.rs create mode 100644 src/server/gitforge/forgejo/repo/mod.rs diff --git a/src/server/config/mod.rs b/src/server/config/mod.rs index 39c269be..210ab5d1 100644 --- a/src/server/config/mod.rs +++ b/src/server/config/mod.rs @@ -7,7 +7,6 @@ use std::{ path::PathBuf, }; -use secrecy::ExposeSecret; use serde::Deserialize; use terrors::OneOf; @@ -251,7 +250,7 @@ impl From for ApiToken { } } /// The API Token is in effect a password, so it must be explicitly exposed to access its value -impl ExposeSecret for ApiToken { +impl secrecy::ExposeSecret for ApiToken { fn expose_secret(&self) -> &String { self.0.expose_secret() } @@ -347,6 +346,17 @@ impl RepoDetails { }, } } + pub fn origin(&self) -> secrecy::Secret { + let repo_details = self; + let user = &repo_details.forge.user; + let hostname = &repo_details.forge.hostname; + let path = &repo_details.repo; + use secrecy::ExposeSecret; + let expose_secret = &repo_details.forge.token; + let token = expose_secret.expose_secret(); + let origin = format!("https://{user}:{token}@{hostname}/{path}.git"); + origin.into() + } } impl Display for RepoDetails { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { diff --git a/src/server/gitforge/errors.rs b/src/server/gitforge/errors.rs index 60c1d9b7..171d234f 100644 --- a/src/server/gitforge/errors.rs +++ b/src/server/gitforge/errors.rs @@ -39,3 +39,20 @@ impl std::fmt::Display for ForgeBranchError { } } } + +#[derive(Debug)] +pub enum RepoCloneError { + InvalidGitDir(std::path::PathBuf), + Wait(std::io::Error), + Spawn(std::io::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::Wait(err) => write!(f, "Waiting for command: {:?}", err), + Self::Spawn(err) => write!(f, "Spawning comming: {:?}", err), + } + } +} diff --git a/src/server/gitforge/forgejo/branch/reset.rs b/src/server/gitforge/forgejo/branch/reset.rs index 829b93d8..bd2e0d70 100644 --- a/src/server/gitforge/forgejo/branch/reset.rs +++ b/src/server/gitforge/forgejo/branch/reset.rs @@ -1,3 +1,4 @@ +use secrecy::ExposeSecret; use tracing::{info, warn}; use crate::server::{ @@ -12,22 +13,19 @@ pub fn reset( to_commit: GitRef, force: Force, ) -> BranchResetResult { - let user = &repo_details.forge.user; - let hostname = &repo_details.forge.hostname; - let path = &repo_details.repo; - use secrecy::ExposeSecret; - let expose_secret = &repo_details.forge.token; - let token = expose_secret.expose_secret(); - let origin = format!("https://{user}:{token}@{hostname}/{path}.git"); + let origin = repo_details.origin(); let force = match force { Force::No => "".to_string(), Force::From(old_ref) => format!("--force-with-lease={branch_name}:{old_ref}"), }; // INFO: never log the command as it contains the API token within the 'origin' - let command = format!("/usr/bin/git push {origin} {to_commit}:{branch_name} {force}"); - drop(origin); + let command: secrecy::Secret = format!( + "/usr/bin/git push {} {to_commit}:{branch_name} {force}", + origin.expose_secret() + ) + .into(); info!("Resetting {branch_name} to {to_commit}"); - match gix::command::prepare(command) + match gix::command::prepare(command.expose_secret()) .with_shell_allow_argument_splitting() .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) diff --git a/src/server/gitforge/forgejo/mod.rs b/src/server/gitforge/forgejo/mod.rs index 928f2aa8..24e4ce23 100644 --- a/src/server/gitforge/forgejo/mod.rs +++ b/src/server/gitforge/forgejo/mod.rs @@ -1,5 +1,8 @@ mod branch; mod file; +mod repo; + +use std::path::PathBuf; use actix::prelude::*; @@ -9,7 +12,7 @@ use tracing::{error, warn}; use crate::server::{ actors::repo::RepoActor, config::{BranchName, RepoConfig, RepoDetails}, - gitforge, + gitforge::{self, RepoCloneError}, types::GitRef, }; @@ -98,6 +101,10 @@ impl super::ForgeLike for ForgeJoEnv { } } } + + fn repo_clone(&self, gitdir: PathBuf) -> Result<(), RepoCloneError> { + repo::clone(&self.repo_details, gitdir) + } } #[derive(Debug, serde::Deserialize)] diff --git a/src/server/gitforge/forgejo/repo/clone.rs b/src/server/gitforge/forgejo/repo/clone.rs new file mode 100644 index 00000000..ef10b4aa --- /dev/null +++ b/src/server/gitforge/forgejo/repo/clone.rs @@ -0,0 +1,40 @@ +use std::path::PathBuf; + +use tracing::{info, warn}; + +use crate::server::{config::RepoDetails, gitforge::RepoCloneError}; + +pub fn clone(repo_details: &RepoDetails, gitdir: PathBuf) -> Result<(), RepoCloneError> { + let Some(gitdir) = gitdir.to_str() else { + return Err(RepoCloneError::InvalidGitDir(gitdir)); + }; + let origin = repo_details.origin(); + // INFO: never log the command as it contains the API token within the 'origin' + use secrecy::ExposeSecret; + let command: secrecy::Secret = format!( + "/usr/bin/git clone --bare -- {} {:?}", + origin.expose_secret(), + gitdir + ) + .into(); + let repo_name = &repo_details.name; + info!("Cloning {repo_name} to {gitdir}"); + match gix::command::prepare(command.expose_secret()) + .with_shell_allow_argument_splitting() + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + { + Ok(mut child) => match child.wait() { + Ok(_) => Ok(()), + Err(err) => { + warn!(?err, "Failed (wait)"); + Err(RepoCloneError::Wait(err)) + } + }, + Err(err) => { + warn!(?err, "Failed (spawn)"); + Err(RepoCloneError::Spawn(err)) + } + } +} diff --git a/src/server/gitforge/forgejo/repo/mod.rs b/src/server/gitforge/forgejo/repo/mod.rs new file mode 100644 index 00000000..bb5a1803 --- /dev/null +++ b/src/server/gitforge/forgejo/repo/mod.rs @@ -0,0 +1,3 @@ +mod clone; + +pub use clone::clone; diff --git a/src/server/gitforge/mock_forge.rs b/src/server/gitforge/mock_forge.rs index d8e2c32d..58e13ce2 100644 --- a/src/server/gitforge/mock_forge.rs +++ b/src/server/gitforge/mock_forge.rs @@ -1,7 +1,9 @@ +use std::path::PathBuf; + use crate::server::{ actors::repo::RepoActor, config::{BranchName, RepoConfig}, - gitforge, + gitforge::{self, RepoCloneError}, types::GitRef, }; @@ -51,4 +53,8 @@ impl super::ForgeLike for MockForgeEnv { async fn commit_status(&self, _commit: &gitforge::Commit) -> gitforge::CommitStatus { todo!() } + + fn repo_clone(&self, _gitdir: PathBuf) -> Result<(), RepoCloneError> { + todo!() + } } diff --git a/src/server/gitforge/mod.rs b/src/server/gitforge/mod.rs index ebc81ff2..a1b863b1 100644 --- a/src/server/gitforge/mod.rs +++ b/src/server/gitforge/mod.rs @@ -1,5 +1,7 @@ #![allow(dead_code)] +use std::path::PathBuf; + use kxio::network::Network; #[cfg(feature = "forgejo")] @@ -24,17 +26,26 @@ use crate::server::{ #[async_trait::async_trait] pub trait ForgeLike { fn name(&self) -> String; + + /// Returns a list of all branches in the repo. async fn branches_get_all(&self) -> Result, ForgeBranchError>; + + /// Returns the contents of the file. async fn file_contents_get( &self, branch: &super::config::BranchName, file_path: &str, ) -> Result; + + /// Assesses the relative positions of the main, next and dev branch and updates their + /// positions as needed. async fn branches_validate_positions( &self, repo_config: RepoConfig, addr: actix::prelude::Addr, ); + + /// Moves a branch to a new commit. fn branch_reset( &self, branch_name: BranchName, @@ -42,7 +53,11 @@ pub trait ForgeLike { force: Force, ) -> BranchResetResult; + /// Checks the results of any (e.g. CI) status checks for the commit. async fn commit_status(&self, commit: &Commit) -> CommitStatus; + + /// Clones a repo to disk. + fn repo_clone(&self, gitdir: PathBuf) -> Result<(), RepoCloneError>; } #[derive(Clone)]