forked from kemitix/git-next
Paul Campbell
57a614bad3
The git config files of external repos are read-only. This is the only place where we make reference to a remote named 'origin', so this also closes kemitix/git-next#85. Closes kemitix/git-next#85
172 lines
6 KiB
Rust
172 lines
6 KiB
Rust
use std::sync::{Arc, RwLock};
|
|
|
|
use config::{
|
|
BranchName, ForgeAlias, ForgeConfig, ForgeDetails, GitDir, RepoAlias, RepoConfig, RepoPath,
|
|
ServerRepoConfig, StoragePathType,
|
|
};
|
|
use git_next_config::{self as config, pike, RemoteUrl};
|
|
use secrecy::Secret;
|
|
|
|
use crate::repository::{OpenRepositoryLike, RealOpenRepository};
|
|
|
|
use super::{Generation, GitRemote};
|
|
|
|
/// The derived information about a repo, used to interact with it
|
|
#[derive(Clone, Debug, derive_more::Display, derive_with::With)]
|
|
#[display("gen-{}:{}:{}/{}", generation, forge.forge_type(), forge.forge_alias(), repo_alias )]
|
|
pub struct RepoDetails {
|
|
pub generation: Generation,
|
|
pub repo_alias: RepoAlias,
|
|
pub repo_path: RepoPath,
|
|
pub branch: BranchName,
|
|
pub forge: ForgeDetails,
|
|
pub repo_config: Option<RepoConfig>,
|
|
pub gitdir: GitDir,
|
|
}
|
|
impl RepoDetails {
|
|
pub fn new(
|
|
generation: Generation,
|
|
repo_alias: &RepoAlias,
|
|
server_repo_config: &ServerRepoConfig,
|
|
forge_alias: &ForgeAlias,
|
|
forge_config: &ForgeConfig,
|
|
gitdir: GitDir,
|
|
) -> Self {
|
|
Self {
|
|
generation,
|
|
repo_alias: repo_alias.clone(),
|
|
repo_path: server_repo_config.repo(),
|
|
repo_config: server_repo_config.repo_config(),
|
|
branch: server_repo_config.branch(),
|
|
gitdir,
|
|
forge: ForgeDetails::new(
|
|
forge_alias.clone(),
|
|
forge_config.forge_type(),
|
|
forge_config.hostname(),
|
|
forge_config.user(),
|
|
forge_config.token(),
|
|
),
|
|
}
|
|
}
|
|
pub fn origin(&self) -> secrecy::Secret<String> {
|
|
let repo_details = self;
|
|
let user = &repo_details.forge.user();
|
|
let hostname = &repo_details.forge.hostname();
|
|
let repo_path = &repo_details.repo_path;
|
|
let expose_secret = repo_details.forge.token();
|
|
use secrecy::ExposeSecret;
|
|
let token = expose_secret.expose_secret();
|
|
let origin = format!("https://{user}:{token}@{hostname}/{repo_path}.git");
|
|
origin.into()
|
|
}
|
|
|
|
pub const fn gitdir(&self) -> &GitDir {
|
|
&self.gitdir
|
|
}
|
|
|
|
pub fn git_remote(&self) -> GitRemote {
|
|
GitRemote::new(self.forge.hostname().clone(), self.repo_path.clone())
|
|
}
|
|
|
|
pub fn with_hostname(mut self, hostname: config::Hostname) -> Self {
|
|
let forge = self.forge;
|
|
self.forge = forge.with_hostname(hostname);
|
|
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 Ok(());
|
|
}
|
|
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(())
|
|
}
|
|
}
|