feat(gitforge): Add ability to clone a repo
All checks were successful
ci/woodpecker/push/tag-created Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
ci/woodpecker/cron/cron-docker-builder Pipeline was successful

Closes kemitix/git-next#56
This commit is contained in:
Paul Campbell 2024-04-19 18:56:42 +01:00
parent 16dc823f58
commit 91870055b0
8 changed files with 110 additions and 14 deletions

View file

@ -7,7 +7,6 @@ use std::{
path::PathBuf, path::PathBuf,
}; };
use secrecy::ExposeSecret;
use serde::Deserialize; use serde::Deserialize;
use terrors::OneOf; use terrors::OneOf;
@ -251,7 +250,7 @@ impl From<String> for ApiToken {
} }
} }
/// The API Token is in effect a password, so it must be explicitly exposed to access its value /// The API Token is in effect a password, so it must be explicitly exposed to access its value
impl ExposeSecret<String> for ApiToken { impl secrecy::ExposeSecret<String> for ApiToken {
fn expose_secret(&self) -> &String { fn expose_secret(&self) -> &String {
self.0.expose_secret() self.0.expose_secret()
} }
@ -347,6 +346,17 @@ impl RepoDetails {
}, },
} }
} }
pub fn origin(&self) -> secrecy::Secret<String> {
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 { impl Display for RepoDetails {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {

View file

@ -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),
}
}
}

View file

@ -1,3 +1,4 @@
use secrecy::ExposeSecret;
use tracing::{info, warn}; use tracing::{info, warn};
use crate::server::{ use crate::server::{
@ -12,22 +13,19 @@ pub fn reset(
to_commit: GitRef, to_commit: GitRef,
force: Force, force: Force,
) -> BranchResetResult { ) -> BranchResetResult {
let user = &repo_details.forge.user; let origin = repo_details.origin();
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 force = match force { let force = match force {
Force::No => "".to_string(), Force::No => "".to_string(),
Force::From(old_ref) => format!("--force-with-lease={branch_name}:{old_ref}"), 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' // 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}"); let command: secrecy::Secret<String> = format!(
drop(origin); "/usr/bin/git push {} {to_commit}:{branch_name} {force}",
origin.expose_secret()
)
.into();
info!("Resetting {branch_name} to {to_commit}"); info!("Resetting {branch_name} to {to_commit}");
match gix::command::prepare(command) match gix::command::prepare(command.expose_secret())
.with_shell_allow_argument_splitting() .with_shell_allow_argument_splitting()
.stdout(std::process::Stdio::null()) .stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null()) .stderr(std::process::Stdio::null())

View file

@ -1,5 +1,8 @@
mod branch; mod branch;
mod file; mod file;
mod repo;
use std::path::PathBuf;
use actix::prelude::*; use actix::prelude::*;
@ -9,7 +12,7 @@ use tracing::{error, warn};
use crate::server::{ use crate::server::{
actors::repo::RepoActor, actors::repo::RepoActor,
config::{BranchName, RepoConfig, RepoDetails}, config::{BranchName, RepoConfig, RepoDetails},
gitforge, gitforge::{self, RepoCloneError},
types::GitRef, 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)] #[derive(Debug, serde::Deserialize)]

View file

@ -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<String> = 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))
}
}
}

View file

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

View file

@ -1,7 +1,9 @@
use std::path::PathBuf;
use crate::server::{ use crate::server::{
actors::repo::RepoActor, actors::repo::RepoActor,
config::{BranchName, RepoConfig}, config::{BranchName, RepoConfig},
gitforge, gitforge::{self, RepoCloneError},
types::GitRef, types::GitRef,
}; };
@ -51,4 +53,8 @@ impl super::ForgeLike for MockForgeEnv {
async fn commit_status(&self, _commit: &gitforge::Commit) -> gitforge::CommitStatus { async fn commit_status(&self, _commit: &gitforge::Commit) -> gitforge::CommitStatus {
todo!() todo!()
} }
fn repo_clone(&self, _gitdir: PathBuf) -> Result<(), RepoCloneError> {
todo!()
}
} }

View file

@ -1,5 +1,7 @@
#![allow(dead_code)] #![allow(dead_code)]
use std::path::PathBuf;
use kxio::network::Network; use kxio::network::Network;
#[cfg(feature = "forgejo")] #[cfg(feature = "forgejo")]
@ -24,17 +26,26 @@ use crate::server::{
#[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.
async fn branches_get_all(&self) -> Result<Vec<Branch>, ForgeBranchError>; async fn branches_get_all(&self) -> Result<Vec<Branch>, ForgeBranchError>;
/// Returns the contents of the file.
async fn file_contents_get( async fn file_contents_get(
&self, &self,
branch: &super::config::BranchName, branch: &super::config::BranchName,
file_path: &str, file_path: &str,
) -> Result<String, ForgeFileError>; ) -> Result<String, ForgeFileError>;
/// Assesses the relative positions of the main, next and dev branch and updates their
/// positions as needed.
async fn branches_validate_positions( async fn branches_validate_positions(
&self, &self,
repo_config: RepoConfig, repo_config: RepoConfig,
addr: actix::prelude::Addr<super::actors::repo::RepoActor>, addr: actix::prelude::Addr<super::actors::repo::RepoActor>,
); );
/// Moves a branch to a new commit.
fn branch_reset( fn branch_reset(
&self, &self,
branch_name: BranchName, branch_name: BranchName,
@ -42,7 +53,11 @@ pub trait ForgeLike {
force: Force, force: Force,
) -> BranchResetResult; ) -> BranchResetResult;
/// 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: &Commit) -> CommitStatus;
/// Clones a repo to disk.
fn repo_clone(&self, gitdir: PathBuf) -> Result<(), RepoCloneError>;
} }
#[derive(Clone)] #[derive(Clone)]