Skip to content

Commit 6a2e6a4

Browse files
committed
.gitmodule file abstraction
It's based on `git-config` but has a well-known schema that we abstract to make it more accessible.
1 parent d091c78 commit 6a2e6a4

File tree

12 files changed

+969
-6
lines changed

12 files changed

+969
-6
lines changed

Cargo.lock

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crate-status.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ Make it the best-performing implementation and the most convenient one.
486486
* [x] primitives to help with graph traversal, along with commit-graph acceleration.
487487

488488
### gix-submodule
489+
* [ ] read `.gitmodule` files, access all their fields, and apply overrides
489490
* CRUD for submodules
490491
* try to handle with all the nifty interactions and be a little more comfortable than what git offers, lay a foundation for smarter git submodules.
491492

@@ -721,7 +722,10 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/gix-lock/README.
721722
* [ ] Use _Commit Graph_ to speed up certain queries
722723
* [ ] subtree
723724
* [ ] interactive rebase status/manipulation
724-
* submodules
725+
* **submodules**
726+
* [ ] handle 'old' form for reading
727+
* [ ] list
728+
* [ ] traverse recursively
725729
* [ ] API documentation
726730
* [ ] Some examples
727731

@@ -754,6 +758,7 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/gix-lock/README.
754758

755759
### gix-validate
756760
* [x] validate ref names
761+
* [x] validate submodule names
757762
* [x] [validate][tagname-validation] tag names
758763

759764
### gix-ref

gix-submodule/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,15 @@ rust-version = "1.65"
1212
doctest = false
1313

1414
[dependencies]
15+
gix-refspec = { version = "^0.14.1", path = "../gix-refspec" }
16+
gix-config = { version = "^0.26.2", path = "../gix-config" }
17+
gix-path = { version = "^0.8.4", path = "../gix-path" }
18+
gix-url = { version = "^0.21.1", path = "../gix-url" }
19+
20+
bstr = { version = "1.5.0", default-features = false }
21+
thiserror = "1.0.44"
22+
23+
[dev-dependencies]
24+
gix-testtools = { path = "../tests/tools"}
25+
gix-features = { path = "../gix-features", features = ["walkdir"] }
26+

