gix_config/file/access/
mutate.rs

1use std::borrow::Cow;
2
3use bstr::BStr;
4use gix_features::threading::OwnShared;
5
6use crate::file::Metadata;
7use crate::{
8    file::{self, rename_section, write::ends_with_newline, SectionBodyIdsLut, SectionId, SectionMut},
9    lookup,
10    parse::{section, Event, FrontMatterEvents},
11    File,
12};
13
14/// Mutating low-level access methods.
15impl<'event> File<'event> {
16    /// Returns the last mutable section with a given `name` and optional `subsection_name`, _if it exists_.
17    pub fn section_mut<'a>(
18        &'a mut self,
19        name: impl AsRef<str>,
20        subsection_name: Option<&BStr>,
21    ) -> Result<SectionMut<'a, 'event>, lookup::existing::Error> {
22        self.section_mut_inner(name.as_ref(), subsection_name)
23    }
24
25    fn section_mut_inner<'a>(
26        &'a mut self,
27        name: &str,
28        subsection_name: Option<&BStr>,
29    ) -> Result<SectionMut<'a, 'event>, lookup::existing::Error> {
30        let id = self
31            .section_ids_by_name_and_subname(name, subsection_name)?
32            .next_back()
33            .expect("BUG: Section lookup vec was empty");
34        let nl = self.detect_newline_style_smallvec();
35        Ok(self
36            .sections
37            .get_mut(&id)
38            .expect("BUG: Section did not have id from lookup")
39            .to_mut(nl))
40    }
41
42    /// Returns the last found mutable section with a given `key`, identifying the name and subsection name like `core` or `remote.origin`.
43    pub fn section_mut_by_key<'a, 'b>(
44        &'a mut self,
45        key: impl Into<&'b BStr>,
46    ) -> Result<SectionMut<'a, 'event>, lookup::existing::Error> {
47        let key = section::unvalidated::Key::parse(key).ok_or(lookup::existing::Error::KeyMissing)?;
48        self.section_mut(key.section_name, key.subsection_name)
49    }
50
51    /// Return the mutable section identified by `id`, or `None` if it didn't exist.
52    ///
53    /// Note that `id` is stable across deletions and insertions.
54    pub fn section_mut_by_id<'a>(&'a mut self, id: SectionId) -> Option<SectionMut<'a, 'event>> {
55        let nl = self.detect_newline_style_smallvec();
56        self.sections.get_mut(&id).map(|s| s.to_mut(nl))
57    }
58
59    /// Returns the last mutable section with a given `name` and optional `subsection_name`, _if it exists_, or create a new section.
60    pub fn section_mut_or_create_new<'a>(
61        &'a mut self,
62        name: impl AsRef<str>,
63        subsection_name: Option<&BStr>,
64    ) -> Result<SectionMut<'a, 'event>, section::header::Error> {
65        self.section_mut_or_create_new_filter(name, subsection_name, |_| true)
66    }
67
68    /// Returns an mutable section with a given `name` and optional `subsection_name`, _if it exists_ **and** passes `filter`, or create
69    /// a new section.
70    pub fn section_mut_or_create_new_filter<'a>(
71        &'a mut self,
72        name: impl AsRef<str>,
73        subsection_name: Option<&BStr>,
74        filter: impl FnMut(&Metadata) -> bool,
75    ) -> Result<SectionMut<'a, 'event>, section::header::Error> {
76        self.section_mut_or_create_new_filter_inner(name.as_ref(), subsection_name, filter)
77    }
78
79    fn section_mut_or_create_new_filter_inner<'a>(
80        &'a mut self,
81        name: &str,
82        subsection_name: Option<&BStr>,
83        mut filter: impl FnMut(&Metadata) -> bool,
84    ) -> Result<SectionMut<'a, 'event>, section::header::Error> {
85        match self
86            .section_ids_by_name_and_subname(name.as_ref(), subsection_name)
87            .ok()
88            .and_then(|it| {
89                it.rev()
90                    .find(|id| self.sections.get(id).is_some_and(|s| filter(s.meta())))
91            }) {
92            Some(id) => {
93                let nl = self.detect_newline_style_smallvec();
94                Ok(self
95                    .sections
96                    .get_mut(&id)
97                    .expect("BUG: Section did not have id from lookup")
98                    .to_mut(nl))
99            }
100            None => self.new_section(name.to_owned(), subsection_name.map(|n| Cow::Owned(n.to_owned()))),
101        }
102    }
103
104    /// Returns the last found mutable section with a given `name` and optional `subsection_name`, that matches `filter`, _if it exists_.
105    ///
106    /// If there are sections matching `section_name` and `subsection_name` but the `filter` rejects all of them, `Ok(None)`
107    /// is returned.
108    pub fn section_mut_filter<'a>(
109        &'a mut self,
110        name: impl AsRef<str>,
111        subsection_name: Option<&BStr>,
112        filter: impl FnMut(&Metadata) -> bool,
113    ) -> Result<Option<file::SectionMut<'a, 'event>>, lookup::existing::Error> {
114        self.section_mut_filter_inner(name.as_ref(), subsection_name, filter)
115    }
116
117    fn section_mut_filter_inner<'a>(
118        &'a mut self,
119        name: &str,
120        subsection_name: Option<&BStr>,
121        mut filter: impl FnMut(&Metadata) -> bool,
122    ) -> Result<Option<file::SectionMut<'a, 'event>>, lookup::existing::Error> {
123        let id = self
124            .section_ids_by_name_and_subname(name, subsection_name)?
125            .rev()
126            .find(|id| {
127                let s = &self.sections[id];
128                filter(s.meta())
129            });
130        let nl = self.detect_newline_style_smallvec();
131        Ok(id.and_then(move |id| self.sections.get_mut(&id).map(move |s| s.to_mut(nl))))
132    }
133
134    /// Like [`section_mut_filter()`][File::section_mut_filter()], but identifies the with a given `key`,
135    /// like `core` or `remote.origin`.
136    pub fn section_mut_filter_by_key<'a, 'b>(
137        &'a mut self,
138        key: impl Into<&'b BStr>,
139        filter: impl FnMut(&Metadata) -> bool,
140    ) -> Result<Option<file::SectionMut<'a, 'event>>, lookup::existing::Error> {
141        let key = section::unvalidated::Key::parse(key).ok_or(lookup::existing::Error::KeyMissing)?;
142        self.section_mut_filter(key.section_name, key.subsection_name, filter)
143    }
144
145    /// Adds a new section. If a subsection name was provided, then
146    /// the generated header will use the modern subsection syntax.
147    /// Returns a reference to the new section for immediate editing.
148    ///
149    /// # Examples
150    ///
151    /// Creating a new empty section:
152    ///
153    /// ```
154    /// # use std::borrow::Cow;
155    /// # use gix_config::File;
156    /// # use std::convert::TryFrom;
157    /// let mut git_config = gix_config::File::default();
158    /// let section = git_config.new_section("hello", Some(Cow::Borrowed("world".into())))?;
159    /// let nl = section.newline().to_owned();
160    /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}"));
161    /// # Ok::<(), Box<dyn std::error::Error>>(())
162    /// ```
163    ///
164    /// Creating a new empty section and adding values to it:
165    ///
166    /// ```
167    /// # use gix_config::File;
168    /// # use std::borrow::Cow;
169    /// # use std::convert::TryFrom;
170    /// # use bstr::ByteSlice;
171    /// # use gix_config::parse::section;
172    /// let mut git_config = gix_config::File::default();
173    /// let mut section = git_config.new_section("hello", Some(Cow::Borrowed("world".into())))?;
174    /// section.push(section::ValueName::try_from("a")?, Some("b".into()));
175    /// let nl = section.newline().to_owned();
176    /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}\ta = b{nl}"));
177    /// let _section = git_config.new_section("core", None);
178    /// assert_eq!(git_config.to_string(), format!("[hello \"world\"]{nl}\ta = b{nl}[core]{nl}"));
179    /// # Ok::<(), Box<dyn std::error::Error>>(())
180    /// ```
181    pub fn new_section(
182        &mut self,
183        name: impl Into<Cow<'event, str>>,
184        subsection: impl Into<Option<Cow<'event, BStr>>>,
185    ) -> Result<SectionMut<'_, 'event>, section::header::Error> {
186        self.new_section_inner(name.into(), subsection.into())
187    }
188
189    fn new_section_inner(
190        &mut self,
191        name: Cow<'event, str>,
192        subsection: Option<Cow<'event, BStr>>,
193    ) -> Result<SectionMut<'_, 'event>, section::header::Error> {
194        let id = self.push_section_internal(file::Section::new(name, subsection, OwnShared::clone(&self.meta))?);
195        let nl = self.detect_newline_style_smallvec();
196        let mut section = self.sections.get_mut(&id).expect("each id yields a section").to_mut(nl);
197        section.push_newline();
198        Ok(section)
199    }
200
201    /// Removes the section with `name` and `subsection_name` , returning it if there was a matching section.
202    /// If multiple sections have the same name, then the last one is returned. Note that
203    /// later sections with the same name have precedent over earlier ones.
204    ///
205    /// # Examples
206    ///
207    /// Creating and removing a section:
208    ///
209    /// ```
210    /// # use gix_config::File;
211    /// # use std::convert::TryFrom;
212    /// let mut git_config = gix_config::File::try_from(
213    /// r#"[hello "world"]
214    ///     some-value = 4
215    /// "#)?;
216    ///
217    /// let section = git_config.remove_section("hello", Some("world".into()));
218    /// assert_eq!(git_config.to_string(), "");
219    /// # Ok::<(), Box<dyn std::error::Error>>(())
220    /// ```
221    ///
222    /// Precedence example for removing sections with the same name:
223    ///
224    /// ```
225    /// # use gix_config::File;
226    /// # use std::convert::TryFrom;
227    /// let mut git_config = gix_config::File::try_from(
228    /// r#"[hello "world"]
229    ///     some-value = 4
230    /// [hello "world"]
231    ///     some-value = 5
232    /// "#)?;
233    ///
234    /// let section = git_config.remove_section("hello", Some("world".into()));
235    /// assert_eq!(git_config.to_string(), "[hello \"world\"]\n    some-value = 4\n");
236    /// # Ok::<(), Box<dyn std::error::Error>>(())
237    /// ```
238    pub fn remove_section<'a>(
239        &mut self,
240        name: impl AsRef<str>,
241        subsection_name: impl Into<Option<&'a BStr>>,
242    ) -> Option<file::Section<'event>> {
243        let id = self
244            .section_ids_by_name_and_subname(name.as_ref(), subsection_name.into())
245            .ok()?
246            .next_back()?;
247        self.remove_section_by_id(id)
248    }
249
250    /// Remove the section identified by `id` if it exists and return it, or return `None` if no such section was present.
251    ///
252    /// Note that section ids are unambiguous even in the face of removals and additions of sections.
253    pub fn remove_section_by_id(&mut self, id: SectionId) -> Option<file::Section<'event>> {
254        self.section_order
255            .remove(self.section_order.iter().position(|v| *v == id)?);
256        let section = self.sections.remove(&id)?;
257        let lut = self
258            .section_lookup_tree
259            .get_mut(&section.header.name)
260            .expect("lookup cache still has name to be deleted");
261        // NOTE: this leaves empty lists in the data structure which our code now has to deal with.
262        for entry in lut {
263            match section.header.subsection_name.as_deref() {
264                Some(subsection_name) => {
265                    if let SectionBodyIdsLut::NonTerminal(map) = entry {
266                        if let Some(ids) = map.get_mut(subsection_name) {
267                            ids.remove(ids.iter().position(|v| *v == id).expect("present"));
268                            break;
269                        }
270                    }
271                }
272                None => {
273                    if let SectionBodyIdsLut::Terminal(ids) = entry {
274                        ids.remove(ids.iter().position(|v| *v == id).expect("present"));
275                        break;
276                    }
277                }
278            }
279        }
280        Some(section)
281    }
282
283    /// Removes the section with `name` and `subsection_name` that passed `filter`, returning the removed section
284    /// if at least one section matched the `filter`.
285    /// If multiple sections have the same name, then the last one is returned. Note that
286    /// later sections with the same name have precedent over earlier ones.
287    pub fn remove_section_filter<'a>(
288        &mut self,
289        name: impl AsRef<str>,
290        subsection_name: impl Into<Option<&'a BStr>>,
291        filter: impl FnMut(&Metadata) -> bool,
292    ) -> Option<file::Section<'event>> {
293        self.remove_section_filter_inner(name.as_ref(), subsection_name.into(), filter)
294    }
295
296    fn remove_section_filter_inner(
297        &mut self,
298        name: &str,
299        subsection_name: Option<&BStr>,
300        mut filter: impl FnMut(&Metadata) -> bool,
301    ) -> Option<file::Section<'event>> {
302        let id = self
303            .section_ids_by_name_and_subname(name, subsection_name)
304            .ok()?
305            .rev()
306            .find(|id| self.sections.get(id).is_some_and(|section| filter(section.meta())))?;
307        self.section_order.remove(
308            self.section_order
309                .iter()
310                .position(|v| *v == id)
311                .expect("known section id"),
312        );
313        self.sections.remove(&id)
314    }
315
316    /// Adds the provided `section` to the config, returning a mutable reference to it for immediate editing.
317    /// Note that its meta-data will remain as is.
318    pub fn push_section(&mut self, section: file::Section<'event>) -> SectionMut<'_, 'event> {
319        let id = self.push_section_internal(section);
320        let nl = self.detect_newline_style_smallvec();
321        let section = self.sections.get_mut(&id).expect("each id yields a section").to_mut(nl);
322        section
323    }
324
325    /// Renames the section with `name` and `subsection_name`, modifying the last matching section
326    /// to use `new_name` and `new_subsection_name`.
327    pub fn rename_section<'a>(
328        &mut self,
329        name: impl AsRef<str>,
330        subsection_name: impl Into<Option<&'a BStr>>,
331        new_name: impl Into<Cow<'event, str>>,
332        new_subsection_name: impl Into<Option<Cow<'event, BStr>>>,
333    ) -> Result<(), rename_section::Error> {
334        let id = self
335            .section_ids_by_name_and_subname(name.as_ref(), subsection_name.into())?
336            .next_back()
337            .expect("list of sections were empty, which violates invariant");
338        let section = self.sections.get_mut(&id).expect("known section-id");
339        section.header = section::Header::new(new_name, new_subsection_name)?;
340        Ok(())
341    }
342
343    /// Renames the section with `name` and `subsection_name`, modifying the last matching section
344    /// that also passes `filter` to use `new_name` and `new_subsection_name`.
345    ///
346    /// Note that the otherwise unused [`lookup::existing::Error::KeyMissing`] variant is used to indicate
347    /// that the `filter` rejected all candidates, leading to no section being renamed after all.
348    pub fn rename_section_filter<'a>(
349        &mut self,
350        name: impl AsRef<str>,
351        subsection_name: impl Into<Option<&'a BStr>>,
352        new_name: impl Into<Cow<'event, str>>,
353        new_subsection_name: impl Into<Option<Cow<'event, BStr>>>,
354        mut filter: impl FnMut(&Metadata) -> bool,
355    ) -> Result<(), rename_section::Error> {
356        let id = self
357            .section_ids_by_name_and_subname(name.as_ref(), subsection_name.into())?
358            .rev()
359            .find(|id| filter(self.sections.get(id).expect("each id has a section").meta()))
360            .ok_or(rename_section::Error::Lookup(lookup::existing::Error::KeyMissing))?;
361        let section = self.sections.get_mut(&id).expect("known section-id");
362        section.header = section::Header::new(new_name, new_subsection_name)?;
363        Ok(())
364    }
365
366    /// Append another File to the end of ourselves, without losing any information.
367    pub fn append(&mut self, other: Self) -> &mut Self {
368        self.append_or_insert(other, None)
369    }
370
371    /// Append another File to the end of ourselves, without losing any information.
372    pub(crate) fn append_or_insert(&mut self, mut other: Self, mut insert_after: Option<SectionId>) -> &mut Self {
373        let nl = self.detect_newline_style_smallvec();
374        fn extend_and_assure_newline<'a>(
375            lhs: &mut FrontMatterEvents<'a>,
376            rhs: FrontMatterEvents<'a>,
377            nl: &impl AsRef<[u8]>,
378        ) {
379            if !ends_with_newline(lhs.as_ref(), nl, true)
380                && !rhs.first().map_or(true, |e| e.to_bstr_lossy().starts_with(nl.as_ref()))
381            {
382                lhs.push(Event::Newline(Cow::Owned(nl.as_ref().into())));
383            }
384            lhs.extend(rhs);
385        }
386        #[allow(clippy::unnecessary_lazy_evaluations)]
387        let our_last_section_before_append =
388            insert_after.or_else(|| (self.section_id_counter != 0).then(|| SectionId(self.section_id_counter - 1)));
389
390        for id in std::mem::take(&mut other.section_order) {
391            let section = other.sections.remove(&id).expect("present");
392
393            let new_id = match insert_after {
394                Some(id) => {
395                    let new_id = self.insert_section_after(section, id);
396                    insert_after = Some(new_id);
397                    new_id
398                }
399                None => self.push_section_internal(section),
400            };
401
402            if let Some(post_matter) = other.frontmatter_post_section.remove(&id) {
403                self.frontmatter_post_section.insert(new_id, post_matter);
404            }
405        }
406
407        if other.frontmatter_events.is_empty() {
408            return self;
409        }
410
411        match our_last_section_before_append {
412            Some(last_id) => extend_and_assure_newline(
413                self.frontmatter_post_section.entry(last_id).or_default(),
414                other.frontmatter_events,
415                &nl,
416            ),
417            None => extend_and_assure_newline(&mut self.frontmatter_events, other.frontmatter_events, &nl),
418        }
419        self
420    }
421}