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
|
||||
assert2 = "0.3"
|
||||
pretty_assertions = "1.4"
|
||||
test-log = "0.2"
|
||||
test-log = { version = "0.2", features = ["env_logger", "trace"] }
|
||||
anyhow = "1.0"
|
||||
|
||||
[package.metadata.bin]
|
||||
|
|
|
@ -181,7 +181,6 @@ pub struct ServerRepoConfig {
|
|||
dev: Option<String>,
|
||||
}
|
||||
impl ServerRepoConfig {
|
||||
#[allow(dead_code)]
|
||||
pub fn repo(&self) -> RepoPath {
|
||||
RepoPath(self.repo.clone())
|
||||
}
|
||||
|
@ -369,6 +368,36 @@ impl RepoDetails {
|
|||
let origin = format!("https://{user}:{token}@{hostname}/{repo_path}.git");
|
||||
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 {
|
||||
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
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||
pub enum ForgeType {
|
||||
|
@ -415,6 +472,15 @@ impl GitDir {
|
|||
pub const fn pathbuf(&self) -> &PathBuf {
|
||||
&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 {
|
||||
type Target = PathBuf;
|
||||
|
@ -433,6 +499,33 @@ impl From<&str> for GitDir {
|
|||
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)]
|
||||
mod tests;
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
use assert2::let_assert;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::filesystem::FileSystem;
|
||||
use crate::{
|
||||
filesystem::FileSystem,
|
||||
server::gitforge::tests::common, /* server::gitforge::tests::common */
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -142,3 +146,82 @@ fn gitdir_should_display_as_pathbuf() {
|
|||
//then
|
||||
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)]
|
||||
pub enum RepoCloneError {
|
||||
InvalidGitDir(GitDir),
|
||||
Io(std::io::Error),
|
||||
Wait(std::io::Error),
|
||||
Spawn(std::io::Error),
|
||||
Validation(String),
|
||||
}
|
||||
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::Io(err) => write!(f, "IO error: {:?}", err),
|
||||
Self::Wait(err) => write!(f, "Waiting for command: {:?}", err),
|
||||
Self::Spawn(err) => write!(f, "Spawning comming: {:?}", err),
|
||||
Self::Validation(err) => write!(f, "Validation: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,13 @@ impl super::ForgeLike for ForgeJoEnv {
|
|||
}
|
||||
|
||||
fn repo_clone(&self, gitdir: GitDir) -> Result<(), RepoCloneError> {
|
||||
repo::clone(&self.repo_details, gitdir)
|
||||
if gitdir.exists() {
|
||||
gitdir
|
||||
.validate(&self.repo_details)
|
||||
.map_err(|e| RepoCloneError::Validation(e.to_string()))
|
||||
} else {
|
||||
repo::clone(&self.repo_details, gitdir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,6 @@ use crate::server::{
|
|||
};
|
||||
|
||||
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();
|
||||
// INFO: never log the command as it contains the API token within the 'origin'
|
||||
use secrecy::ExposeSecret;
|
||||
|
|
|
@ -94,4 +94,4 @@ impl std::ops::Deref for Forge {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod tests;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::*;
|
||||
|
||||
mod common;
|
||||
pub mod common;
|
||||
|
||||
#[cfg(feature = "forgejo")]
|
||||
mod forgejo;
|
||||
|
|
Loading…
Reference in a new issue