feat: update auth of interal repos when changed in config
Closes kemitix/git-next#100
This commit is contained in:
parent
df352443b7
commit
5cf581a24a
33 changed files with 421 additions and 114 deletions
|
@ -13,5 +13,5 @@ debug = 0
|
||||||
strip = "debuginfo"
|
strip = "debuginfo"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
RUST_LOG = "hyper=warn,debug"
|
RUST_LOG = "hyper=warn,git_url_parse=warn,debug"
|
||||||
RUSTFLAGS = "--cfg tokio_unstable"
|
RUSTFLAGS = "--cfg tokio_unstable"
|
||||||
|
|
|
@ -61,6 +61,7 @@ gix = { version = "0.63", features = [
|
||||||
"blocking-http-transport-reqwest-rust-tls",
|
"blocking-http-transport-reqwest-rust-tls",
|
||||||
] }
|
] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
git-url-parse = "0.4"
|
||||||
|
|
||||||
# fs/network
|
# fs/network
|
||||||
kxio = { version = "1.2" }
|
kxio = { version = "1.2" }
|
||||||
|
@ -91,6 +92,7 @@ derive_more = { version = "1.0.0-beta.6", features = [
|
||||||
] }
|
] }
|
||||||
derive-with = "0.5"
|
derive-with = "0.5"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
pike = "0.1"
|
||||||
|
|
||||||
# file watcher
|
# file watcher
|
||||||
inotify = "0.10"
|
inotify = "0.10"
|
||||||
|
|
|
@ -23,6 +23,10 @@ toml = { workspace = true }
|
||||||
# Secrets and Password
|
# Secrets and Password
|
||||||
secrecy = { workspace = true }
|
secrecy = { workspace = true }
|
||||||
|
|
||||||
|
# Git
|
||||||
|
gix = { workspace = true }
|
||||||
|
git-url-parse = { workspace = true }
|
||||||
|
|
||||||
# Webhooks
|
# Webhooks
|
||||||
ulid = { workspace = true }
|
ulid = { workspace = true }
|
||||||
|
|
||||||
|
@ -30,12 +34,14 @@ ulid = { workspace = true }
|
||||||
derive_more = { workspace = true }
|
derive_more = { workspace = true }
|
||||||
derive-with = { workspace = true }
|
derive-with = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
pike = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# # Testing
|
# # Testing
|
||||||
assert2 = { workspace = true }
|
assert2 = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
|
test-log = { workspace = true }
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
nursery = { level = "warn", priority = -1 }
|
nursery = { level = "warn", priority = -1 }
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
crate::newtype!(ForgeAlias: String, Hash, derive_more::Display, Default: "The name of a Forge to connect to");
|
crate::newtype!(ForgeAlias: String, Hash, PartialOrd, Ord, derive_more::Display, Default: "The name of a Forge to connect to");
|
||||||
impl From<&ForgeAlias> for std::path::PathBuf {
|
impl From<&ForgeAlias> for std::path::PathBuf {
|
||||||
fn from(value: &ForgeAlias) -> Self {
|
fn from(value: &ForgeAlias) -> Self {
|
||||||
Self::from(&value.0)
|
Self::from(&value.0)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use derive_more::Deref;
|
use derive_more::Deref;
|
||||||
|
use pike::pike;
|
||||||
|
|
||||||
/// The path to the directory containing the git repository.
|
/// The path to the directory containing the git repository.
|
||||||
#[derive(
|
#[derive(
|
||||||
|
@ -24,8 +25,16 @@ impl GitDir {
|
||||||
pub const fn pathbuf(&self) -> &PathBuf {
|
pub const fn pathbuf(&self) -> &PathBuf {
|
||||||
&self.pathbuf
|
&self.pathbuf
|
||||||
}
|
}
|
||||||
pub const fn storage_path_type(&self) -> &StoragePathType {
|
pub const fn storage_path_type(&self) -> StoragePathType {
|
||||||
&self.storage_path_type
|
self.storage_path_type
|
||||||
|
}
|
||||||
|
pub fn as_fs(&self) -> kxio::fs::FileSystem {
|
||||||
|
pike! {
|
||||||
|
self
|
||||||
|
|> Self::pathbuf
|
||||||
|
|> PathBuf::clone
|
||||||
|
|> kxio::fs::new
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl std::fmt::Display for GitDir {
|
impl std::fmt::Display for GitDir {
|
||||||
|
@ -45,8 +54,13 @@ impl From<&GitDir> for PathBuf {
|
||||||
value.to_path_buf()
|
value.to_path_buf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl From<&GitDir> for kxio::fs::FileSystem {
|
||||||
|
fn from(gitdir: &GitDir) -> Self {
|
||||||
|
gitdir.as_fs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum StoragePathType {
|
pub enum StoragePathType {
|
||||||
Internal,
|
Internal,
|
||||||
External,
|
External,
|
||||||
|
|
|
@ -10,6 +10,7 @@ pub mod git_dir;
|
||||||
mod host_name;
|
mod host_name;
|
||||||
mod newtype;
|
mod newtype;
|
||||||
mod registered_webhook;
|
mod registered_webhook;
|
||||||
|
mod remote_url;
|
||||||
mod repo_alias;
|
mod repo_alias;
|
||||||
mod repo_branches;
|
mod repo_branches;
|
||||||
mod repo_config;
|
mod repo_config;
|
||||||
|
@ -33,6 +34,7 @@ pub use git_dir::GitDir;
|
||||||
pub use git_dir::StoragePathType;
|
pub use git_dir::StoragePathType;
|
||||||
pub use host_name::Hostname;
|
pub use host_name::Hostname;
|
||||||
pub use registered_webhook::RegisteredWebhook;
|
pub use registered_webhook::RegisteredWebhook;
|
||||||
|
pub use remote_url::RemoteUrl;
|
||||||
pub use repo_alias::RepoAlias;
|
pub use repo_alias::RepoAlias;
|
||||||
pub use repo_branches::RepoBranches;
|
pub use repo_branches::RepoBranches;
|
||||||
pub use repo_config::RepoConfig;
|
pub use repo_config::RepoConfig;
|
||||||
|
@ -43,3 +45,6 @@ pub use user::User;
|
||||||
pub use webhook::auth::WebhookAuth;
|
pub use webhook::auth::WebhookAuth;
|
||||||
pub use webhook::forge_notification::ForgeNotification;
|
pub use webhook::forge_notification::ForgeNotification;
|
||||||
pub use webhook::id::WebhookId;
|
pub use webhook::id::WebhookId;
|
||||||
|
|
||||||
|
// re-export
|
||||||
|
pub use pike::{pike, pike_opt, pike_res};
|
||||||
|
|
|
@ -28,8 +28,6 @@ macro_rules! newtype {
|
||||||
derive_more::From,
|
derive_more::From,
|
||||||
PartialEq,
|
PartialEq,
|
||||||
Eq,
|
Eq,
|
||||||
PartialOrd,
|
|
||||||
Ord,
|
|
||||||
derive_more::AsRef,
|
derive_more::AsRef,
|
||||||
derive_more::Deref,
|
derive_more::Deref,
|
||||||
$($derive),*
|
$($derive),*
|
||||||
|
|
16
crates/config/src/remote_url.rs
Normal file
16
crates/config/src/remote_url.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
crate::newtype!(RemoteUrl: git_url_parse::GitUrl, derive_more::Display: "The URL of a remote repository");
|
||||||
|
impl RemoteUrl {
|
||||||
|
pub fn parse(url: impl Into<String>) -> Option<Self> {
|
||||||
|
Some(Self::new(::git_url_parse::GitUrl::parse(&url.into()).ok()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<gix::Url> for RemoteUrl {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(url: gix::Url) -> Result<Self, Self::Error> {
|
||||||
|
let pass = url.password().map(|p| p.to_owned());
|
||||||
|
let mut parsed = ::git_url_parse::GitUrl::parse(&url.to_string()).map_err(|_| ())?;
|
||||||
|
parsed.token = pass;
|
||||||
|
Ok(Self(parsed))
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,6 @@ use derive_more::Display;
|
||||||
|
|
||||||
use crate::newtype;
|
use crate::newtype;
|
||||||
|
|
||||||
newtype!(RepoAlias: String, Display, Default: r#"The alias of a repo.
|
newtype!(RepoAlias: String, Display, Default, PartialOrd, Ord: r#"The alias of a repo.
|
||||||
|
|
||||||
This is the alias for the repo within `git-next-server.toml`."#);
|
This is the alias for the repo within `git-next-server.toml`."#);
|
||||||
|
|
|
@ -12,6 +12,8 @@ use crate::server::ServerStorage;
|
||||||
use crate::server::Webhook;
|
use crate::server::Webhook;
|
||||||
use crate::webhook::push::Branch;
|
use crate::webhook::push::Branch;
|
||||||
|
|
||||||
|
mod url;
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
|
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
type TestResult = Result<()>;
|
type TestResult = Result<()>;
|
||||||
|
|
||||||
|
|
25
crates/config/src/tests/url.rs
Normal file
25
crates/config/src/tests/url.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn url_parse_https_url() -> TestResult {
|
||||||
|
let url = "https://user:pass@git.host/user/repo.git";
|
||||||
|
|
||||||
|
let result = RemoteUrl::parse(url);
|
||||||
|
|
||||||
|
tracing::debug!(?result);
|
||||||
|
assert!(result.is_some());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn url_parse_ssh_url() -> TestResult {
|
||||||
|
let url = "git@git.host:user/repo.git";
|
||||||
|
|
||||||
|
let result = RemoteUrl::parse(url);
|
||||||
|
|
||||||
|
tracing::debug!(?result);
|
||||||
|
assert!(result.is_some());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -58,6 +58,7 @@ mockall = { workspace = true }
|
||||||
assert2 = { workspace = true }
|
assert2 = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
|
test-log = { workspace = true }
|
||||||
|
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
|
|
|
@ -37,8 +37,8 @@ impl From<config::webhook::Push> for Commit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newtype!(Sha: String, Display, Hash: "The unique SHA for a git commit.");
|
newtype!(Sha: String, Display, Hash,PartialOrd, Ord: "The unique SHA for a git commit.");
|
||||||
newtype!(Message: String, Hash: "The commit message for a git commit.");
|
newtype!(Message: String, Hash, PartialOrd, Ord: "The commit message for a git commit.");
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Histories {
|
pub struct Histories {
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use config::{
|
use config::{
|
||||||
BranchName, ForgeAlias, ForgeConfig, ForgeDetails, GitDir, RepoAlias, RepoConfig, RepoPath,
|
BranchName, ForgeAlias, ForgeConfig, ForgeDetails, GitDir, RepoAlias, RepoConfig, RepoPath,
|
||||||
ServerRepoConfig,
|
ServerRepoConfig, StoragePathType,
|
||||||
};
|
};
|
||||||
use git_next_config as config;
|
use git_next_config::{self as config, pike, RemoteUrl};
|
||||||
|
use secrecy::Secret;
|
||||||
|
|
||||||
|
use crate::repository::{OpenRepositoryLike, RealOpenRepository};
|
||||||
|
|
||||||
use super::{Generation, GitRemote};
|
use super::{Generation, GitRemote};
|
||||||
|
|
||||||
|
@ -55,6 +60,10 @@ impl RepoDetails {
|
||||||
origin.into()
|
origin.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn gitdir(&self) -> &GitDir {
|
||||||
|
&self.gitdir
|
||||||
|
}
|
||||||
|
|
||||||
pub fn git_remote(&self) -> GitRemote {
|
pub fn git_remote(&self) -> GitRemote {
|
||||||
GitRemote::new(self.forge.hostname().clone(), self.repo_path.clone())
|
GitRemote::new(self.forge.hostname().clone(), self.repo_path.clone())
|
||||||
}
|
}
|
||||||
|
@ -64,4 +73,100 @@ impl RepoDetails {
|
||||||
self.forge = forge.with_hostname(hostname);
|
self.forge = forge.with_hostname(hostname);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// url is a secret as it contains auth token
|
||||||
|
pub fn url(&self) -> Secret<String> {
|
||||||
|
let user = self.forge.user();
|
||||||
|
use secrecy::ExposeSecret;
|
||||||
|
let token = self.forge.token().expose_secret();
|
||||||
|
let hostname = self.forge.hostname();
|
||||||
|
let repo_path = &self.repo_path;
|
||||||
|
format!("https://{user}:{token}@{hostname}/{repo_path}.git").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::result_large_err)]
|
||||||
|
pub fn open(&self) -> Result<impl OpenRepositoryLike, crate::validation::remotes::Error> {
|
||||||
|
let gix_repo = pike! {
|
||||||
|
self
|
||||||
|
|> Self::gitdir
|
||||||
|
|> GitDir::pathbuf
|
||||||
|
|> gix::ThreadSafeRepository::open
|
||||||
|
}?;
|
||||||
|
let repo = pike! {
|
||||||
|
gix_repo
|
||||||
|
|> RwLock::new
|
||||||
|
|> Arc::new
|
||||||
|
|> RealOpenRepository::new
|
||||||
|
};
|
||||||
|
Ok(repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remote_url(&self) -> Option<git_next_config::RemoteUrl> {
|
||||||
|
use secrecy::ExposeSecret;
|
||||||
|
RemoteUrl::parse(self.url().expose_secret())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub fn assert_remote_url(&self, found: Option<RemoteUrl>) -> crate::repository::Result<()> {
|
||||||
|
let Some(found) = found else {
|
||||||
|
tracing::debug!("No remote url found to assert");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let Some(expected) = self.remote_url() else {
|
||||||
|
tracing::debug!("No remote url to assert against");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
if found != expected {
|
||||||
|
tracing::debug!(?found, ?expected, "urls differ");
|
||||||
|
match self.gitdir.storage_path_type() {
|
||||||
|
StoragePathType::External => {
|
||||||
|
tracing::debug!("Refusing to update an external repo - user must resolve this");
|
||||||
|
return Err(crate::repository::Error::MismatchDefaultFetchRemote {
|
||||||
|
found: Box::new(found),
|
||||||
|
expected: Box::new(expected),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
StoragePathType::Internal => {
|
||||||
|
tracing::debug!(?expected, "Need to update config with new url");
|
||||||
|
self.write_remote_url(&expected)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub fn write_remote_url(&self, url: &RemoteUrl) -> Result<(), kxio::fs::Error> {
|
||||||
|
if self.gitdir.storage_path_type() != StoragePathType::Internal {
|
||||||
|
// return Err(Not an internal repo)
|
||||||
|
}
|
||||||
|
let fs = self.gitdir.as_fs();
|
||||||
|
// load config file
|
||||||
|
let config_filename = &self.gitdir.join("config");
|
||||||
|
let config_file = fs.file_read_to_string(config_filename)?;
|
||||||
|
let mut config_lines = config_file
|
||||||
|
.lines()
|
||||||
|
.map(|l| l.to_owned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
tracing::debug!(?config_lines, "original file");
|
||||||
|
let url_line = format!(r#" url = "{}""#, url);
|
||||||
|
if config_lines
|
||||||
|
.iter()
|
||||||
|
.any(|line| *line == r#"[remote "origin"]"#)
|
||||||
|
{
|
||||||
|
tracing::debug!("has an 'origin' remote");
|
||||||
|
config_lines
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|line| line.starts_with(r#" url = "#))
|
||||||
|
.for_each(|line| line.clone_from(&url_line));
|
||||||
|
} else {
|
||||||
|
tracing::debug!("has no 'origin' remote");
|
||||||
|
config_lines.push(r#"[remote "origin"]"#.to_owned());
|
||||||
|
config_lines.push(url_line);
|
||||||
|
}
|
||||||
|
tracing::debug!(?config_lines, "updated file");
|
||||||
|
// write config file back out
|
||||||
|
fs.file_write(config_filename, config_lines.join("\n").as_str())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
use super::RepoDetails;
|
use super::RepoDetails;
|
||||||
use super::Result;
|
use super::Result;
|
||||||
pub use crate::repository::open::OpenRepositoryLike;
|
pub use crate::repository::open::OpenRepositoryLike;
|
||||||
use crate::repository::RealOpenRepository;
|
use crate::repository::{Direction, RealOpenRepository};
|
||||||
use derive_more::Deref as _;
|
use derive_more::Deref as _;
|
||||||
use git_next_config::GitDir;
|
|
||||||
use std::sync::{atomic::AtomicBool, Arc, RwLock};
|
use std::sync::{atomic::AtomicBool, Arc, RwLock};
|
||||||
|
|
||||||
#[mockall::automock]
|
#[mockall::automock]
|
||||||
pub trait RepositoryFactory: std::fmt::Debug + Sync + Send {
|
pub trait RepositoryFactory: std::fmt::Debug + Sync + Send {
|
||||||
fn duplicate(&self) -> Box<dyn RepositoryFactory>;
|
fn duplicate(&self) -> Box<dyn RepositoryFactory>;
|
||||||
fn open(&self, gitdir: &GitDir) -> Result<Box<dyn OpenRepositoryLike>>;
|
fn open(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>>;
|
||||||
fn git_clone(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>>;
|
fn git_clone(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,9 +25,13 @@ struct RealRepositoryFactory;
|
||||||
|
|
||||||
#[cfg(not(tarpaulin_include))] // requires network access to either clone new and/or fetch.
|
#[cfg(not(tarpaulin_include))] // requires network access to either clone new and/or fetch.
|
||||||
impl RepositoryFactory for RealRepositoryFactory {
|
impl RepositoryFactory for RealRepositoryFactory {
|
||||||
fn open(&self, gitdir: &GitDir) -> Result<Box<dyn OpenRepositoryLike>> {
|
fn open(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>> {
|
||||||
let gix_repo = gix::ThreadSafeRepository::open(gitdir.to_path_buf())?.to_thread_local();
|
let repo = repo_details
|
||||||
let repo = RealOpenRepository::new(Arc::new(RwLock::new(gix_repo.into())));
|
.open()
|
||||||
|
.map_err(|e| crate::repository::Error::Open(e.to_string()))?;
|
||||||
|
let found = repo.find_default_remote(Direction::Fetch);
|
||||||
|
repo_details.assert_remote_url(found)?;
|
||||||
|
|
||||||
Ok(Box::new(repo))
|
Ok(Box::new(repo))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,7 @@ use super::RepoDetails;
|
||||||
use crate::repository::test::TestRepository;
|
use crate::repository::test::TestRepository;
|
||||||
use crate::validation::remotes::validate_default_remotes;
|
use crate::validation::remotes::validate_default_remotes;
|
||||||
pub use factory::RepositoryFactory;
|
pub use factory::RepositoryFactory;
|
||||||
use git_next_config as config;
|
use git_next_config::{GitDir, RemoteUrl};
|
||||||
use git_next_config::GitDir;
|
|
||||||
pub use open::otest::OnFetch;
|
pub use open::otest::OnFetch;
|
||||||
pub use open::otest::OnPush;
|
pub use open::otest::OnPush;
|
||||||
pub use open::OpenRepository;
|
pub use open::OpenRepository;
|
||||||
|
@ -36,14 +35,13 @@ pub const fn test(fs: kxio::fs::FileSystem) -> TestRepository {
|
||||||
pub fn open(
|
pub fn open(
|
||||||
repository_factory: &dyn factory::RepositoryFactory,
|
repository_factory: &dyn factory::RepositoryFactory,
|
||||||
repo_details: &RepoDetails,
|
repo_details: &RepoDetails,
|
||||||
gitdir: config::GitDir,
|
|
||||||
) -> Result<Box<dyn OpenRepositoryLike>> {
|
) -> Result<Box<dyn OpenRepositoryLike>> {
|
||||||
let open_repository = if !gitdir.exists() {
|
let open_repository = if !repo_details.gitdir.exists() {
|
||||||
info!("Local copy not found - cloning...");
|
info!("Local copy not found - cloning...");
|
||||||
repository_factory.git_clone(repo_details)?
|
repository_factory.git_clone(repo_details)?
|
||||||
} else {
|
} else {
|
||||||
info!("Local copy found - opening...");
|
info!("Local copy found - opening...");
|
||||||
repository_factory.open(&gitdir)?
|
repository_factory.open(repo_details)?
|
||||||
};
|
};
|
||||||
info!("Validating...");
|
info!("Validating...");
|
||||||
validate_default_remotes(&*open_repository, repo_details)
|
validate_default_remotes(&*open_repository, repo_details)
|
||||||
|
@ -89,6 +87,9 @@ pub enum Error {
|
||||||
#[error("invalid git dir: {0}")]
|
#[error("invalid git dir: {0}")]
|
||||||
InvalidGitDir(git_next_config::GitDir),
|
InvalidGitDir(git_next_config::GitDir),
|
||||||
|
|
||||||
|
#[error("kxiofs: {0}")]
|
||||||
|
KxioFs(#[from] kxio::fs::Error),
|
||||||
|
|
||||||
#[error("io: {0}")]
|
#[error("io: {0}")]
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
|
|
||||||
|
@ -112,6 +113,12 @@ pub enum Error {
|
||||||
|
|
||||||
#[error("fake repository lock")]
|
#[error("fake repository lock")]
|
||||||
FakeLock,
|
FakeLock,
|
||||||
|
|
||||||
|
#[error("MismatchDefaultFetchRemote(found: {found:?}, expected: {expected:?})")]
|
||||||
|
MismatchDefaultFetchRemote {
|
||||||
|
found: Box<RemoteUrl>,
|
||||||
|
expected: Box<RemoteUrl>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
mod gix_errors {
|
mod gix_errors {
|
||||||
|
|
|
@ -12,7 +12,7 @@ use std::{
|
||||||
|
|
||||||
use crate as git;
|
use crate as git;
|
||||||
use git::repository::Direction;
|
use git::repository::Direction;
|
||||||
use git_next_config as config;
|
use git_next_config::{self as config, RemoteUrl};
|
||||||
pub use oreal::RealOpenRepository;
|
pub use oreal::RealOpenRepository;
|
||||||
pub use otest::TestOpenRepository;
|
pub use otest::TestOpenRepository;
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ pub fn test(
|
||||||
pub trait OpenRepositoryLike: std::fmt::Debug + Sync {
|
pub trait OpenRepositoryLike: std::fmt::Debug + Sync {
|
||||||
fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
|
fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
|
||||||
fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>>;
|
fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>>;
|
||||||
fn find_default_remote(&self, direction: Direction) -> Option<git::GitRemote>;
|
fn find_default_remote(&self, direction: Direction) -> Option<RemoteUrl>;
|
||||||
fn fetch(&self) -> Result<(), git::fetch::Error>;
|
fn fetch(&self) -> Result<(), git::fetch::Error>;
|
||||||
fn push(
|
fn push(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
use crate::{self as git, repository::OpenRepositoryLike};
|
use crate::{self as git, repository::OpenRepositoryLike};
|
||||||
use config::BranchName;
|
use config::BranchName;
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
use git_next_config as config;
|
use git_next_config::{self as config, RemoteUrl};
|
||||||
|
|
||||||
use gix::bstr::BStr;
|
use gix::bstr::BStr;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -36,17 +36,24 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
||||||
})?;
|
})?;
|
||||||
Ok(refs)
|
Ok(refs)
|
||||||
}
|
}
|
||||||
fn find_default_remote(&self, direction: git::repository::Direction) -> Option<git::GitRemote> {
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
fn find_default_remote(&self, direction: git::repository::Direction) -> Option<RemoteUrl> {
|
||||||
let Ok(repository) = self.0.read() else {
|
let Ok(repository) = self.0.read() else {
|
||||||
#[cfg(not(tarpaulin_include))] // don't test mutex lock failure
|
#[cfg(not(tarpaulin_include))] // don't test mutex lock failure
|
||||||
|
tracing::debug!("no repository");
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let thread_local = repository.to_thread_local();
|
let thread_local = repository.to_thread_local();
|
||||||
let Some(Ok(remote)) = thread_local.find_default_remote(direction.into()) else {
|
let Some(Ok(remote)) = thread_local.find_default_remote(direction.into()) else {
|
||||||
#[cfg(not(tarpaulin_include))] // test is on local repo - should always have remotes
|
#[cfg(not(tarpaulin_include))] // test is on local repo - should always have remotes
|
||||||
|
tracing::debug!("no remote");
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
remote.url(direction.into()).map(Into::into)
|
remote
|
||||||
|
.url(direction.into())
|
||||||
|
.cloned()
|
||||||
|
.and_then(|url| RemoteUrl::try_from(url).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
@ -216,9 +223,9 @@ fn as_gix_error(branch: BranchName) -> impl FnOnce(String) -> git::commit::log::
|
||||||
|error| git::commit::log::Error::Gix { branch, error }
|
|error| git::commit::log::Error::Gix { branch, error }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&gix::Url> for git::GitRemote {
|
impl From<&RemoteUrl> for git::GitRemote {
|
||||||
fn from(url: &gix::Url) -> Self {
|
fn from(url: &RemoteUrl) -> Self {
|
||||||
let host = url.host().unwrap_or_default();
|
let host = url.host.clone().unwrap_or_default();
|
||||||
let path = url.path.to_string();
|
let path = url.path.to_string();
|
||||||
let path = path.strip_prefix('/').map_or(path.as_str(), |path| path);
|
let path = path.strip_prefix('/').map_or(path.as_str(), |path| path);
|
||||||
let path = path.strip_suffix(".git").map_or(path, |path| path);
|
let path = path.strip_suffix(".git").map_or(path, |path| path);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
use crate::{self as git, repository::OpenRepositoryLike};
|
use crate::{self as git, repository::OpenRepositoryLike};
|
||||||
use derive_more::{Constructor, Deref};
|
use derive_more::{Constructor, Deref};
|
||||||
use git_next_config as config;
|
use git_next_config::{self as config, RemoteUrl};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
path::Path,
|
path::Path,
|
||||||
|
@ -73,7 +73,7 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
|
||||||
self.real.remote_branches()
|
self.real.remote_branches()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_default_remote(&self, direction: git::repository::Direction) -> Option<git::GitRemote> {
|
fn find_default_remote(&self, direction: git::repository::Direction) -> Option<RemoteUrl> {
|
||||||
self.real.find_default_remote(direction)
|
self.real.find_default_remote(direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ fn should_fetch_from_repo() {
|
||||||
cwd.join("../.."),
|
cwd.join("../.."),
|
||||||
config::git_dir::StoragePathType::External,
|
config::git_dir::StoragePathType::External,
|
||||||
);
|
);
|
||||||
let_assert!(Ok(repo) = crate::repository::factory::real().open(&gitdir));
|
let repo_details = given::repo_details(&given::a_filesystem()).with_gitdir(gitdir);
|
||||||
|
let_assert!(Ok(repo) = crate::repository::factory::real().open(&repo_details));
|
||||||
let_assert!(Ok(_) = repo.fetch());
|
let_assert!(Ok(_) = repo.fetch());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,13 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn should_find_default_push_remote() {
|
fn should_find_default_push_remote() {
|
||||||
// uses the current repo
|
let fs = given::a_filesystem();
|
||||||
let_assert!(Ok(cwd) = std::env::current_dir());
|
let gitdir = GitDir::new(fs.base().to_path_buf(), config::StoragePathType::External);
|
||||||
let gitdir = config::GitDir::new(
|
let repo_details = given::repo_details(&given::a_filesystem()).with_gitdir(gitdir);
|
||||||
cwd.join("../.."),
|
let url = repo_details.url();
|
||||||
config::git_dir::StoragePathType::External,
|
given::a_bare_repo_with_url(fs.base(), url.expose_secret(), &fs);
|
||||||
); // from ./crate/git directory to the project root
|
let_assert!(Ok(repo) = git::repository::factory::real().open(&repo_details));
|
||||||
let_assert!(Ok(repo) = git::repository::factory::real().open(&gitdir));
|
let remote = repo.find_default_remote(crate::repository::Direction::Push);
|
||||||
let_assert!(Some(remote) = repo.find_default_remote(crate::repository::Direction::Push));
|
assert!(remote.is_some());
|
||||||
assert_eq!(
|
|
||||||
remote,
|
|
||||||
git::GitRemote::new(
|
|
||||||
config::Hostname::new("git.kemitix.net"),
|
|
||||||
config::RepoPath::new("kemitix/git-next".to_string())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use git_next_config::GitDir;
|
use git_next_config::{ApiToken, GitDir, StoragePathType};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -7,46 +7,134 @@ use crate::tests::given;
|
||||||
// clone - can't test are this required a remote server (git_clone only works with https origins)
|
// clone - can't test are this required a remote server (git_clone only works with https origins)
|
||||||
// open
|
// open
|
||||||
// - outside storage path
|
// - outside storage path
|
||||||
|
// - - auth matches
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
fn open_where_storage_external() -> TestResult {
|
fn open_where_storage_external_auth_matches() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
// create a bare repo
|
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::External);
|
||||||
let _x = gix::prepare_clone_bare("https://user:auth@git.kemitix.net/user/repo.git", fs.base())?;
|
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
|
||||||
let gitdir = GitDir::new(
|
use secrecy::ExposeSecret;
|
||||||
fs.base().to_path_buf(),
|
let url = repo_details.url();
|
||||||
git_next_config::git_dir::Provenance::Internal,
|
let url = url.expose_secret();
|
||||||
);
|
given::a_bare_repo_with_url(fs.base(), url, &fs);
|
||||||
let factory = crate::repository::factory::real();
|
let factory = crate::repository::factory::real();
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let result = factory.open(&gitdir);
|
let result = factory.open(&repo_details);
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?result, "open");
|
tracing::debug!(?result, "open");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
// verify origin in .git/config matches url
|
||||||
|
let config = fs.file_read_to_string(&fs.base().join("config"))?;
|
||||||
|
tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth");
|
||||||
|
assert!(config.lines().any(|line| line.contains(url)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
// - outside storage path
|
||||||
|
// - - NEW: auth does not match
|
||||||
|
// - - - do *NOT* change repo config
|
||||||
|
#[test_log::test]
|
||||||
|
fn open_where_storage_external_auth_differs_error() -> TestResult {
|
||||||
|
//given
|
||||||
|
let fs = given::a_filesystem();
|
||||||
|
let repo_details = given::repo_details(&fs);
|
||||||
|
use secrecy::ExposeSecret;
|
||||||
|
let original_url = repo_details.url();
|
||||||
|
let original_url = original_url.expose_secret();
|
||||||
|
given::a_bare_repo_with_url(fs.base(), original_url, &fs);
|
||||||
|
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::External);
|
||||||
|
let factory = crate::repository::factory::real();
|
||||||
|
// create new authentication
|
||||||
|
let api_token = ApiToken::new(given::a_name().into());
|
||||||
|
let forge_details = repo_details.forge.clone().with_token(api_token);
|
||||||
|
let repo_details = repo_details.with_gitdir(gitdir).with_forge(forge_details);
|
||||||
|
|
||||||
|
//when
|
||||||
|
let_assert!(Err(err) = factory.open(&repo_details));
|
||||||
|
|
||||||
|
//then
|
||||||
|
tracing::debug!(?err, "open");
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
git::repository::Error::MismatchDefaultFetchRemote {
|
||||||
|
found: _,
|
||||||
|
expected: _
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
// verify origin in .git/config is unchanged
|
||||||
|
let config = fs.file_read_to_string(&fs.base().join("config"))?;
|
||||||
|
tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth");
|
||||||
|
assert!(config.lines().any(|line| line.contains(original_url))); // the original urk
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
// - in storage path
|
// - in storage path
|
||||||
|
// - - auth matches
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
fn open_where_storage_internal() -> TestResult {
|
fn open_where_storage_internal_auth_matches() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
// create a bare repo
|
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||||
let _x = gix::prepare_clone_bare("https://user:auth@git.kemitix.net/user/repo.git", fs.base())?;
|
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
|
||||||
let gitdir = GitDir::new(
|
use secrecy::ExposeSecret;
|
||||||
fs.base().to_path_buf(),
|
let url = repo_details.url();
|
||||||
git_next_config::git_dir::Provenance::Internal,
|
let url = url.expose_secret();
|
||||||
);
|
// create a bare repg with the auth from the forge_config
|
||||||
|
given::a_bare_repo_with_url(fs.base(), url, &fs);
|
||||||
let factory = crate::repository::factory::real();
|
let factory = crate::repository::factory::real();
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let result = factory.open(&gitdir);
|
let result = factory.open(&repo_details);
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?result, "open");
|
tracing::debug!(?result, "open");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
// verify origin in .git/config matches url
|
||||||
|
let config = fs.file_read_to_string(&fs.base().join("config"))?;
|
||||||
|
tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth");
|
||||||
|
assert!(
|
||||||
|
config.lines().any(|line| line.contains(url)),
|
||||||
|
"Mismatched URL should not be updated"
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
// - - auth matches
|
// - in storage path
|
||||||
// - - NEW: auth does not match
|
// - - NEW: auth does not match
|
||||||
|
// - - - CHANGE repo config to match updated auth
|
||||||
|
#[test_log::test]
|
||||||
|
fn open_where_storage_internal_auth_differs_update_config() -> TestResult {
|
||||||
|
//given
|
||||||
|
let fs = given::a_filesystem();
|
||||||
|
let repo_details = given::repo_details(&fs);
|
||||||
|
use secrecy::ExposeSecret;
|
||||||
|
let original_url = repo_details.url();
|
||||||
|
let original_url = original_url.expose_secret();
|
||||||
|
given::a_bare_repo_with_url(fs.base(), original_url, &fs);
|
||||||
|
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||||
|
let factory = crate::repository::factory::real();
|
||||||
|
// create new authentication
|
||||||
|
let api_token = ApiToken::new(given::a_name().into());
|
||||||
|
let forge_details = repo_details.forge.clone().with_token(api_token);
|
||||||
|
let repo_details = repo_details.with_gitdir(gitdir).with_forge(forge_details);
|
||||||
|
let updated_url = repo_details.url();
|
||||||
|
let updated_url = updated_url.expose_secret().to_lowercase();
|
||||||
|
|
||||||
|
//when
|
||||||
|
let result = factory.open(&repo_details);
|
||||||
|
|
||||||
|
//then
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
// verify origin in .git/config is unchanged
|
||||||
|
let config = fs.file_read_to_string(&fs.base().join("config"))?;
|
||||||
|
tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth");
|
||||||
|
assert!(
|
||||||
|
config
|
||||||
|
.lines()
|
||||||
|
.any(|line| line.to_lowercase().contains(&updated_url)),
|
||||||
|
"Updated URL should be written to config file"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
use crate as git;
|
use crate as git;
|
||||||
|
use assert2::let_assert;
|
||||||
|
|
||||||
|
mod factory;
|
||||||
mod validate;
|
mod validate;
|
||||||
|
|
||||||
|
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
|
@ -10,7 +10,7 @@ fn should_ok_a_valid_repo() {
|
||||||
let mut open_repository = git::repository::open::mock();
|
let mut open_repository = git::repository::open::mock();
|
||||||
open_repository
|
open_repository
|
||||||
.expect_find_default_remote()
|
.expect_find_default_remote()
|
||||||
.returning(move |_direction| Some(repo_details_mock.git_remote()));
|
.returning(move |_direction| repo_details_mock.remote_url());
|
||||||
|
|
||||||
let result = validate_default_remotes(&*open_repository, &repo_details);
|
let result = validate_default_remotes(&*open_repository, &repo_details);
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
|
@ -28,7 +28,7 @@ fn should_fail_where_no_default_push_remote() {
|
||||||
.expect_find_default_remote()
|
.expect_find_default_remote()
|
||||||
.returning(move |direction| match direction {
|
.returning(move |direction| match direction {
|
||||||
Direction::Push => None,
|
Direction::Push => None,
|
||||||
Direction::Fetch => Some(repo_details_mock.git_remote()),
|
Direction::Fetch => repo_details_mock.remote_url(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = validate_default_remotes(&*open_repository, &repo_details);
|
let result = validate_default_remotes(&*open_repository, &repo_details);
|
||||||
|
@ -46,7 +46,7 @@ fn should_fail_where_no_default_fetch_remote() {
|
||||||
open_repository
|
open_repository
|
||||||
.expect_find_default_remote()
|
.expect_find_default_remote()
|
||||||
.returning(move |direction| match direction {
|
.returning(move |direction| match direction {
|
||||||
Direction::Push => Some(repo_details_mock.git_remote()),
|
Direction::Push => repo_details_mock.remote_url(),
|
||||||
Direction::Fetch => None,
|
Direction::Fetch => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -65,8 +65,8 @@ fn should_fail_where_invalid_default_push_remote() {
|
||||||
open_repository
|
open_repository
|
||||||
.expect_find_default_remote()
|
.expect_find_default_remote()
|
||||||
.returning(move |direction| match direction {
|
.returning(move |direction| match direction {
|
||||||
Direction::Push => Some(given::a_git_remote()),
|
Direction::Push => Some(given::a_remote_url()),
|
||||||
Direction::Fetch => Some(repo_details_mock.git_remote()),
|
Direction::Fetch => repo_details_mock.remote_url(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = validate_default_remotes(&*open_repository, &repo_details);
|
let result = validate_default_remotes(&*open_repository, &repo_details);
|
||||||
|
@ -84,8 +84,8 @@ fn should_fail_where_invalid_default_fetch_remote() {
|
||||||
open_repository
|
open_repository
|
||||||
.expect_find_default_remote()
|
.expect_find_default_remote()
|
||||||
.returning(move |direction| match direction {
|
.returning(move |direction| match direction {
|
||||||
Direction::Push => Some(repo_details_mock.git_remote()),
|
Direction::Push => repo_details_mock.remote_url(),
|
||||||
Direction::Fetch => Some(given::a_git_remote()),
|
Direction::Fetch => Some(given::a_remote_url()),
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = validate_default_remotes(&*open_repository, &repo_details);
|
let result = validate_default_remotes(&*open_repository, &repo_details);
|
||||||
|
|
|
@ -214,7 +214,7 @@ mod repo_details {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub mod given {
|
pub mod given {
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::{self as git, repository::open::MockOpenRepositoryLike};
|
use crate::{self as git, repository::open::MockOpenRepositoryLike};
|
||||||
use config::{
|
use config::{
|
||||||
|
@ -270,9 +270,9 @@ pub mod given {
|
||||||
pub fn a_forge_config() -> ForgeConfig {
|
pub fn a_forge_config() -> ForgeConfig {
|
||||||
ForgeConfig::new(
|
ForgeConfig::new(
|
||||||
ForgeType::MockForge,
|
ForgeType::MockForge,
|
||||||
a_name(),
|
format!("hostname-{}", a_name()),
|
||||||
a_name(),
|
format!("user-{}", a_name()),
|
||||||
a_name(),
|
format!("token-{}", a_name()),
|
||||||
Default::default(), // no repos
|
Default::default(), // no repos
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -369,6 +369,32 @@ pub mod given {
|
||||||
config::RepoPath::new(given::a_name()),
|
config::RepoPath::new(given::a_name()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
pub fn a_bare_repo_with_url(path: &Path, url: &str, fs: &kxio::fs::FileSystem) {
|
||||||
|
// create a basic bare repo
|
||||||
|
let repo = gix::prepare_clone_bare(url, fs.base()).unwrap();
|
||||||
|
repo.persist();
|
||||||
|
// load config file
|
||||||
|
let config_file = fs.file_read_to_string(&path.join("config")).unwrap();
|
||||||
|
// add use are origin url
|
||||||
|
let mut config_lines = config_file.lines().collect::<Vec<_>>();
|
||||||
|
config_lines.push(r#"[remote "origin"]"#);
|
||||||
|
let url_line = format!(r#" url = "{}""#, url);
|
||||||
|
tracing::info!(?url, %url_line, "writing");
|
||||||
|
config_lines.push(&url_line);
|
||||||
|
// write config file back out
|
||||||
|
fs.file_write(&path.join("config"), config_lines.join("\n").as_str())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
pub fn a_remote_url() -> git_next_config::RemoteUrl {
|
||||||
|
let hostname = given::a_hostname();
|
||||||
|
let owner = given::a_name();
|
||||||
|
let repo = given::a_name();
|
||||||
|
git_next_config::RemoteUrl::parse(format!("git@{hostname}:{owner}/{repo}.git")).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub mod then {
|
pub mod then {
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use git_next_config::RemoteUrl;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate as git;
|
use crate as git;
|
||||||
|
@ -13,18 +14,22 @@ pub fn validate_default_remotes(
|
||||||
let fetch_remote = open_repository
|
let fetch_remote = open_repository
|
||||||
.find_default_remote(git::repository::Direction::Fetch)
|
.find_default_remote(git::repository::Direction::Fetch)
|
||||||
.ok_or_else(|| Error::NoDefaultFetchRemote)?;
|
.ok_or_else(|| Error::NoDefaultFetchRemote)?;
|
||||||
let git_remote = repo_details.git_remote();
|
let Some(remote_url) = repo_details.remote_url() else {
|
||||||
info!(config = %git_remote, push = %push_remote, fetch = %fetch_remote, "Check remotes match");
|
return Err(git::validation::remotes::Error::UnableToOpenRepo(
|
||||||
if git_remote != push_remote {
|
"Unable to build forge url".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
info!(config = %remote_url, push = %push_remote, fetch = %fetch_remote, "Check remotes match");
|
||||||
|
if remote_url != push_remote {
|
||||||
return Err(Error::MismatchDefaultPushRemote {
|
return Err(Error::MismatchDefaultPushRemote {
|
||||||
found: push_remote,
|
found: push_remote,
|
||||||
expected: git_remote,
|
expected: remote_url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if git_remote != fetch_remote {
|
if remote_url != fetch_remote {
|
||||||
return Err(Error::MismatchDefaultFetchRemote {
|
return Err(Error::MismatchDefaultFetchRemote {
|
||||||
found: fetch_remote,
|
found: fetch_remote,
|
||||||
expected: git_remote,
|
expected: remote_url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -53,13 +58,16 @@ pub enum Error {
|
||||||
|
|
||||||
#[error("MismatchDefaultPushRemote(found: {found}, expected: {expected})")]
|
#[error("MismatchDefaultPushRemote(found: {found}, expected: {expected})")]
|
||||||
MismatchDefaultPushRemote {
|
MismatchDefaultPushRemote {
|
||||||
found: git::GitRemote,
|
found: RemoteUrl,
|
||||||
expected: git::GitRemote,
|
expected: RemoteUrl,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("MismatchDefaultFetchRemote(found: {found}, expected: {expected})")]
|
#[error("MismatchDefaultFetchRemote(found: {found}, expected: {expected})")]
|
||||||
MismatchDefaultFetchRemote {
|
MismatchDefaultFetchRemote {
|
||||||
found: git::GitRemote,
|
found: RemoteUrl,
|
||||||
expected: git::GitRemote,
|
expected: RemoteUrl,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[error("Unable to open repo")]
|
||||||
|
GixOpen(#[from] gix::open::Error),
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ mod repos {
|
||||||
fn where_repo_has_no_push_remote() {
|
fn where_repo_has_no_push_remote() {
|
||||||
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
|
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
|
||||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||||
|
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
|
||||||
let mut mock_open_repository = git::repository::open::mock();
|
let mut mock_open_repository = git::repository::open::mock();
|
||||||
mock_open_repository
|
mock_open_repository
|
||||||
.expect_find_default_remote()
|
.expect_find_default_remote()
|
||||||
|
@ -29,9 +30,8 @@ mod repos {
|
||||||
repository_factory
|
repository_factory
|
||||||
.expect_open()
|
.expect_open()
|
||||||
.return_once(move |_| Ok(mock_open_repository));
|
.return_once(move |_| Ok(mock_open_repository));
|
||||||
let repo_details = given::repo_details(&fs);
|
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Ok(open_repository) = repository_factory.open(&gitdir),
|
Ok(open_repository) = repository_factory.open(&repo_details),
|
||||||
"open repo"
|
"open repo"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -62,6 +62,7 @@ mod positions {
|
||||||
fn where_fetch_fails_should_error() {
|
fn where_fetch_fails_should_error() {
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||||
|
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
|
||||||
let mut mock_open_repository = git::repository::open::mock();
|
let mut mock_open_repository = git::repository::open::mock();
|
||||||
mock_open_repository
|
mock_open_repository
|
||||||
.expect_fetch()
|
.expect_fetch()
|
||||||
|
@ -71,10 +72,9 @@ mod positions {
|
||||||
.expect_open()
|
.expect_open()
|
||||||
.return_once(move |_| Ok(mock_open_repository));
|
.return_once(move |_| Ok(mock_open_repository));
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Ok(repository) = repository_factory.open(&gitdir),
|
Ok(repository) = repository_factory.open(&repo_details),
|
||||||
"open repo"
|
"open repo"
|
||||||
);
|
);
|
||||||
let repo_details = given::repo_details(&fs); //.with_gitdir(gitdir);
|
|
||||||
let repo_config = given::a_repo_config();
|
let repo_config = given::a_repo_config();
|
||||||
|
|
||||||
let result = validate_positions(&*repository, &repo_details, repo_config);
|
let result = validate_positions(&*repository, &repo_details, repo_config);
|
||||||
|
@ -91,6 +91,7 @@ mod positions {
|
||||||
fn where_main_branch_is_missing_or_commit_log_is_empty_should_error() {
|
fn where_main_branch_is_missing_or_commit_log_is_empty_should_error() {
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||||
|
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
|
||||||
let repo_config = given::a_repo_config();
|
let repo_config = given::a_repo_config();
|
||||||
let main_branch = repo_config.branches().main();
|
let main_branch = repo_config.branches().main();
|
||||||
let mut mock_open_repository = git::repository::open::mock();
|
let mut mock_open_repository = git::repository::open::mock();
|
||||||
|
@ -112,10 +113,9 @@ mod positions {
|
||||||
.expect_open()
|
.expect_open()
|
||||||
.return_once(move |_| Ok(mock_open_repository));
|
.return_once(move |_| Ok(mock_open_repository));
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Ok(open_repository) = repository_factory.open(&gitdir),
|
Ok(open_repository) = repository_factory.open(&repo_details),
|
||||||
"open repo"
|
"open repo"
|
||||||
);
|
);
|
||||||
let repo_details = given::repo_details(&fs);
|
|
||||||
|
|
||||||
let result = validate_positions(&*open_repository, &repo_details, repo_config);
|
let result = validate_positions(&*open_repository, &repo_details, repo_config);
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
|
@ -130,6 +130,7 @@ mod positions {
|
||||||
fn where_next_branch_is_missing_or_commit_log_is_empty_should_error() {
|
fn where_next_branch_is_missing_or_commit_log_is_empty_should_error() {
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||||
|
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
|
||||||
let repo_config = given::a_repo_config();
|
let repo_config = given::a_repo_config();
|
||||||
let next_branch = repo_config.branches().next();
|
let next_branch = repo_config.branches().next();
|
||||||
let mut mock_open_repository = git::repository::open::mock();
|
let mut mock_open_repository = git::repository::open::mock();
|
||||||
|
@ -151,10 +152,9 @@ mod positions {
|
||||||
.expect_open()
|
.expect_open()
|
||||||
.return_once(move |_| Ok(mock_open_repository));
|
.return_once(move |_| Ok(mock_open_repository));
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Ok(open_repository) = repository_factory.open(&gitdir),
|
Ok(open_repository) = repository_factory.open(&repo_details),
|
||||||
"open repo"
|
"open repo"
|
||||||
);
|
);
|
||||||
let repo_details = given::repo_details(&fs);
|
|
||||||
|
|
||||||
let result = validate_positions(&*open_repository, &repo_details, repo_config);
|
let result = validate_positions(&*open_repository, &repo_details, repo_config);
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
|
@ -169,6 +169,7 @@ mod positions {
|
||||||
fn where_dev_branch_is_missing_or_commit_log_is_empty_should_error() {
|
fn where_dev_branch_is_missing_or_commit_log_is_empty_should_error() {
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||||
|
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
|
||||||
let repo_config = given::a_repo_config();
|
let repo_config = given::a_repo_config();
|
||||||
let dev_branch = repo_config.branches().dev();
|
let dev_branch = repo_config.branches().dev();
|
||||||
let mut mock_open_repository = git::repository::open::mock();
|
let mut mock_open_repository = git::repository::open::mock();
|
||||||
|
@ -190,10 +191,9 @@ mod positions {
|
||||||
.expect_open()
|
.expect_open()
|
||||||
.return_once(move |_| Ok(mock_open_repository));
|
.return_once(move |_| Ok(mock_open_repository));
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Ok(open_repository) = repository_factory.open(&gitdir),
|
Ok(open_repository) = repository_factory.open(&repo_details),
|
||||||
"open repo"
|
"open repo"
|
||||||
);
|
);
|
||||||
let repo_details = given::repo_details(&fs);
|
|
||||||
|
|
||||||
let result = validate_positions(&*open_repository, &repo_details, repo_config);
|
let result = validate_positions(&*open_repository, &repo_details, repo_config);
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
|
|
|
@ -14,8 +14,7 @@ impl Handler<actor::messages::CloneRepo> for actor::RepoActor {
|
||||||
) -> Self::Result {
|
) -> Self::Result {
|
||||||
actor::logger(&self.log, "Handler: CloneRepo: start");
|
actor::logger(&self.log, "Handler: CloneRepo: start");
|
||||||
tracing::debug!("Handler: CloneRepo: start");
|
tracing::debug!("Handler: CloneRepo: start");
|
||||||
let gitdir = self.repo_details.gitdir.clone();
|
match git::repository::open(&*self.repository_factory, &self.repo_details) {
|
||||||
match git::repository::open(&*self.repository_factory, &self.repo_details, gitdir) {
|
|
||||||
Ok(repository) => {
|
Ok(repository) => {
|
||||||
actor::logger(&self.log, "open okay");
|
actor::logger(&self.log, "open okay");
|
||||||
tracing::debug!("open okay");
|
tracing::debug!("open okay");
|
||||||
|
|
|
@ -38,7 +38,7 @@ impl From<config::RegisteredWebhook> for WebhookRegistered {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newtype!(MessageToken: u32, Copy, Default, Display: r#"An incremental token used to identify the current set of messages.
|
newtype!(MessageToken: u32, Copy, Default, Display, PartialOrd, Ord: r#"An incremental token used to identify the current set of messages.
|
||||||
|
|
||||||
Primarily used by [ValidateRepo] to reduce duplicate messages. The token is incremented when a new Webhook message is
|
Primarily used by [ValidateRepo] to reduce duplicate messages. The token is incremented when a new Webhook message is
|
||||||
received, marking that message the latest, and causing any previous messages still being processed to be dropped when
|
received, marking that message the latest, and causing any previous messages still being processed to be dropped when
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use config::git_dir::StoragePathType;
|
use config::git_dir::StoragePathType;
|
||||||
|
use git_next_config::RemoteUrl;
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -10,15 +11,15 @@ pub fn has_all_valid_remote_defaults(
|
||||||
has_remote_defaults(
|
has_remote_defaults(
|
||||||
open_repository,
|
open_repository,
|
||||||
HashMap::from([
|
HashMap::from([
|
||||||
(Direction::Push, Some(repo_details.git_remote())),
|
(Direction::Push, repo_details.remote_url()),
|
||||||
(Direction::Fetch, Some(repo_details.git_remote())),
|
(Direction::Fetch, repo_details.remote_url()),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_remote_defaults(
|
pub fn has_remote_defaults(
|
||||||
open_repository: &mut MockOpenRepositoryLike,
|
open_repository: &mut MockOpenRepositoryLike,
|
||||||
remotes: HashMap<Direction, Option<GitRemote>>,
|
remotes: HashMap<Direction, Option<RemoteUrl>>,
|
||||||
) {
|
) {
|
||||||
remotes.into_iter().for_each(|(direction, remote)| {
|
remotes.into_iter().for_each(|(direction, remote)| {
|
||||||
open_repository
|
open_repository
|
||||||
|
|
|
@ -42,13 +42,8 @@ async fn should_clone() -> TestResult {
|
||||||
async fn should_open() -> TestResult {
|
async fn should_open() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (mut open_repository, /* mut */ repo_details) = given::an_open_repository(&fs);
|
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||||
// #[allow(clippy::unwrap_used)]
|
|
||||||
// let repo_config = repo_details.repo_config.take().unwrap();
|
|
||||||
|
|
||||||
given::has_all_valid_remote_defaults(&mut open_repository, &repo_details);
|
given::has_all_valid_remote_defaults(&mut open_repository, &repo_details);
|
||||||
// handles_validate_repo_message(&mut open_repository, repo_config.branches());
|
|
||||||
|
|
||||||
// factory opens a repository
|
// factory opens a repository
|
||||||
let mut repository_factory = MockRepositoryFactory::new();
|
let mut repository_factory = MockRepositoryFactory::new();
|
||||||
let opened = Arc::new(RwLock::new(vec![]));
|
let opened = Arc::new(RwLock::new(vec![]));
|
||||||
|
@ -139,7 +134,7 @@ async fn opened_repo_with_no_default_push_should_not_proceed() -> TestResult {
|
||||||
&mut open_repository,
|
&mut open_repository,
|
||||||
HashMap::from([
|
HashMap::from([
|
||||||
(Direction::Push, None),
|
(Direction::Push, None),
|
||||||
(Direction::Fetch, Some(repo_details.git_remote())),
|
(Direction::Fetch, repo_details.remote_url()),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -167,7 +162,7 @@ async fn opened_repo_with_no_default_fetch_should_not_proceed() -> TestResult {
|
||||||
given::has_remote_defaults(
|
given::has_remote_defaults(
|
||||||
&mut open_repository,
|
&mut open_repository,
|
||||||
HashMap::from([
|
HashMap::from([
|
||||||
(Direction::Push, Some(repo_details.git_remote())),
|
(Direction::Push, repo_details.remote_url()),
|
||||||
(Direction::Fetch, None),
|
(Direction::Fetch, None),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
|
@ -354,6 +354,7 @@ async fn should_reject_message_with_expired_token() -> TestResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(actix::test)]
|
||||||
|
// NOTE: failed then passed on retry: count = 1
|
||||||
async fn should_send_validate_repo_when_retryable_error() -> TestResult {
|
async fn should_send_validate_repo_when_retryable_error() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
|
|
@ -14,7 +14,7 @@ use config::{
|
||||||
};
|
};
|
||||||
use git::{
|
use git::{
|
||||||
repository::{factory::MockRepositoryFactory, open::MockOpenRepositoryLike, Direction},
|
repository::{factory::MockRepositoryFactory, open::MockOpenRepositoryLike, Direction},
|
||||||
Generation, GitRemote, MockForgeLike, RepoDetails,
|
Generation, MockForgeLike, RepoDetails,
|
||||||
};
|
};
|
||||||
use git_next_actor_macros::message;
|
use git_next_actor_macros::message;
|
||||||
use git_next_config as config;
|
use git_next_config as config;
|
||||||
|
|
Loading…
Reference in a new issue