Skip to main content

gix_config/file/access/
mutate.rs

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