Skip to content

Commit 507dc7e

Browse files
committed
Merge branch 'clone'
2 parents dc7e255 + 4474352 commit 507dc7e

File tree

39 files changed

+1015
-120
lines changed

39 files changed

+1015
-120
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ Please see _'Development Status'_ for a listing of all crates and their capabili
5252
* **remote**
5353
* [x] **refs** - list all references available on the remote based on the current remote configuration.
5454
* [x] **ref-map** - show how remote references relate to their local tracking branches as mapped by refspecs.
55-
* **fetch** - fetch the current remote or the given one, optionally just as dry-run.
55+
* [x] **fetch** - fetch the current remote or the given one, optionally just as dry-run.
56+
* **clone**
57+
* [ ] initialize a new **bare** repository and fetch all objects.
58+
* [ ] initialize a new repository, fetch all objects and checkout the main worktree.
5659
* **credential**
5760
* [x] **fill/approve/reject** - The same as `git credential`, but implemented in Rust, calling helpers only when from trusted configuration.
5861
* **free** - no git repository necessary

etc/check-package-size.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@ echo "in root: gitoxide CLI"
5757
(enter git-odb && indent cargo diet -n --package-size-limit 120KB)
5858
(enter git-protocol && indent cargo diet -n --package-size-limit 50KB)
5959
(enter git-packetline && indent cargo diet -n --package-size-limit 35KB)
60-
(enter git-repository && indent cargo diet -n --package-size-limit 185KB)
60+
(enter git-repository && indent cargo diet -n --package-size-limit 200KB)
6161
(enter git-transport && indent cargo diet -n --package-size-limit 55KB)
6262
(enter gitoxide-core && indent cargo diet -n --package-size-limit 90KB)

git-config/src/file/access/mutate.rs

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::borrow::Cow;
22

33
use git_features::threading::OwnShared;
44

5+
use crate::file::SectionBodyIdsLut;
56
use crate::{
67
file::{self, rename_section, write::ends_with_newline, MetadataFilter, SectionId, SectionMut},
78
lookup,
@@ -11,7 +12,7 @@ use crate::{
1112

1213
/// Mutating low-level access methods.
1314
impl<'event> File<'event> {
14-
/// Returns an mutable section with a given `name` and optional `subsection_name`, _if it exists_.
15+
/// Returns the last mutable section with a given `name` and optional `subsection_name`, _if it exists_.
1516
pub fn section_mut<'a>(
1617
&'a mut self,
1718
name: impl AsRef<str>,
@@ -29,7 +30,16 @@ impl<'event> File<'event> {
2930
.expect("BUG: Section did not have id from lookup")
3031
.to_mut(nl))
3132
}
32-
/// Returns an mutable section with a given `name` and optional `subsection_name`, _if it exists_, or create a new section.
33+
34+
/// Return the mutable section identified by `id`, or `None` if it didn't exist.
35+
///
36+
/// Note that `id` is stable across deletions and insertions.
37+
pub fn section_mut_by_id<'a>(&'a mut self, id: SectionId) -> Option<SectionMut<'a, 'event>> {
38+
let nl = self.detect_newline_style_smallvec();
39+
self.sections.get_mut(&id).map(|s| s.to_mut(nl))
40+
}
41+
42+
/// Returns the last mutable section with a given `name` and optional `subsection_name`, _if it exists_, or create a new section.
3343
pub fn section_mut_or_create_new<'a>(
3444
&'a mut self,
3545
name: impl AsRef<str>,
@@ -182,13 +192,39 @@ impl<'event> File<'event> {
182192
.ok()?
183193
.rev()
184194
.next()?;
185-
self.section_order.remove(
186-
self.section_order
187-
.iter()
188-
.position(|v| *v == id)
189-
.expect("known section id"),
190-
);
191-
self.sections.remove(&id)
195+
self.remove_section_by_id(id)
196+
}
197+
198+
/// Remove the section identified by `id` if it exists and return it, or return `None` if no such section was present.
199+
///
200+
/// Note that section ids are unambiguous even in the face of removals and additions of sections.
201+
pub fn remove_section_by_id(&mut self, id: SectionId) -> Option<file::Section<'event>> {
202+
self.section_order
203+
.remove(self.section_order.iter().position(|v| *v == id)?);
204+
let section = self.sections.remove(&id)?;
205+
let lut = self
206+
.section_lookup_tree
207+
.get_mut(&section.header.name)
208+
.expect("lookup cache still has name to be deleted");
209+
for entry in lut {
210+
match section.header.subsection_name.as_deref() {
211+
Some(subsection_name) => {
212+
if let SectionBodyIdsLut::NonTerminal(map) = entry {
213+
if let Some(ids) = map.get_mut(subsection_name) {
214+
ids.remove(ids.iter().position(|v| *v == id).expect("present"));
215+
break;
216+
}
217+
}
218+
}
219+
None => {
220+
if let SectionBodyIdsLut::Terminal(ids) = entry {
221+
ids.remove(ids.iter().position(|v| *v == id).expect("present"));
222+
break;
223+
}
224+
}
225+
}
226+
}
227+
Some(section)
192228
}
193229

