Skip to main content

gix_config/file/access/
mutate.rs

1use bstr::BStr;
2use gix_error::ExnMessageResult;
3use gix_error::ExnResult;
4use gix_features::threading::OwnShared;
5
6use crate::{
7    AsBStrOpt, File,
8    file::{self, IntoBStringOpt, Metadata, SectionId, SectionMut, write::ends_with_newline},
9    lookup,
10    parse::{Event, FrontMatterEvents, Span, section},
11};
12
13impl IntoBStringOpt for Option<bstr::BString> {
14    fn into_bstring_opt(self) -> Option<bstr::BString> {
15        self
16    }
17}
18
19impl IntoBStringOpt for bstr::BString {
20    fn into_bstring_opt(self) -> Option<bstr::BString> {
21        Some(self)
22    }
23}
24
25impl IntoBStringOpt for String {
26    fn into_bstring_opt(self) -> Option<bstr::BString> {
27        Some(self.into())
28    }
29}
30
31impl IntoBStringOpt for Vec<u8> {
32    fn into_bstring_opt(self) -> Option<bstr::BString> {
33        Some(self.into())
34    }
35}
36
37impl<const N: usize> IntoBStringOpt for [u8; N] {
38    fn into_bstring_opt(self) -> Option<bstr::BString> {
39        Some(self.to_vec().into())
40    }
41}
42
43impl<T: crate::AsBStr + ?Sized> IntoBStringOpt for &T {
44    fn into_bstring_opt(self) -> Option<bstr::BString> {
45        Some(self.as_bstr().to_owned())
46    }
47}
48
49/// Mutating low-level access methods.
50impl File {
51    /// Returns the last mutable section with a given `name` and optional `subsection_name`, _if it exists_.
52    pub fn section_mut(&mut self, name: impl AsRef<str>, subsection_name: impl AsBStrOpt) -> ExnResult<SectionMut<'_>> {
53        self.section_mut_inner(name.as_ref(), subsection_name.as_bstr_opt())
54    }
55
56    fn section_mut_inner<'a>(&'a mut self, name: &str, subsection_name: Option<&BStr>) -> ExnResult<SectionMut<'a>> {
57        let id = self
58            .section_ids_by_name_and_subname(name, subsection_name)?
59            .next_back()
60            .expect("BUG: Section lookup vec was empty");
61        let nl = self.detect_newline_style_smallvec();
62        Ok(self
63            .section_mut_from_id(id, nl)
64            .expect("BUG: Section did not have id from lookup"))
65    }
66
67    /// Returns the last found mutable section with a given `key`, identifying the name and subsection name like `core` or `remote.origin`.
68    pub fn section_mut_by_key(&mut self, key: impl crate::AsBStr) -> ExnResult<SectionMut<'_>> {
69        let key = section::unvalidated::KeyRef::parse(&key).ok_or_else(lookup::existing::key_missing)?;
70        self.section_mut_inner(key.section_name, key.subsection_name)
71    }
72
73    /// Return the mutable section identified by `id`, or `None` if it didn't exist.
74    ///
75    /// Note that `id` is stable across deletions and insertions.
76    pub fn section_mut_by_id(&mut self, id: SectionId) -> Option<SectionMut<'_>> {
77        let nl = self.detect_newline_style_smallvec();
78        self.section_mut_from_id(id, nl)
79    }
80
81    /// Returns the last mutable section with a given `name` and optional `subsection_name`, _if it exists_, or create a new section.
82    pub fn section_mut_or_create_new(
83        &mut self,
84        name: impl AsRef<str>,
85        subsection_name: impl AsBStrOpt,
86    ) -> ExnMessageResult<SectionMut<'_>> {
87        self.section_mut_or_create_new_inner(name.as_ref(), subsection_name.as_bstr_opt())
88    }
89
90    pub(crate) fn section_mut_or_create_new_inner<'a>(
91        &'a mut self,
92        name: &str,
93        subsection_name: Option<&BStr>,
94    ) -> ExnMessageResult<SectionMut<'a>> {
95        self.section_mut_or_create_new_filter_inner(name, subsection_name, |_| true)
96    }
97
98    /// Returns an mutable section with a given `name` and optional `subsection_name`, _if it exists_ **and** passes `filter`, or create
99    /// a new section.
100    pub fn section_mut_or_create_new_filter(
101        &mut self,
102        name: impl AsRef<str>,
103        subsection_name: impl AsBStrOpt,
104        filter: impl FnMut(&Metadata) -> bool,
105    ) -> ExnMessageResult<SectionMut<'_>> {
106        self.section_mut_or_create_new_filter_inner(name.as_ref(), subsection_name.as_bstr_opt(), filter)
107    }
108
109    pub(crate) fn section_mut_or_create_new_filter_inner<'a>(
110        &'a mut self,
111        name: &str,
112        subsection_name: Option<&BStr>,
113        mut filter: impl FnMut(&Metadata) -> bool,
114    ) -> ExnMessageResult<SectionMut<'a>> {
115        match self
116            .section_ids_by_name_and_subname(name, subsection_name)
117            .ok()
118            .and_then(|it| {
119                it.rev()
120                    .find(|id| self.sections.get(id).is_some_and(|s| filter(&s.meta)))
121            }) {
122            Some(id) => {
123                let nl = self.detect_newline_style_smallvec();
124                Ok(self
125                    .section_mut_from_id(id, nl)
126                    .expect("BUG: Section did not have id from lookup"))
127            }
128            None => self.new_section_inner(name, subsection_name.map(bstr::BString::from)),
129        }
130    }
131
132    /// Returns the last found mutable section with a given `name` and optional `subsection_name`, that matches `filter`, _if it exists_.
133    ///
134    /// If there are sections matching `section_name` and `subsection_name` but the `filter` rejects all of them, `Ok(None)`
135    /// is returned.
136    pub fn section_mut_filter(
137        &mut self,
138        name: impl AsRef<str>,
139        subsection_name: impl AsBStrOpt,
140        filter: impl FnMut(&Metadata) -> bool,
141    ) -> ExnResult<Option<file::SectionMut<'_>>> {
142        self.section_mut_filter_inner(name.as_ref(), subsection_name.as_bstr_opt(), filter)
143    }
144
145    fn section_mut_filter_inner<'a>(
146        &'a mut self,
147        name: &str,
148        subsection_name: Option<&BStr>,
149        mut filter: impl FnMut(&Metadata) -> bool,
150    ) -> ExnResult<Option<file::SectionMut<'a>>> {
151        let id = self
152            .section_ids_by_name_and_subname(name, subsection_name)?
153            .rev()
154            .find(|id| {
155                let s = &self.sections[id];
156                filter(&s.meta)
157            });
158        let nl = self.detect_newline_style_smallvec();
159        Ok(id.and_then(move |id| self.section_mut_from_id(id, nl)))
160    }
161
162    /// Like [`section_mut_filter()`][File::section_mut_filter()], but identifies the with a given `key`,
163    /// like `core` or `remote.origin`.
164    pub fn section_mut_filter_by_key(
165        &mut self,
166        key: impl crate::AsBStr,
167        filter: impl FnMut(&Metadata) -> bool,
168    ) -> ExnResult<Option<file::SectionMut<'_>>> {
169        let key = section::unvalidated::KeyRef::parse(&key).ok_or_else(lookup::existing::key_missing)?;
170        self.section_mut_filter_inner(key.section_name, key.subsection_name, filter)
171    }
172
173    /// Adds a new section. If a subsection name was provided, then
174    /// the generated header will use the modern subsection syntax.
175    /// Returns a reference to the new section for immediate editing.
176    ///
177    /// # Examples
178    ///
179    /// Creating a new empty section:
180    ///
181    /// ```
182    /// # use gix_config::File;
183    /// # use std::convert::TryFrom;
184    /// let mut git_config = gix_config::File::default();
185    /// let section = git_config.new_section("hello", "world")?;
186    /// let nl = section.newline().to_owned();
187    /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}"));
188    /// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
189    /// ```
190    ///
191    /// Creating a new empty section and adding values to it:
192    ///
193    /// ```
194    /// # use gix_config::File;
195    /// # use std::convert::TryFrom;
196    /// # use bstr::ByteSlice;
197    /// # use gix_config::parse::section;
198    /// let mut git_config = gix_config::File::default();
199    /// let mut section = git_config.new_section("hello", "world")?;
200    /// section.push("a", Some("b".into()))?;
201    /// let nl = section.newline().to_owned();
202    /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}\ta = b{nl}"));
203    /// let _section = git_config.new_section("core", None);
204    /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}\ta = b{nl}[core]{nl}"));
205    /// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
206    /// ```
207    pub fn new_section(
208        &mut self,
209        name: impl AsRef<str>,
210        subsection: impl IntoBStringOpt,
211    ) -> ExnMessageResult<SectionMut<'_>> {
212        self.new_section_inner(name.as_ref(), subsection.into_bstring_opt())
213    }
214
215    fn new_section_inner(&mut self, name: &str, subsection: Option<bstr::BString>) -> ExnMessageResult<SectionMut<'_>> {
216        let section = file::SectionData::new(name, subsection, OwnShared::clone(&self.meta), &mut self.backing)?;
217        let id = self.push_section_internal(section);
218        let nl = self.detect_newline_style_smallvec();
219        let mut section = self.section_mut_from_id(id, nl).expect("each id yields a section");
220        section.push_newline()?;
221        Ok(section)
222    }
223
224    /// Removes the section with `name` and `subsection_name`, returning it if there was a matching section.
225    /// If multiple sections have the same name, then the last one is returned. Note that
226    /// later sections with the same name have precedent over earlier ones.
227    ///
228    /// # Examples
229    ///
230    /// Creating and removing a section:
231    ///
232    /// ```
233    /// # use gix_config::File;
234    /// # use std::convert::TryFrom;
235    /// let mut git_config = gix_config::File::try_from(
236    /// r#"[hello "world"]
237    ///     some-value = 4
238    /// "#)?;
239    ///
240    /// let section = git_config.remove_section("hello", "world");
241    /// assert!(section.is_some());
242    /// assert_eq!(git_config.to_string(), "");
243    /// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
244    /// ```
245    ///
246    /// Precedence example for removing sections with the same name:
247    ///
248    /// ```
249    /// # use gix_config::File;
250    /// # use std::convert::TryFrom;
251    /// let mut git_config = gix_config::File::try_from(
252    /// r#"[hello "world"]
253    ///     some-value = 4
254    /// [hello "world"]
255    ///     some-value = 5
256    /// "#)?;
257    ///
258    /// let section = git_config.remove_section("hello", "world");
259    /// assert!(section.is_some());
260    /// assert_eq!(git_config.to_string(), "[hello \"world\"]\n    some-value = 4\n");
261    /// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
262    /// ```
263    pub fn remove_section(&mut self, name: impl AsRef<str>, subsection_name: impl AsBStrOpt) -> Option<file::Section> {
264        let id = self
265            .section_ids_by_name_and_subname(name.as_ref(), subsection_name.as_bstr_opt())
266            .ok()
267            .and_then(|mut ids| ids.next_back());
268        let id = id?;
269        self.remove_section_by_id(id)
270    }
271
272    /// Remove the section identified by `id` if it exists and return it, or return `None` if no such section was present.
273    ///
274    /// Note that section ids are unambiguous even in the face of removals and additions of sections.
275    pub fn remove_section_by_id(&mut self, id: SectionId) -> Option<file::Section> {
276        let section = self.sections.remove(&id)?;
277        let position = self.section_order_position(id);
278        self.section_order.remove(position);
279        let lookup_name = section::Name(section.header.name.to_bstring_in(&self.backing));
280        file::util::remove_section_id_from_lookup(
281            &mut self.section_lookup_tree,
282            &lookup_name,
283            section
284                .header
285                .subsection_name
286                .as_ref()
287                .map(|name| name.value_in(&self.backing)),
288            id,
289        );
290        Some(file::Section::from_data(&section, &self.backing))
291    }
292
293    /// Removes the section with `name` and `subsection_name` that passed `filter`, returning the removed section
294    /// if at least one section matched the `filter`.
295    /// If multiple sections have the same name, then the last one is returned. Note that
296    /// later sections with the same name have precedent over earlier ones.
297    pub fn remove_section_filter(
298        &mut self,
299        name: impl AsRef<str>,
300        subsection_name: impl AsBStrOpt,
301        filter: impl FnMut(&Metadata) -> bool,
302    ) -> Option<file::Section> {
303        self.remove_section_filter_inner(name.as_ref(), subsection_name.as_bstr_opt(), filter)
304    }
305
306    fn remove_section_filter_inner(
307        &mut self,
308        name: &str,
309        subsection_name: Option<&BStr>,
310        mut filter: impl FnMut(&Metadata) -> bool,
311    ) -> Option<file::Section> {
312        let id = self
313            .section_ids_by_name_and_subname(name, subsection_name)
314            .ok()
315            .into_iter()
316            .flatten()
317            .rev()
318            .find(|id| self.sections.get(id).is_some_and(|section| filter(&section.meta)));
319        let id = id?;
320        self.remove_section_by_id(id)
321    }
322
323    /// Adds the provided `section` to the config, returning a mutable reference to it for immediate editing.
324    /// Note that its meta-data will remain as is.
325    pub fn push_section(&mut self, section: file::Section) -> ExnMessageResult<SectionMut<'_>> {
326        let section = section.into_data(&mut self.backing)?;
327        let id = self.push_section_internal(section);
328        let nl = self.detect_newline_style_smallvec();
329        Ok(self.section_mut_from_id(id, nl).expect("each id yields a section"))
330    }
331
332    /// Renames all sections with `name` and `subsection_name` to use `new_name` and `new_subsection_name`.
333    ///
334    /// Multiple sections may have the same name, and all matching sections are renamed. Existing sections with the target name
335    /// are preserved.
336    pub fn rename_section(
337        &mut self,
338        name: impl AsRef<str>,
339        subsection_name: impl AsBStrOpt,
340        new_name: impl AsRef<str>,
341        new_subsection_name: impl IntoBStringOpt,
342    ) -> ExnResult {
343        self.rename_section_filter(name, subsection_name, new_name, new_subsection_name, |_| true)
344    }
345
346    /// Renames all sections with `name` and `subsection_name` that pass `filter` to use `new_name` and
347    /// `new_subsection_name`.
348    ///
349    /// Existing sections with the target name are preserved.
350    ///
351    /// A not-found error indicates that the `filter` rejected all candidates, leading to no section being renamed.
352    pub fn rename_section_filter(
353        &mut self,
354        name: impl AsRef<str>,
355        subsection_name: impl AsBStrOpt,
356        new_name: impl AsRef<str>,
357        new_subsection_name: impl IntoBStringOpt,
358        mut filter: impl FnMut(&Metadata) -> bool,
359    ) -> ExnResult {
360        let ids: Vec<_> = self
361            .section_ids_by_name_and_subname(name.as_ref(), subsection_name.as_bstr_opt())?
362            .filter(|id| filter(&self.sections.get(id).expect("each id has a section").meta))
363            .collect();
364        if ids.is_empty() {
365            return Err(lookup::existing::key_missing());
366        }
367        use gix_error::ResultExt;
368        let header = section::HeaderData::new_in(new_name, new_subsection_name.into_bstring_opt(), &mut self.backing)
369            .or_erased()?;
370        for id in ids {
371            file::util::set_section_header(
372                self.sections
373                    .get_mut(&id)
374                    .expect("each id from the lookup has a section"),
375                &self.backing,
376                &mut self.section_lookup_tree,
377                &self.section_order,
378                header.clone(),
379            );
380        }
381        Ok(())
382    }
383
384    /// Append another File to the end of ourselves, without losing any information.
385    pub fn append(&mut self, other: Self) -> ExnMessageResult<&mut Self> {
386        self.append_or_insert(other, None)
387    }
388
389    /// Append another File to the end of ourselves, without losing any information.
390    pub(crate) fn append_or_insert(
391        &mut self,
392        mut other: Self,
393        mut insert_after: Option<SectionId>,
394    ) -> ExnMessageResult<&mut Self> {
395        let nl = self.detect_newline_style_smallvec();
396        let our_last_section_before_append =
397            insert_after.or_else(|| (self.next_section_id != 0).then(|| SectionId(self.next_section_id - 1)));
398        let needs_separator = if other.frontmatter_events.is_empty() {
399            false
400        } else {
401            let lhs_ends_with_newline = match our_last_section_before_append {
402                Some(id) => self
403                    .frontmatter_post_section
404                    .get(&id)
405                    .is_none_or(|events| ends_with_newline(events, &self.backing, &nl, true)),
406                None => ends_with_newline(self.frontmatter_events.as_ref(), &self.backing, &nl, true),
407            };
408            !lhs_ends_with_newline
409                && !other
410                    .frontmatter_events
411                    .first()
412                    .is_none_or(|event| event.to_bstr_lossy_in(&other.backing).starts_with(nl.as_ref()))
413        };
414        let separator = needs_separator
415            .then(|| Span::append(&mut self.backing, nl.as_ref()).map(Event::Newline))
416            .transpose()?;
417        other.rebase_events(self.backing.len())?;
418        self.backing.extend_from_slice(&other.backing);
419
420        fn extend_with_separator(lhs: &mut FrontMatterEvents, separator: Option<Event>, rhs: FrontMatterEvents) {
421            if let Some(separator) = separator {
422                lhs.push(separator);
423            }
424            lhs.extend(rhs);
425        }
426        for id in std::mem::take(&mut other.section_order) {
427            let section = other.sections.remove(&id).expect("present");
428
429            let new_id = match insert_after {
430                Some(id) => {
431                    let new_id = self.insert_section_after(section, id);
432                    insert_after = Some(new_id);
433                    new_id
434                }
435                None => self.push_section_internal(section),
436            };
437
438            if let Some(post_matter) = other.frontmatter_post_section.remove(&id) {
439                self.frontmatter_post_section.insert(new_id, post_matter);
440            }
441        }
442
443        if other.frontmatter_events.is_empty() {
444            return Ok(self);
445        }
446
447        match our_last_section_before_append {
448            Some(last_id) => extend_with_separator(
449                self.frontmatter_post_section.entry(last_id).or_default(),
450                separator,
451                other.frontmatter_events,
452            ),
453            None => extend_with_separator(&mut self.frontmatter_events, separator, other.frontmatter_events),
454        }
455        Ok(self)
456    }
457
458    fn rebase_events(&mut self, offset: usize) -> ExnMessageResult {
459        for event in &mut self.frontmatter_events {
460            event.rebase(offset)?;
461        }
462        for events in self.frontmatter_post_section.values_mut() {
463            for event in events {
464                event.rebase(offset)?;
465            }
466        }
467        for section in self.sections.values_mut() {
468            section.header.rebase(offset)?;
469            for event in &mut section.body.0 {
470                event.rebase(offset)?;
471            }
472        }
473        Ok(())
474    }
475
476    pub(crate) fn section_mut_from_id(
477        &mut self,
478        id: SectionId,
479        newline: smallvec::SmallVec<[u8; 2]>,
480    ) -> Option<SectionMut<'_>> {
481        let section = self.sections.get_mut(&id)?;
482        let lookup = file::mutable::section::LookupMut {
483            tree: &mut self.section_lookup_tree,
484            order: &self.section_order,
485        };
486        Some(section.to_mut(&mut self.backing, lookup, newline))
487    }
488}