gix_config/file/access/
mutate.rs

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