194230
/// Removes the section with `name` and `subsection_name` that passed `filter`, returning the removed section

git-config/src/file/access/read_only.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
use std::{borrow::Cow, convert::TryFrom, iter::FromIterator};
1+
use std::{borrow::Cow, convert::TryFrom};
22

33
use bstr::BStr;
44
use git_features::threading::OwnShared;
55
use smallvec::SmallVec;
66

7+
use crate::file::SectionId;
78
use crate::{
89
file,
910
file::{
@@ -206,6 +207,25 @@ impl<'event> File<'event> {
206207
})
207208
}
208209

210+
/// Similar to [`sections_by_name()`][Self::sections_by_name()], but returns an identifier for this section as well to allow
211+
/// referring to it unambiguously even in the light of deletions.
212+
#[must_use]
213+
pub fn sections_and_ids_by_name<'a>(
214+
&'a self,
215+
name: &'a str,
216+
) -> Option<impl Iterator<Item = (&file::Section<'event>, SectionId)> + '_> {
217+
self.section_ids_by_name(name).ok().map(move |ids| {
218+
ids.map(move |id| {
219+
(
220+
self.sections
221+
.get(&id)
222+
.expect("section doesn't have id from from lookup"),
223+
id,
224+
)
225+
})
226+
})
227+
}
228+
209229
/// Gets all sections that match the provided `name`, ignoring any subsections, and pass the `filter`.
210230
#[must_use]
211231
pub fn sections_by_name_and_filter<'a>(
@@ -258,6 +278,11 @@ impl<'event> File<'event> {
258278
self.section_order.iter().map(move |id| &self.sections[id])
259279
}
260280

281+
/// Return an iterator over all sections and their ids, in order of occurrence in the file itself.
282+
pub fn sections_and_ids(&self) -> impl Iterator<Item = (&file::Section<'event>, SectionId)> + '_ {
283+
self.section_order.iter().map(move |id| (&self.sections[id], *id))
284+
}
285+
261286
/// Return an iterator over all sections along with non-section events that are placed right after them,
262287
/// in order of occurrence in the file itself.
263288
///
@@ -296,6 +321,6 @@ impl<'event> File<'event> {
296321
}
297322

298323
pub(crate) fn detect_newline_style_smallvec(&self) -> SmallVec<[u8; 2]> {
299-
SmallVec::from_iter(self.detect_newline_style().iter().copied())
324+
self.detect_newline_style().as_ref().into()
300325
}
301326
}

git-config/src/file/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ impl AddAssign<usize> for Size {
110110
/// words, it's possible that a section may have an ID of 3 but the next section
111111
/// has an ID of 5 as 4 was deleted.
112112
#[derive(PartialEq, Eq, Hash, Copy, Clone, PartialOrd, Ord, Debug)]
113-
pub(crate) struct SectionId(pub(crate) usize);
113+
pub struct SectionId(pub(crate) usize);
114114

115115
/// All section body ids referred to by a section name.
116116
///

git-config/src/file/mutable/section.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ impl<'a, 'event> SectionMut<'a, 'event> {
9595
Some((key_range, value_range)) => {
9696
let value_range = value_range.unwrap_or(key_range.end - 1..key_range.end);
9797
let range_start = value_range.start;
98-
let ret = self.remove_internal(value_range);
98+
let ret = self.remove_internal(value_range, false);
9999
self.section
100100
.body
101101
.0
@@ -109,7 +109,7 @@ impl<'a, 'event> SectionMut<'a, 'event> {
109109
pub fn remove(&mut self, key: impl AsRef<str>) -> Option<Cow<'event, BStr>> {
110110
let key = Key::from_str_unchecked(key.as_ref());
111111
let (key_range, _value_range) = self.key_and_value_range_by(&key)?;
112-
Some(self.remove_internal(key_range))
112+
Some(self.remove_internal(key_range, true))
113113
}
114114

115115
/// Adds a new line event. Note that you don't need to call this unless
@@ -231,17 +231,33 @@ impl<'a, 'event> SectionMut<'a, 'event> {
231231
}
232232

233233
/// Performs the removal, assuming the range is valid.
234-
fn remove_internal(&mut self, range: Range<usize>) -> Cow<'event, BStr> {
235-
self.section
236-
.body
237-
.0
238-
.drain(range)
239-
.fold(Cow::Owned(BString::default()), |mut acc, e| {
234+
fn remove_internal(&mut self, range: Range<usize>, fix_whitespace: bool) -> Cow<'event, BStr> {
235+
let events = &mut self.section.body.0;
236+
if fix_whitespace
237+
&& events
238+
.get(range.end)
239+
.map_or(false, |ev| matches!(ev, Event::Newline(_)))
240+
{
241+
events.remove(range.end);
242+
}
243+
let value = events
244+
.drain(range.clone())
245+
.fold(Cow::Owned(BString::default()), |mut acc: Cow<'_, BStr>, e| {
240246
if let Event::Value(v) | Event::ValueNotDone(v) | Event::ValueDone(v) = e {
241247
acc.to_mut().extend(&**v);
242248
}
243249
acc
244-
})
250+
});
251+
if fix_whitespace
252+
&& range
253+
.start
254+
.checked_sub(1)
255+
.and_then(|pos| events.get(pos))
256+
.map_or(false, |ev| matches!(ev, Event::Whitespace(_)))
257+
{
258+
events.remove(range.start - 1);
259+
}
260+
value
245261
}
246262
}
247263

git-config/src/parse/section/header.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,13 @@ impl<'a> Header<'a> {
4141
}
4242
}
4343

