Skip to content

Commit

Permalink
feat: open::Options::config_overrides() for early configuration; su…
Browse files Browse the repository at this point in the history
…pport for `init.defaultBranch`. (#450)
  • Loading branch information
Byron committed Oct 12, 2022
1 parent 6225f35 commit 0b5c53e
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 62 deletions.
5 changes: 5 additions & 0 deletions git-repository/src/config/cache/init.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::{interpolate_context, util, Error, StageOne};
use crate::bstr::BString;
use crate::{config::Cache, repository};

/// Initialization
Expand Down Expand Up @@ -33,6 +34,7 @@ impl Cache {
includes: use_includes,
}: repository::permissions::Config,
lenient_config: bool,
config_overrides: &[BString],
) -> Result<Self, Error> {
let options = git_config::file::init::Options {
includes: if use_includes {
Expand Down Expand Up @@ -108,6 +110,9 @@ impl Cache {
if use_env {
globals.append(git_config::File::from_env(options)?.unwrap_or_default());
}
if !config_overrides.is_empty() {
crate::config::overrides::apply(&mut globals, config_overrides, git_config::Source::Api)?;
}
globals
};

Expand Down
7 changes: 6 additions & 1 deletion git-repository/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use crate::{bstr::BString, remote, repository::identity, revision::spec, Reposit

pub(crate) mod cache;
mod snapshot;
pub use snapshot::{apply_cli_overrides, credential_helpers};
pub use snapshot::credential_helpers;

///
pub mod overrides;

/// A platform to access configuration values as read from disk.
///
Expand Down Expand Up @@ -62,6 +65,8 @@ pub enum Error {
DecodeBoolean { key: String, value: BString },
#[error(transparent)]
PathInterpolation(#[from] git_config::path::interpolate::Error),
#[error("Configuration overrides at open or init time could not be applied.")]
ConfigOverrides(#[from] overrides::Error),
}

/// Utility type to keep pre-obtained configuration values, only for those required during initial setup
Expand Down
43 changes: 43 additions & 0 deletions git-repository/src/config/overrides.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use crate::bstr::{BStr, BString, ByteSlice};
use std::convert::TryFrom;

/// The error returned by [SnapshotMut::apply_cli_overrides()][crate::config::SnapshotMut::apply_cli_overrides()].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("{input:?} is not a valid configuration key. Examples are 'core.abbrev' or 'remote.origin.url'")]
InvalidKey { input: BString },
#[error("Key {key:?} could not be parsed")]
SectionKey {
key: BString,
source: git_config::parse::section::key::Error,
},
#[error(transparent)]
SectionHeader(#[from] git_config::parse::section::header::Error),
}

pub(crate) fn apply(
config: &mut git_config::File<'static>,
values: impl IntoIterator<Item = impl AsRef<BStr>>,
source: git_config::Source,
) -> Result<(), Error> {
let mut file = git_config::File::new(git_config::file::Metadata::from(source));
for key_value in values {
let key_value = key_value.as_ref();
let mut tokens = key_value.splitn(2, |b| *b == b'=').map(|v| v.trim());
let key = tokens.next().expect("always one value").as_bstr();
let value = tokens.next();
let key = git_config::parse::key(key.to_str().map_err(|_| Error::InvalidKey { input: key.into() })?)
.ok_or_else(|| Error::InvalidKey { input: key.into() })?;
let mut section = file.section_mut_or_create_new(key.section_name, key.subsection_name)?;
section.push(
git_config::parse::section::Key::try_from(key.value_name.to_owned()).map_err(|err| Error::SectionKey {
source: err,
key: key.value_name.into(),
})?,
value.map(|v| v.as_bstr()),
);
}
config.append(file);
Ok(())
}
9 changes: 9 additions & 0 deletions git-repository/src/config/snapshot/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ impl<'repo> Snapshot<'repo> {

/// Utilities
impl<'repo> SnapshotMut<'repo> {
/// Apply configuration values of the form `core.abbrev=5` or `remote.origin.url = foo` or `core.bool-implicit-true`
/// to the repository configuration, marked with [source CLI][git_config::Source::Cli].
pub fn apply_cli_overrides(
&mut self,
values: impl IntoIterator<Item = impl AsRef<BStr>>,
) -> Result<&mut Self, crate::config::overrides::Error> {
crate::config::overrides::apply(&mut self.config, values, git_config::Source::Cli)?;
Ok(self)
}
/// Apply all changes made to this instance.
///
/// Note that this would also happen once this instance is dropped, but using this method may be more intuitive and won't squelch errors
Expand Down
46 changes: 1 addition & 45 deletions git-repository/src/config/snapshot/apply_cli_overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,4 @@ use crate::{
config::SnapshotMut,
};

/// The error returned by [SnapshotMut::apply_cli_overrides()][crate::config::SnapshotMut::apply_cli_overrides()].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("{input:?} is not a valid configuration key. Examples are 'core.abbrev' or 'remote.origin.url'")]
InvalidKey { input: BString },
#[error("Key {key:?} could not be parsed")]
SectionKey {
key: BString,
source: git_config::parse::section::key::Error,
},
#[error(transparent)]
SectionHeader(#[from] git_config::parse::section::header::Error),
}

impl SnapshotMut<'_> {
/// Apply configuration values of the form `core.abbrev=5` or `remote.origin.url = foo` or `core.bool-implicit-true`
/// to the repository configuration, marked with [source CLI][git_config::Source::Cli].
pub fn apply_cli_overrides(
&mut self,
values: impl IntoIterator<Item = impl AsRef<BStr>>,
) -> Result<&mut Self, Error> {
let mut file = git_config::File::new(git_config::file::Metadata::from(git_config::Source::Cli));
for key_value in values {
let key_value = key_value.as_ref();
let mut tokens = key_value.splitn(2, |b| *b == b'=').map(|v| v.trim());
let key = tokens.next().expect("always one value").as_bstr();
let value = tokens.next();
let key = git_config::parse::key(key.to_str().map_err(|_| Error::InvalidKey { input: key.into() })?)
.ok_or_else(|| Error::InvalidKey { input: key.into() })?;
let mut section = file.section_mut_or_create_new(key.section_name, key.subsection_name)?;
section.push(
git_config::parse::section::Key::try_from(key.value_name.to_owned()).map_err(|err| {
Error::SectionKey {
source: err,
key: key.value_name.into(),
}
})?,
value.map(|v| v.as_bstr()),
);
}
self.config.append(file);
Ok(self)
}
}
impl SnapshotMut<'_> {}
3 changes: 0 additions & 3 deletions git-repository/src/config/snapshot/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
mod _impls;
mod access;

///
pub mod apply_cli_overrides;

///
pub mod credential_helpers;
46 changes: 45 additions & 1 deletion git-repository/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,12 @@ pub mod remote;

///
pub mod init {
use crate::bstr::BString;
use git_ref::store::WriteReflog;
use git_ref::transaction::{PreviousValue, RefEdit};
use git_ref::{FullName, Target};
use std::borrow::Cow;
use std::convert::TryInto;
use std::path::Path;

use crate::ThreadSafeRepository;
Expand All @@ -333,6 +339,13 @@ pub mod init {
Init(#[from] crate::create::Error),
#[error(transparent)]
Open(#[from] crate::open::Error),
#[error("Invalid default branch name: {name:?}")]
InvalidBranchName {
name: BString,
source: git_validate::refname::Error,
},
#[error("Could not edit HEAD reference with new default name")]
EditHeadForDefaultBranch(#[from] crate::reference::edit::Error),
}

impl ThreadSafeRepository {
Expand All @@ -347,6 +360,11 @@ pub mod init {
}

/// Similar to [`init`][Self::init()], but allows to determine how exactly to open the newly created repository.
///
/// # Deviation
///
/// Instead of naming the default branch `master`, we name it `main` unless configured explicitly using the `init.defaultBranch`
/// configuration key.
pub fn init_opts(
directory: impl AsRef<Path>,
create_options: crate::create::Options,
Expand All @@ -355,7 +373,33 @@ pub mod init {
let path = crate::create::into(directory.as_ref(), create_options)?;
let (git_dir, worktree_dir) = path.into_repository_and_work_tree_directories();
open_options.git_dir_trust = Some(git_sec::Trust::Full);
ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, open_options).map_err(Into::into)
let repo = ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, open_options)?;
let branch_name = repo
.config
.resolved
.string("init", None, "defaultBranch")
.unwrap_or(Cow::Borrowed("main".into()));
if branch_name.as_ref() != "main" {
let sym_ref: FullName =
format!("refs/heads/{branch_name}")
.try_into()
.map_err(|err| Error::InvalidBranchName {
name: branch_name.into_owned(),
source: err,
})?;
let mut repo = repo.to_thread_local();
repo.refs.write_reflog = WriteReflog::Disable;
repo.edit_reference(RefEdit {
change: git_ref::transaction::Change::Update {
log: Default::default(),
expected: PreviousValue::Any,
new: Target::Symbolic(sym_ref),
},
name: "HEAD".try_into().expect("valid"),
deref: false,
})?;
}
Ok(repo)
}
}
}
Expand Down
17 changes: 16 additions & 1 deletion git-repository/src/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::path::PathBuf;

use git_features::threading::OwnShared;

use crate::bstr::BString;
use crate::{config, config::cache::interpolate_context, permission, Permissions, ThreadSafeRepository};

/// A way to configure the usage of replacement objects, see `git replace`.
Expand Down Expand Up @@ -72,6 +73,7 @@ pub struct Options {
pub(crate) lossy_config: Option<bool>,
pub(crate) lenient_config: bool,
pub(crate) bail_if_untrusted: bool,
pub(crate) config_overrides: Vec<BString>,
}

impl Default for Options {
Expand All @@ -85,6 +87,7 @@ impl Default for Options {
lossy_config: None,
lenient_config: true,
bail_if_untrusted: false,
config_overrides: Vec::new(),
}
}
}
Expand Down Expand Up @@ -126,6 +129,14 @@ impl Options {

/// Builder methods
impl Options {
/// Apply the given configuration `values` like `init.defaultBranch=special` or `core.bool-implicit-true` in memory to as early
/// as the configuration is initialized to allow affecting the repository instantiation phase, both on disk or when opening.
/// The configuration is marked with [source API][git_config::Source::Api].
pub fn config_overrides(mut self, values: impl IntoIterator<Item = impl Into<BString>>) -> Self {
self.config_overrides = values.into_iter().map(Into::into).collect();
self
}

/// Set the amount of slots to use for the object database. It's a value that doesn't need changes on the client, typically,
/// but should be controlled on the server.
pub fn object_store_slots(mut self, slots: git_odb::store::init::Slots) -> Self {
Expand Down Expand Up @@ -225,6 +236,7 @@ impl git_sec::trust::DefaultForLevel for Options {
lossy_config: None,
bail_if_untrusted: false,
lenient_config: true,
config_overrides: Vec::new(),
},
git_sec::Trust::Reduced => Options {
object_store_slots: git_odb::store::init::Slots::Given(32), // limit resource usage
Expand All @@ -235,6 +247,7 @@ impl git_sec::trust::DefaultForLevel for Options {
bail_if_untrusted: false,
lenient_config: true,
lossy_config: None,
config_overrides: Vec::new(),
},
}
}
Expand Down Expand Up @@ -330,6 +343,7 @@ impl ThreadSafeRepository {
lenient_config,
bail_if_untrusted,
permissions: Permissions { ref env, config },
ref config_overrides,
} = options;
let git_dir_trust = git_dir_trust.expect("trust must be been determined by now");

Expand Down Expand Up @@ -368,6 +382,7 @@ impl ThreadSafeRepository {
env.clone(),
config,
lenient_config,
config_overrides,
)?;

if bail_if_untrusted && git_dir_trust != git_sec::Trust::Full {
Expand Down Expand Up @@ -505,7 +520,7 @@ mod tests {
fn size_of_options() {
assert_eq!(
std::mem::size_of::<Options>(),
72,
96,
"size shouldn't change without us knowing"
);
}
Expand Down
31 changes: 24 additions & 7 deletions git-repository/tests/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,39 @@ mod bare {
}

mod non_bare {
use git_repository as git;

#[test]
fn init_bare_with_custom_branch_name() -> crate::Result {
let tmp = tempfile::tempdir()?;
let repo: git::Repository = git::ThreadSafeRepository::init_opts(
tmp.path(),
git::create::Options {
bare: true,
fs_capabilities: None,
},
git::open::Options::isolated().config_overrides(Some("init.defaultBranch=special")),
)?
.into();
assert_eq!(
repo.head()?.referent_name().expect("name").as_bstr(),
"refs/heads/special"
);
Ok(())
}
#[test]
fn init_into_empty_directory_creates_a_dot_git_dir() -> crate::Result {
let tmp = tempfile::tempdir()?;
let repo = git_repository::init(tmp.path())?;
assert_eq!(repo.kind(), git_repository::Kind::WorkTree { is_linked: false });
let repo = git::init(tmp.path())?;
assert_eq!(repo.kind(), git::Kind::WorkTree { is_linked: false });
assert_eq!(repo.work_dir(), Some(tmp.path()), "there is a work tree by default");
assert_eq!(
repo.git_dir(),
tmp.path().join(".git"),
"there is a work tree by default"
);
assert_eq!(git_repository::open(repo.git_dir())?, repo);
assert_eq!(
git_repository::open(repo.work_dir().as_ref().expect("non-bare repo"))?,
repo
);
assert_eq!(git::open(repo.git_dir())?, repo);
assert_eq!(git::open(repo.work_dir().as_ref().expect("non-bare repo"))?, repo);
Ok(())
}

Expand Down
9 changes: 5 additions & 4 deletions git-repository/tests/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ mod worktree;

#[test]
fn size_in_memory() {
let expected = [728, 744, 784];
let actual_size = std::mem::size_of::<Repository>();
let limit = 810;
assert!(
expected.contains(&actual_size),
"size of Repository shouldn't change without us noticing, it's meant to be cloned: should have been within {:?}, was {}",
expected, actual_size
actual_size < limit,
"size of Repository shouldn't change without us noticing, it's meant to be cloned: should have been below {:?}, was {}",
limit,
actual_size
);
}

0 comments on commit 0b5c53e

Please sign in to comment.