use std::convert::TryInto; use gix_macros::momo; use crate::{ bstr::{BStr, BString}, config, remote, Remote, }; /// The error returned by [`Remote::save_to()`]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("The remote pointing to {} is anonymous and can't be saved.", url.to_bstring())] NameMissing { url: gix_url::Url }, } /// The error returned by [`Remote::save_as_to()`]. /// /// Note that this type should rather be in the `as` module, but cannot be as it's part of the Rust syntax. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum AsError { #[error(transparent)] Save(#[from] Error), #[error(transparent)] Name(#[from] crate::remote::name::Error), } /// Serialize into git-config. impl Remote<'_> { /// Save ourselves to the given `config` if we are a named remote or fail otherwise. /// /// Note that all sections named `remote ""` will be cleared of all values we are about to write, /// and the last `remote ""` section will be containing all relevant values so that reloading the remote /// from `config` would yield the same in-memory state. #[allow(clippy::result_large_err)] pub fn save_to(&self, config: &mut gix_config::File<'static>) -> Result<(), Error> { fn as_key(name: &str) -> gix_config::parse::section::Key<'_> { name.try_into().expect("valid") } let name = self.name().ok_or_else(|| Error::NameMissing { url: self .url .as_ref() .or(self.push_url.as_ref()) .expect("one url is always set") .to_owned(), })?; if let Some(section_ids) = config.sections_and_ids_by_name("remote").map(|it| { it.filter_map(|(s, id)| (s.header().subsection_name() == Some(name.as_bstr())).then_some(id)) .collect::>() }) { let mut sections_to_remove = Vec::new(); const KEYS_TO_REMOVE: &[&str] = &[ config::tree::Remote::URL.name, config::tree::Remote::PUSH_URL.name, config::tree::Remote::FETCH.name, config::tree::Remote::PUSH.name, config::tree::Remote::TAG_OPT.name, ]; for id in section_ids { let mut section = config.section_mut_by_id(id).expect("just queried"); let was_empty = section.num_values() == 0; for key in KEYS_TO_REMOVE { while section.remove(key).is_some() {} } let is_empty_after_deletions_of_values_to_be_written = section.num_values() == 0; if !was_empty && is_empty_after_deletions_of_values_to_be_written { sections_to_remove.push(id); } } for id in sections_to_remove { config.remove_section_by_id(id); } } let mut section = config .section_mut_or_create_new("remote", Some(name.as_ref())) .expect("section name is validated and 'remote' is acceptable"); if let Some(url) = self.url.as_ref() { section.push(as_key("url"), Some(url.to_bstring().as_ref())); } if let Some(url) = self.push_url.as_ref() { section.push(as_key("pushurl"), Some(url.to_bstring().as_ref())); } if self.fetch_tags != Default::default() { section.push( as_key(config::tree::Remote::TAG_OPT.name), BStr::new(match self.fetch_tags { remote::fetch::Tags::All => "--tags", remote::fetch::Tags::None => "--no-tags", remote::fetch::Tags::Included => unreachable!("BUG: the default shouldn't be written and we try"), }) .into(), ); } for (key, spec) in self .fetch_specs .iter() .map(|spec| ("fetch", spec)) .chain(self.push_specs.iter().map(|spec| ("push", spec))) { section.push(as_key(key), Some(spec.to_ref().to_bstring().as_ref())); } Ok(()) } /// Forcefully set our name to `name` and write our state to `config` similar to [`save_to()`][Self::save_to()]. /// /// Note that this sets a name for anonymous remotes, but overwrites the name for those who were named before. /// If this name is different from the current one, the git configuration will still contain the previous name, /// and the caller should account for that. #[allow(clippy::result_large_err)] #[momo] pub fn save_as_to( &mut self, name: impl Into, config: &mut gix_config::File<'static>, ) -> Result<(), AsError> { let name = crate::remote::name::validated(name)?; let prev_name = self.name.take(); self.name = Some(name.into()); self.save_to(config).map_err(|err| { self.name = prev_name; err.into() }) } }