44+
/// Return true if `name` is valid as subsection name, like `origin` in `[remote "origin"]`.
45+
pub fn is_valid_subsection(name: &BStr) -> bool {
46+
name.find_byteset(b"\n\0").is_none()
47+
}
48+
4449
fn validated_subsection(name: Cow<'_, BStr>) -> Result<Cow<'_, BStr>, Error> {
45-
name.find_byteset(b"\n\0")
46-
.is_none()
50+
is_valid_subsection(name.as_ref())
4751
.then(|| name)
4852
.ok_or(Error::InvalidSubSection)
4953
}
@@ -55,6 +59,24 @@ fn validated_name(name: Cow<'_, BStr>) -> Result<Cow<'_, BStr>, Error> {
5559
.ok_or(Error::InvalidName)
5660
}
5761

62+
#[cfg(test)]
63+
mod tests {
64+
use super::*;
65+
66+
#[test]
67+
fn empty_header_names_are_legal() {
68+
assert!(Header::new("", None).is_ok(), "yes, git allows this, so do we");
69+
}
70+
71+
#[test]
72+
fn empty_header_sub_names_are_legal() {
73+
assert!(
74+
Header::new("remote", Some("".into())).is_ok(),
75+
"yes, git allows this, so do we"
76+
);
77+
}
78+
}
79+
5880
impl Header<'_> {
5981
///Return true if this is a header like `[legacy.subsection]`, or false otherwise.
6082
pub fn is_legacy(&self) -> bool {

git-config/src/parse/section/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ mod types {
160160
generate_case_insensitive!(
161161
Name,
162162
name,
163-
"Valid names consist alphanumeric characters or dashes.",
163+
"Valid names consist of alphanumeric characters or dashes.",
164164
is_valid_name,
165165
bstr::BStr,
166-
"Wrapper struct for section header names, like `includeIf`, since these are case-insensitive."
166+
"Wrapper struct for section header names, like `remote`, since these are case-insensitive."
167167
);
168168

169169
generate_case_insensitive!(

git-config/tests/file/access/mutate.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,51 @@
1+
mod remove_section {
2+
use std::convert::TryFrom;
3+
4+
#[test]
5+
fn removal_of_all_sections_programmatically_with_sections_and_ids_by_name() {
6+
let mut file = git_config::File::try_from("[core] \na = b\nb=c\n\n[core \"name\"]\nd = 1\ne = 2").unwrap();
7+
for id in file
8+
.sections_and_ids_by_name("core")
9+
.expect("2 sections present")
10+
.map(|(_, id)| id)
11+
.collect::<Vec<_>>()
12+
{
13+
file.remove_section_by_id(id);
14+
}
15+
assert!(file.is_void());
16+
assert_eq!(file.sections().count(), 0);
17+
}
18+
19+
#[test]
20+
fn removal_of_all_sections_programmatically_with_sections_and_ids() {
21+
let mut file = git_config::File::try_from("[core] \na = b\nb=c\n\n[core \"name\"]\nd = 1\ne = 2").unwrap();
22+
for id in file.sections_and_ids().map(|(_, id)| id).collect::<Vec<_>>() {
23+
file.remove_section_by_id(id);
24+
}
25+
assert!(file.is_void());
26+
assert_eq!(file.sections().count(), 0);
27+
}
28+
29+
#[test]
30+
fn removal_is_complete_and_sections_can_be_readded() {
31+
let mut file = git_config::File::try_from("[core] \na = b\nb=c\n\n[core \"name\"]\nd = 1\ne = 2").unwrap();
32+
assert_eq!(file.sections().count(), 2);
33+
34+
let removed = file.remove_section("core", None).expect("removed correct section");
35+
assert_eq!(removed.header().name(), "core");
36+
assert_eq!(removed.header().subsection_name(), None);
37+
assert_eq!(file.sections().count(), 1);
38+
39+
let removed = file.remove_section("core", Some("name")).expect("found");
40+
assert_eq!(removed.header().name(), "core");
41+
assert_eq!(removed.header().subsection_name().expect("present"), "name");
42+
assert_eq!(file.sections().count(), 0);
43+
44+
file.section_mut_or_create_new("core", None).expect("creation succeeds");
45+
file.section_mut_or_create_new("core", Some("name"))
46+
.expect("creation succeeds");
47+
}
48+
}
149
mod rename_section {
250
use std::{borrow::Cow, convert::TryFrom};
351

git-config/tests/file/mutable/section.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ fn section_mut_or_create_new_filter_may_reject_existing_sections() -> crate::Res
2828
Ok(())
2929
}
3030

31+
#[test]
32+
fn section_mut_by_id() {
33+
let mut config = multi_value_section();
34+
let id = config.sections_and_ids().next().expect("at least one").1;
35+
let section = config.section_mut_by_id(id).expect("present");
36+
assert_eq!(section.header().name(), "a");
37+
assert_eq!(section.header().subsection_name(), None);
38+
}
39+
3140
mod remove {
3241
use super::multi_value_section;
3342

@@ -49,10 +58,7 @@ mod remove {
4958
}
5059

5160
assert!(!section.is_void(), "everything is still there");
52-
assert_eq!(
53-
config.to_string(),
54-
"\n [a]\n \n \n \n \n "
55-
);
61+
assert_eq!(config.to_string(), "\n [a]\n");
5662
Ok(())
5763
}
5864
}

0 commit comments

Comments
 (0)