feat: validate repo if it already exists
All checks were successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created 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
All checks were successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created 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#60
This commit is contained in:
parent
7b46045737
commit
ff6e61b0ee
8 changed files with 192 additions and 8 deletions
|
@ -63,7 +63,7 @@ tokio = { version = "1.37", features = ["full"] }
|
||||||
# Testing
|
# Testing
|
||||||
assert2 = "0.3"
|
assert2 = "0.3"
|
||||||
pretty_assertions = "1.4"
|
pretty_assertions = "1.4"
|
||||||
test-log = "0.2"
|
test-log = { version = "0.2", features = ["env_logger", "trace"] }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|
||||||
[package.metadata.bin]
|
[package.metadata.bin]
|
||||||
|
|
|
@ -181,7 +181,6 @@ pub struct ServerRepoConfig {
|
||||||
dev: Option<String>,
|
dev: Option<String>,
|
||||||
}
|
}
|
||||||
impl ServerRepoConfig {
|
impl ServerRepoConfig {
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn repo(&self) -> RepoPath {
|
pub fn repo(&self) -> RepoPath {
|
||||||
RepoPath(self.repo.clone())
|
RepoPath(self.repo.clone())
|
||||||
}
|
}
|
||||||
|
@ -369,6 +368,36 @@ impl RepoDetails {
|
||||||
let origin = format!("https://{user}:{token}@{hostname}/{repo_path}.git");
|
let origin = format!("https://{user}:{token}@{hostname}/{repo_path}.git");
|
||||||
origin.into()
|
origin.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn validate_repo(&self) -> Result<(), RepoValidationError> {
|
||||||
|
self.gitdir.validate(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_default_push_remote(&self) -> Result<GitRemote, RepoValidationError> {
|
||||||
|
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 {
|
||||||
|
return Err(RepoValidationError::NoDefaultPushRemote);
|
||||||
|
};
|
||||||
|
let Some(url) = remote.url(gix::remote::Direction::Push) else {
|
||||||
|
return Err(RepoValidationError::NoUrlForDefaultPushRemote);
|
||||||
|
};
|
||||||
|
let Some(host) = url.host() else {
|
||||||
|
return Err(RepoValidationError::NoHostnameForDefaultPushRemote);
|
||||||
|
};
|
||||||
|
let path = url.path.to_string();
|
||||||
|
let path = path.strip_prefix('/').map_or(path.as_str(), |path| path);
|
||||||
|
let path = path.strip_suffix(".git").map_or(path, |path| path);
|
||||||
|
Ok(GitRemote::new(
|
||||||
|
Hostname(host.to_string()),
|
||||||
|
RepoPath(path.to_string()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn git_remote(&self) -> GitRemote {
|
||||||
|
GitRemote::new(self.forge.hostname.clone(), self.repo_path.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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 {
|
||||||
|
@ -386,6 +415,34 @@ impl Display for RepoDetails {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RepoValidationError {
|
||||||
|
NoDefaultPushRemote,
|
||||||
|
NoUrlForDefaultPushRemote,
|
||||||
|
NoHostnameForDefaultPushRemote,
|
||||||
|
UnableToOpenRepo(String),
|
||||||
|
Io(std::io::Error),
|
||||||
|
MismatchDefaultPushRemote {
|
||||||
|
found: GitRemote,
|
||||||
|
configured: GitRemote,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
impl Display for RepoValidationError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::NoDefaultPushRemote => write!(f, "There is no default push remote"),
|
||||||
|
Self::NoUrlForDefaultPushRemote => write!(f, "The default push remote has no url"),
|
||||||
|
Self::NoHostnameForDefaultPushRemote => {
|
||||||
|
write!(f, "The default push remote has no hostname")
|
||||||
|
}
|
||||||
|
Self::UnableToOpenRepo(err) => write!(f, "Unable to open the git dir: {err}"),
|
||||||
|
Self::Io(err) => write!(f, "IO Error: {err:?}"),
|
||||||
|
Self::MismatchDefaultPushRemote{found, configured} =>
|
||||||
|
write!(f, "The default push remote for the git dir doesn't match the configuration: found: {found}, expected: {configured}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Identifier for the type of Forge
|
/// Identifier for the type of Forge
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
pub enum ForgeType {
|
pub enum ForgeType {
|
||||||
|
@ -415,6 +472,15 @@ impl GitDir {
|
||||||
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> {
|
||||||
|
let configured = repo_details.git_remote();
|
||||||
|
let found = repo_details.find_default_push_remote()?;
|
||||||
|
if configured != found {
|
||||||
|
return Err(RepoValidationError::MismatchDefaultPushRemote { found, configured });
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl Deref for GitDir {
|
impl Deref for GitDir {
|
||||||
type Target = PathBuf;
|
type Target = PathBuf;
|
||||||
|
@ -433,6 +499,33 @@ impl From<&str> for GitDir {
|
||||||
Self(value.into())
|
Self(value.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl From<GitDir> for PathBuf {
|
||||||
|
fn from(value: GitDir) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct GitRemote {
|
||||||
|
host: Hostname,
|
||||||
|
repo_path: RepoPath,
|
||||||
|
}
|
||||||
|
impl GitRemote {
|
||||||
|
pub const fn new(host: Hostname, repo_path: RepoPath) -> Self {
|
||||||
|
Self { host, repo_path }
|
||||||
|
}
|
||||||
|
pub const fn host(&self) -> &Hostname {
|
||||||
|
&self.host
|
||||||
|
}
|
||||||
|
pub const fn repo_path(&self) -> &RepoPath {
|
||||||
|
&self.repo_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for GitRemote {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}:{}", self.host, self.repo_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
use assert2::let_assert;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use crate::filesystem::FileSystem;
|
use crate::{
|
||||||
|
filesystem::FileSystem,
|
||||||
|
server::gitforge::tests::common, /* server::gitforge::tests::common */
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -142,3 +146,82 @@ fn gitdir_should_display_as_pathbuf() {
|
||||||
//then
|
//then
|
||||||
assert_eq!(result, "foo/dir");
|
assert_eq!(result, "foo/dir");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// 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> {
|
||||||
|
let cwd = std::env::current_dir().map_err(RepoValidationError::Io)?;
|
||||||
|
let mut repo_details = common::repo_details(
|
||||||
|
1,
|
||||||
|
common::forge_details(1, ForgeType::MockForge),
|
||||||
|
None,
|
||||||
|
GitDir::new(&cwd), // Server GitDir - should be ignored
|
||||||
|
);
|
||||||
|
repo_details.forge.hostname = Hostname("git.kemitix.net".to_string());
|
||||||
|
repo_details.repo_path = RepoPath("kemitix/git-next".to_string());
|
||||||
|
let found_git_remote = repo_details.find_default_push_remote()?;
|
||||||
|
let config_git_remote = repo_details.git_remote();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
found_git_remote, config_git_remote,
|
||||||
|
"Default Push Remote must match config"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<(), RepoValidationError> {
|
||||||
|
let cwd = std::env::current_dir().map_err(RepoValidationError::Io)?;
|
||||||
|
let mut repo_details = common::repo_details(
|
||||||
|
1,
|
||||||
|
common::forge_details(1, ForgeType::MockForge),
|
||||||
|
None,
|
||||||
|
GitDir::new(&cwd), // Server GitDir - should be ignored
|
||||||
|
);
|
||||||
|
repo_details.forge.hostname = Hostname("git.kemitix.net".to_string());
|
||||||
|
repo_details.repo_path = RepoPath("kemitix/git-next".to_string());
|
||||||
|
let git_dir = &repo_details.gitdir;
|
||||||
|
git_dir.validate(&repo_details)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gitdir_validate_should_fail_a_non_git_dir() {
|
||||||
|
let_assert!(Ok(fs) = kxio::filesystem::FileSystem::new_temp());
|
||||||
|
let cwd = fs.cwd();
|
||||||
|
let repo_details = common::repo_details(
|
||||||
|
1,
|
||||||
|
common::forge_details(1, ForgeType::MockForge),
|
||||||
|
None,
|
||||||
|
GitDir::new(cwd), // Server GitDir - should be ignored
|
||||||
|
);
|
||||||
|
let git_dir = &repo_details.gitdir;
|
||||||
|
let_assert!(Err(_) = git_dir.validate(&repo_details));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() {
|
||||||
|
let_assert!(Ok(cwd) = std::env::current_dir().map_err(RepoValidationError::Io));
|
||||||
|
let mut repo_details = common::repo_details(
|
||||||
|
1,
|
||||||
|
common::forge_details(1, ForgeType::MockForge),
|
||||||
|
None,
|
||||||
|
GitDir::new(&cwd), // Server GitDir - should be ignored
|
||||||
|
);
|
||||||
|
repo_details.forge.hostname = Hostname("localhost".to_string());
|
||||||
|
repo_details.repo_path = RepoPath("hello/world".to_string());
|
||||||
|
let git_dir = &repo_details.gitdir;
|
||||||
|
let_assert!(Err(_) = git_dir.validate(&repo_details));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn git_remote_to_string_is_as_expected() {
|
||||||
|
let git_remote = GitRemote::new(Hostname("foo".to_string()), RepoPath("bar".to_string()));
|
||||||
|
let as_string = git_remote.to_string();
|
||||||
|
|
||||||
|
assert_eq!(as_string, "foo:bar");
|
||||||
|
}
|
||||||
|
|
|
@ -43,16 +43,20 @@ impl std::fmt::Display for ForgeBranchError {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum RepoCloneError {
|
pub enum RepoCloneError {
|
||||||
InvalidGitDir(GitDir),
|
InvalidGitDir(GitDir),
|
||||||
|
Io(std::io::Error),
|
||||||
Wait(std::io::Error),
|
Wait(std::io::Error),
|
||||||
Spawn(std::io::Error),
|
Spawn(std::io::Error),
|
||||||
|
Validation(String),
|
||||||
}
|
}
|
||||||
impl std::error::Error for RepoCloneError {}
|
impl std::error::Error for RepoCloneError {}
|
||||||
impl std::fmt::Display for RepoCloneError {
|
impl std::fmt::Display for RepoCloneError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::InvalidGitDir(gitdir) => write!(f, "Invalid Git dir: {:?}", gitdir),
|
Self::InvalidGitDir(gitdir) => write!(f, "Invalid Git dir: {:?}", gitdir),
|
||||||
|
Self::Io(err) => write!(f, "IO error: {:?}", err),
|
||||||
Self::Wait(err) => write!(f, "Waiting for command: {:?}", err),
|
Self::Wait(err) => write!(f, "Waiting for command: {:?}", err),
|
||||||
Self::Spawn(err) => write!(f, "Spawning comming: {:?}", err),
|
Self::Spawn(err) => write!(f, "Spawning comming: {:?}", err),
|
||||||
|
Self::Validation(err) => write!(f, "Validation: {}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,8 +101,14 @@ impl super::ForgeLike for ForgeJoEnv {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repo_clone(&self, gitdir: GitDir) -> Result<(), RepoCloneError> {
|
fn repo_clone(&self, gitdir: GitDir) -> Result<(), RepoCloneError> {
|
||||||
|
if gitdir.exists() {
|
||||||
|
gitdir
|
||||||
|
.validate(&self.repo_details)
|
||||||
|
.map_err(|e| RepoCloneError::Validation(e.to_string()))
|
||||||
|
} else {
|
||||||
repo::clone(&self.repo_details, gitdir)
|
repo::clone(&self.repo_details, gitdir)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
|
|
@ -6,8 +6,6 @@ use crate::server::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn clone(repo_details: &RepoDetails, gitdir: GitDir) -> Result<(), RepoCloneError> {
|
pub fn clone(repo_details: &RepoDetails, gitdir: GitDir) -> Result<(), RepoCloneError> {
|
||||||
// TODO: (#60) If directory already exists, validate it is git repo (e.g. is a git repo,
|
|
||||||
// matches the same origin)
|
|
||||||
let origin = repo_details.origin();
|
let origin = repo_details.origin();
|
||||||
// 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'
|
||||||
use secrecy::ExposeSecret;
|
use secrecy::ExposeSecret;
|
||||||
|
|
|
@ -94,4 +94,4 @@ impl std::ops::Deref for Forge {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
pub mod tests;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
mod common;
|
pub mod common;
|
||||||
|
|
||||||
#[cfg(feature = "forgejo")]
|
#[cfg(feature = "forgejo")]
|
||||||
mod forgejo;
|
mod forgejo;
|
||||||
|
|
Loading…
Reference in a new issue