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, 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 { 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 { 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 { 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 { use secrecy::ExposeSecret; RemoteUrl::parse(self.url().expose_secret()) } #[tracing::instrument] pub fn assert_remote_url(&self, found: Option) -> 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::>(); 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(()) } }