Skip to content

Commit 84d594c

Browse files
committed
feat!: more type-safety for remote names by making clear they can be named after URLs.
`reference::remote::Name` was moved to `remote::Name` to represent all remote names without necessarily having been validated. Note that we also don't validate names anymore if read from configuration, but use it as is. This means that remotes can be read and written back even with invalid names.
1 parent 5fa9546 commit 84d594c

File tree

20 files changed

+140
-112
lines changed

20 files changed

+140
-112
lines changed

git-repository/src/clone/fetch/mod.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::bstr::BString;
12
use crate::{clone::PrepareFetch, Repository};
23

34
/// The error returned by [`PrepareFetch::fetch_only()`].
@@ -58,13 +59,13 @@ impl PrepareFetch {
5859
.as_mut()
5960
.expect("user error: multiple calls are allowed only until it succeeds");
6061

61-
let remote_name = match self.remote_name.as_deref() {
62+
let remote_name = match self.remote_name.as_ref() {
6263
Some(name) => name.to_owned(),
6364
None => repo
6465
.config
6566
.resolved
66-
.string("clone", None, "defaultRemoteName")
67-
.map(|n| crate::remote::name::validated(n.to_string()))
67+
.string_by_key("clone.defaultRemoteName")
68+
.map(|n| crate::remote::name::validated(n.into_owned()))
6869
.unwrap_or_else(|| Ok("origin".into()))?,
6970
};
7071

@@ -114,7 +115,7 @@ impl PrepareFetch {
114115
repo,
115116
&outcome.ref_map.remote_refs,
116117
reflog_message.as_ref(),
117-
&remote_name,
118+
remote_name.as_ref(),
118119
)?;
119120

120121
Ok((self.repo.take().expect("still present"), outcome))
@@ -161,7 +162,7 @@ impl PrepareFetch {
161162
/// [`configure_remote()`][Self::configure_remote()].
162163
///
163164
/// If not set here, it defaults to `origin` or the value of `clone.defaultRemoteName`.
164-
pub fn with_remote_name(mut self, name: impl Into<String>) -> Result<Self, crate::remote::name::Error> {
165+
pub fn with_remote_name(mut self, name: impl Into<BString>) -> Result<Self, crate::remote::name::Error> {
165166
self.remote_name = Some(crate::remote::name::validated(name)?);
166167
Ok(self)
167168
}

git-repository/src/clone/fetch/util.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ use git_ref::{
77
};
88

99
use super::Error;
10+
use crate::bstr::BString;
1011
use crate::{
1112
bstr::{BStr, ByteSlice},
1213
Repository,
1314
};
1415

1516
pub fn write_remote_to_local_config_file(
1617
remote: &mut crate::Remote<'_>,
17-
remote_name: String,
18+
remote_name: BString,
1819
) -> Result<git_config::File<'static>, Error> {
1920
let mut metadata = git_config::file::Metadata::from(git_config::Source::Local);
2021
let config_path = remote.repo.git_dir().join("config");
@@ -50,7 +51,7 @@ pub fn update_head(
5051
repo: &mut Repository,
5152
remote_refs: &[git_protocol::handshake::Ref],
5253
reflog_message: &BStr,
53-
remote_name: &str,
54+
remote_name: &BStr,
5455
) -> Result<(), Error> {
5556
use git_ref::{
5657
transaction::{PreviousValue, RefEdit},
@@ -171,7 +172,7 @@ fn setup_branch_config(
171172
repo: &mut Repository,
172173
branch: &FullNameRef,
173174
branch_id: Option<&git_hash::oid>,
174-
remote_name: &str,
175+
remote_name: &BStr,
175176
) -> Result<(), Error> {
176177
let short_name = match branch.category_and_short_name() {
177178
Some((cat, shortened)) if cat == git_ref::Category::LocalBranch => match shortened.to_str() {

git-repository/src/clone/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::bstr::BString;
12
use std::convert::TryInto;
23

34
type ConfigureRemoteFn = Box<dyn FnMut(crate::Remote<'_>) -> Result<crate::Remote<'_>, crate::remote::init::Error>>;
@@ -9,7 +10,7 @@ pub struct PrepareFetch {
910
/// A freshly initialized repository which is owned by us, or `None` if it was handed to the user
1011
repo: Option<crate::Repository>,
1112
/// The name of the remote, which defaults to `origin` if not overridden.
12-
remote_name: Option<String>,
13+
remote_name: Option<BString>,
1314
/// A function to configure a remote prior to fetching a pack.
1415
configure_remote: Option<ConfigureRemoteFn>,
1516
/// Options for preparing a fetch operation.

git-repository/src/config/cache/access.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::{borrow::Cow, convert::TryInto, path::PathBuf, time::Duration};
22

33
use git_lock::acquire::Fail;
44

5+
use crate::bstr::BStr;
56
use crate::{
67
config::{cache::util::ApplyLeniencyDefault, checkout_options, Cache},
78
remote,
@@ -121,7 +122,7 @@ impl Cache {
121122
pub(crate) fn trusted_file_path(
122123
&self,
123124
section_name: impl AsRef<str>,
124-
subsection_name: Option<&str>,
125+
subsection_name: Option<&BStr>,
125126
key: impl AsRef<str>,
126127
) -> Option<Result<Cow<'_, std::path::Path>, git_config::path::interpolate::Error>> {
127128
let path = self.resolved.path_filter(

git-repository/src/config/snapshot/access.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ impl<'repo> Snapshot<'repo> {
1919
/// For a non-degenerating version, use [`try_boolean(…)`][Self::try_boolean()].
2020
///
2121
/// Note that this method takes the most recent value at `key` even if it is from a file with reduced trust.
22-
pub fn boolean(&self, key: &str) -> Option<bool> {
22+
pub fn boolean<'a>(&self, key: impl Into<&'a BStr>) -> Option<bool> {
2323
self.try_boolean(key).and_then(Result::ok)
2424
}
2525

2626
/// Like [`boolean()`][Self::boolean()], but it will report an error if the value couldn't be interpreted as boolean.
27-
pub fn try_boolean(&self, key: &str) -> Option<Result<bool, git_config::value::Error>> {
27+
pub fn try_boolean<'a>(&self, key: impl Into<&'a BStr>) -> Option<Result<bool, git_config::value::Error>> {
2828
let key = git_config::parse::key(key)?;
2929
self.repo
3030
.config
@@ -38,12 +38,12 @@ impl<'repo> Snapshot<'repo> {
3838
/// For a non-degenerating version, use [`try_integer(…)`][Self::try_integer()].
3939
///
4040
/// Note that this method takes the most recent value at `key` even if it is from a file with reduced trust.
41-
pub fn integer(&self, key: &str) -> Option<i64> {
41+
pub fn integer<'a>(&self, key: impl Into<&'a BStr>) -> Option<i64> {
4242
self.try_integer(key).and_then(Result::ok)
4343
}
4444

4545
/// Like [`integer()`][Self::integer()], but it will report an error if the value couldn't be interpreted as boolean.
46-
pub fn try_integer(&self, key: &str) -> Option<Result<i64, git_config::value::Error>> {
46+
pub fn try_integer<'a>(&self, key: impl Into<&'a BStr>) -> Option<Result<i64, git_config::value::Error>> {
4747
let key = git_config::parse::key(key)?;
4848
self.repo
4949
.config
@@ -54,7 +54,7 @@ impl<'repo> Snapshot<'repo> {
5454
/// Return the string at `key`, or `None` if there is no such value.
5555
///
5656
/// Note that this method takes the most recent value at `key` even if it is from a file with reduced trust.
57-
pub fn string(&self, key: &str) -> Option<Cow<'_, BStr>> {
57+
pub fn string<'a>(&self, key: impl Into<&'a BStr>) -> Option<Cow<'_, BStr>> {
5858
let key = git_config::parse::key(key)?;
5959
self.repo
6060
.config
@@ -65,9 +65,9 @@ impl<'repo> Snapshot<'repo> {
6565
/// Return the trusted and fully interpolated path at `key`, or `None` if there is no such value
6666
/// or if no value was found in a trusted file.
6767
/// An error occurs if the path could not be interpolated to its final value.
68-
pub fn trusted_path(
68+
pub fn trusted_path<'a>(
6969
&self,
70-
key: &str,
70+
key: impl Into<&'a BStr>,
7171
) -> Option<Result<Cow<'_, std::path::Path>, git_config::path::interpolate::Error>> {
7272
let key = git_config::parse::key(key)?;
7373
self.repo

git-repository/src/reference/remote/mod.rs renamed to git-repository/src/reference/remote.rs

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
1-
use std::{borrow::Cow, convert::TryInto};
1+
use std::convert::TryInto;
22

3-
use crate::{
4-
bstr::{BStr, ByteSlice},
5-
remote, Reference,
6-
};
7-
8-
/// The name of a remote, either interpreted as symbol like `origin` or as url as returned by [`Reference::remote_name()`].
9-
#[derive(Debug, PartialEq, Eq, Clone)]
10-
pub enum Name<'repo> {
11-
/// A symbolic name, like `origin`
12-
Symbol(Cow<'repo, str>),
13-
/// A url pointing to the remote host directly.
14-
Url(Cow<'repo, BStr>),
15-
}
16-
17-
mod name;
3+
use crate::{remote, Reference};
184

195
/// Remotes
206
impl<'repo> Reference<'repo> {
@@ -29,8 +15,8 @@ impl<'repo> Reference<'repo> {
2915
/// - it's recommended to use the [`remote(…)`][Self::remote()] method as it will configure the remote with additional
3016
/// information.
3117
/// - `branch.<name>.pushRemote` falls back to `branch.<name>.remote`.
32-
pub fn remote_name(&self, direction: remote::Direction) -> Option<Name<'repo>> {
33-
let name = self.name().shorten().to_str().ok()?;
18+
pub fn remote_name(&self, direction: remote::Direction) -> Option<remote::Name<'repo>> {
19+
let name = self.name().shorten();
3420
let config = &self.repo.config.resolved;
3521
(direction == remote::Direction::Push)
3622
.then(|| {
@@ -54,8 +40,8 @@ impl<'repo> Reference<'repo> {
5440
) -> Option<Result<crate::Remote<'repo>, remote::find::existing::Error>> {
5541
// TODO: use `branch.<name>.merge`
5642
self.remote_name(direction).map(|name| match name {
57-
Name::Symbol(name) => self.repo.find_remote(name.as_ref()).map_err(Into::into),
58-
Name::Url(url) => git_url::parse(url.as_ref()).map_err(Into::into).and_then(|url| {
43+
remote::Name::Symbol(name) => self.repo.find_remote(name.as_ref()).map_err(Into::into),
44+
remote::Name::Url(url) => git_url::parse(url.as_ref()).map_err(Into::into).and_then(|url| {
5945
self.repo
6046
.remote_at(url)
6147
.map_err(|err| remote::find::existing::Error::Find(remote::find::Error::Init(err)))

git-repository/src/remote/access.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,8 @@ use crate::{bstr::BStr, remote, Remote};
55
/// Access
66
impl<'repo> Remote<'repo> {
77
/// Return the name of this remote or `None` if it wasn't persisted to disk yet.
8-
// TODO: name can also be a URL but we don't see it like this. There is a problem with accessing such names
9-
// too as they would require a BStr, but valid subsection names are strings, so some degeneration must happen
10-
// for access at least. Argh. Probably use `reference::remote::Name` and turn it into `remote::Name` as it's
11-
// actually correct.
12-
pub fn name(&self) -> Option<&str> {
13-
self.name.as_deref()
8+
pub fn name(&self) -> Option<&remote::Name<'static>> {
9+
self.name.as_ref()
1410
}
1511

1612
/// Return our repository reference.

git-repository/src/remote/errors.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub mod find {
2626

2727
///
2828
pub mod existing {
29+
use crate::bstr::BString;
30+
2931
/// The error returned by [`Repository::find_remote(…)`][crate::Repository::find_remote()].
3032
#[derive(Debug, thiserror::Error)]
3133
#[allow(missing_docs)]
@@ -35,7 +37,7 @@ pub mod find {
3537
#[error("remote name could not be parsed as URL")]
3638
UrlParse(#[from] git_url::parse::Error),
3739
#[error("The remote named {name:?} did not exist")]
38-
NotFound { name: String },
40+
NotFound { name: BString },
3941
}
4042
}
4143
}

git-repository/src/remote/init.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ mod error {
1111
#[derive(Debug, thiserror::Error)]
1212
#[allow(missing_docs)]
1313
pub enum Error {
14-
#[error(transparent)]
15-
Name(#[from] crate::remote::name::Error),
1614
#[error(transparent)]
1715
Url(#[from] git_url::parse::Error),
1816
#[error("The rewritten {kind} url {rewritten_url:?} failed to parse")]
@@ -23,12 +21,13 @@ mod error {
2321
},
2422
}
2523
}
24+
use crate::bstr::BString;
2625
pub use error::Error;
2726

2827
/// Initialization
2928
impl<'repo> Remote<'repo> {
3029
pub(crate) fn from_preparsed_config(
31-
name: Option<String>,
30+
name_or_url: Option<BString>,
3231
url: Option<git_url::Url>,
3332
push_url: Option<git_url::Url>,
3433
fetch_specs: Vec<RefSpec>,
@@ -44,7 +43,7 @@ impl<'repo> Remote<'repo> {
4443
.then(|| rewrite_urls(&repo.config, url.as_ref(), push_url.as_ref()))
4544
.unwrap_or(Ok((None, None)))?;
4645
Ok(Remote {
47-
name: name.map(remote::name::validated).transpose()?,
46+
name: name_or_url.map(Into::into),
4847
url,
4948
url_alias,
5049
push_url,

git-repository/src/remote/mod.rs

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use crate::bstr::BStr;
2+
use std::borrow::Cow;
3+
14
/// The direction of an operation carried out (or to be carried out) through a remote.
25
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
36
pub enum Direction {
@@ -17,35 +20,24 @@ impl Direction {
1720
}
1821
}
1922

23+
/// The name of a remote, either interpreted as symbol like `origin` or as url as returned by [`Remote::name()`][crate::Remote::name()].
24+
#[derive(Debug, PartialEq, Eq, Clone)]
25+
pub enum Name<'repo> {
26+
/// A symbolic name, like `origin`.
27+
/// Note that it has not necessarily been validated yet.
28+
Symbol(Cow<'repo, str>),
29+
/// A url pointing to the remote host directly.
30+
Url(Cow<'repo, BStr>),
31+
}
32+
33+
///
34+
pub mod name;
35+
2036
mod build;
2137

2238
mod errors;
2339
pub use errors::find;
2440

25-
///
26-
pub mod name {
27-
/// The error returned by [validated()].
28-
#[derive(Debug, thiserror::Error)]
29-
#[error("remote names must be valid within refspecs for fetching: {name:?}")]
30-
#[allow(missing_docs)]
31-
pub struct Error {
32-
source: git_refspec::parse::Error,
33-
name: String,
34-
}
35-
36-
/// Return `name` if it is valid or convert it into an `Error`.
37-
pub fn validated(name: impl Into<String>) -> Result<String, Error> {
38-
let name = name.into();
39-
match git_refspec::parse(
40-
format!("refs/heads/test:refs/remotes/{name}/test").as_str().into(),
41-
git_refspec::parse::Operation::Fetch,
42-
) {
43-
Ok(_) => Ok(name),
44-
Err(err) => Err(Error { source: err, name }),
45-
}
46-
}
47-
}
48-
4941
///
5042
pub mod init;
5143

git-repository/src/reference/remote/name.rs renamed to git-repository/src/remote/name.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
11
use std::{borrow::Cow, convert::TryFrom};
22

33
use super::Name;
4-
use crate::bstr::{BStr, ByteSlice, ByteVec};
4+
use crate::bstr::{BStr, BString, ByteSlice, ByteVec};
5+
6+
/// The error returned by [validated()].
7+
#[derive(Debug, thiserror::Error)]
8+
#[error("remote names must be valid within refspecs for fetching: {name:?}")]
9+
#[allow(missing_docs)]
10+
pub struct Error {
11+
source: git_refspec::parse::Error,
12+
name: BString,
13+
}
14+
15+
/// Return `name` if it is valid or convert it into an `Error`.
16+
pub fn validated(name: impl Into<BString>) -> Result<BString, Error> {
17+
let name = name.into();
18+
match git_refspec::parse(
19+
format!("refs/heads/test:refs/remotes/{name}/test").as_str().into(),
20+
git_refspec::parse::Operation::Fetch,
21+
) {
22+
Ok(_) => Ok(name),
23+
Err(err) => Err(Error { source: err, name }),
24+
}
25+
}
526

627
impl Name<'_> {
728
/// Obtain the name as string representation.
@@ -48,6 +69,12 @@ impl<'a> TryFrom<Cow<'a, BStr>> for Name<'a> {
4869
}
4970
}
5071

72+
impl From<BString> for Name<'static> {
73+
fn from(name: BString) -> Self {
74+
Self::try_from(Cow::Owned(name)).expect("String is never illformed")
75+
}
76+
}
77+
5178
impl<'a> AsRef<BStr> for Name<'a> {
5279
fn as_ref(&self) -> &BStr {
5380
self.as_bstr()

0 commit comments

Comments
 (0)