gix-submodule/src/access.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use crate::config::{Branch, FetchRecurse, Ignore, Update};
2+
use crate::{config, File};
3+
use bstr::BStr;
4+
use std::borrow::Cow;
5+
use std::path::Path;
6+
7+
/// Access
8+
///
9+
/// Note that all methods perform validation of the requested value and report issues right away.
10+
/// If a bypass is needed, use [`config()`](File::config()) for direct access.
11+
impl File {
12+
/// Return the underlying configuration file.
13+
///
14+
/// Note that it might have been merged with values from another configuration file and may
15+
/// thus not be accurately reflecting that state of a `.gitmodules` file anymore.
16+
pub fn config(&self) -> &gix_config::File<'static> {
17+
&self.config
18+
}
19+
20+
/// Return the path at which the `.gitmodules` file lives, if it is known.
21+
pub fn config_path(&self) -> Option<&Path> {
22+
self.config.sections().filter_map(|s| s.meta().path.as_deref()).next()
23+
}
24+
25+
/// Return the unvalidated names of the submodules for which configuration is present.
26+
///
27+
/// Note that these exact names have to be used for querying submodule values.
28+
pub fn names(&self) -> impl Iterator<Item = &BStr> {
29+
self.config
30+
.sections_by_name("submodule")
31+
.into_iter()
32+
.flatten()
33+
.filter_map(|s| s.header().subsection_name())
34+
}
35+
36+
/// Given the `relative_path` (as seen from the root of the worktree) of a submodule with possibly platform-specific
37+
/// component separators, find the submodule's name associated with this path, or `None` if none was found.
38+
///
39+
/// Note that this does a linear search and compares `relative_path` in a normalized form to the same form of the path
40+
/// associated with the submodule.
41+
pub fn name_by_path(&self, relative_path: &BStr) -> Option<&BStr> {
42+
self.names()
43+
.filter_map(|n| self.path(n).ok().map(|p| (n, p)))
44+
.find_map(|(n, p)| (p == relative_path).then_some(n))
45+
}
46+
47+
/// Return the path relative to the root directory of the working tree at which the submodule is expected to be checked out.
48+
/// It's an error if the path doesn't exist as it's the only way to associate a path in the index with additional submodule
49+
/// information, like the URL to fetch from.
50+
///
51+
/// ### Deviation
52+
///
53+
/// Git currently allows absolute paths to be used when adding submodules, but fails later as it can't find the submodule by
54+
/// relative path anymore. Let's play it safe here.
55+
pub fn path(&self, name: &BStr) -> Result<Cow<'_, BStr>, config::path::Error> {
56+
let path_bstr =
57+
self.config
58+
.string("submodule", Some(name), "path")
59+
.ok_or_else(|| config::path::Error::Missing {
60+
submodule: name.to_owned(),
61+
})?;
62+
if path_bstr.is_empty() {
63+
return Err(config::path::Error::Missing {
64+
submodule: name.to_owned(),
65+
});
66+
}
67+
let path = gix_path::from_bstr(path_bstr.as_ref());
68+
if path.is_absolute() {
69+
return Err(config::path::Error::Absolute {
70+
submodule: name.to_owned(),
71+
actual: path_bstr.into_owned(),
72+
});
73+
}
74+
if gix_path::normalize(path, "").is_none() {
75+
return Err(config::path::Error::OutsideOfWorktree {
76+
submodule: name.to_owned(),
77+
actual: path_bstr.into_owned(),
78+
});
79+
}
80+
Ok(path_bstr)
81+
}
82+
83+
/// Retrieve the `url` field of the submodule named `name`. It's an error if it doesn't exist or is empty.
84+
pub fn url(&self, name: &BStr) -> Result<gix_url::Url, config::url::Error> {
85+
let url = self
86+
.config
87+
.string("submodule", Some(name), "url")
88+
.ok_or_else(|| config::url::Error::Missing {
89+
submodule: name.to_owned(),
90+
})?;
91+
92+
if url.is_empty() {
93+
return Err(config::url::Error::Missing {
94+
submodule: name.to_owned(),
95+
});
96+
}
97+
gix_url::Url::from_bytes(url.as_ref()).map_err(|err| config::url::Error::Parse {
98+
submodule: name.to_owned(),
99+
source: err,
100+
})
101+
}
102+
103+
/// Retrieve the `update` field of the submodule named `name`, if present.
104+
pub fn update(&self, name: &BStr) -> Result<Option<Update>, config::update::Error> {
105+
let value: Update = match self.config.string("submodule", Some(name), "update") {
106+
Some(v) => v.as_ref().try_into().map_err(|()| config::update::Error::Invalid {
107+
submodule: name.to_owned(),
108+
actual: v.into_owned(),
109+
})?,
110+
None => return Ok(None),
111+
};
112+
113+
if let Update::Command(cmd) = &value {
114+
let ours = self.config.meta();
115+
let has_value_from_foreign_section = self
116+
.config
117+
.sections_by_name("submodule")
118+
.into_iter()
119+
.flatten()
120+
.any(|s| (s.header().subsection_name() == Some(name) && s.meta() as *const _ != ours as *const _));
121+
if !has_value_from_foreign_section {
122+
return Err(config::update::Error::CommandForbiddenInModulesConfiguration {
123+
submodule: name.to_owned(),
124+
actual: cmd.to_owned(),
125+
});
126+
}
127+
}
128+
Ok(Some(value))
129+
}
130+
131+
/// Retrieve the `branch` field of the submodule named `name`, or `None` if unset.
132+
///
133+
/// Note that `Default` is implemented for [`Branch`].
134+
pub fn branch(&self, name: &BStr) -> Result<Option<Branch>, config::branch::Error> {
135+
let branch = match self.config.string("submodule", Some(name), "branch") {
136+
Some(v) => v,
137+
None => return Ok(None),
138+
};
139+
140+
Branch::try_from(branch.as_ref())
141+
.map(Some)
142+
.map_err(|err| config::branch::Error {
143+
submodule: name.to_owned(),
144+
actual: branch.into_owned(),
145+
source: err,
146+
})
147+
}
148+
149+
/// Retrieve the `fetchRecurseSubmodules` field of the submodule named `name`, or `None` if unset.
150+
///
151+
/// Note that if it's unset, it should be retrieved from `fetch.recurseSubmodules` in the configuration.
152+
pub fn fetch_recurse(&self, name: &BStr) -> Result<Option<FetchRecurse>, config::Error> {
153+
self.config
154+
.boolean("submodule", Some(name), "fetchRecurseSubmodules")
155+
.map(FetchRecurse::new)
156+
.transpose()
157+
.map_err(|value| config::Error {
158+
field: "fetchRecurseSubmodules",
159+
submodule: name.to_owned(),
160+
actual: value,
161+
})
162+
}
163+
164+
/// Retrieve the `ignore` field of the submodule named `name`, or `None` if unset.
165+
pub fn ignore(&self, name: &BStr) -> Result<Option<Ignore>, config::Error> {
166+
self.config
167+
.string("submodule", Some(name), "ignore")
168+
.map(|value| {
169+
Ignore::try_from(value.as_ref()).map_err(|()| config::Error {
170+
field: "ignore",
171+
submodule: name.to_owned(),
172+
actual: value.into_owned(),
173+
})
174+
})
175+
.transpose()
176+
}
177+
178+
/// Retrieve the `shallow` field of the submodule named `name`, or `None` if unset.
179+
///
180+
/// If `true`, the submodule will be checked out with `depth = 1`. If unset, `false` is assumed.
181+
pub fn shallow(&self, name: &BStr) -> Result<Option<bool>, gix_config::value::Error> {
182+
self.config.boolean("submodule", Some(name), "shallow").transpose()
183+
}
184+
}

0 commit comments

Comments
 (0)