Description
Current behavior 😯
When a user has global configuration like remote.origin.prune = true
, a repository configuration with no remote is opened, and a remote is created and saved with that name to the repository’s configuration, the new configuration is written to the existing section from the global configuration. This means that the metadata from the global configuration (including trust) is used instead of the file’s metadata, and an attempt to save the modified configuration with config.write_to_filter(&mut config_file, |section| section.meta() == config.meta())
will omit the new remote.
This differs from the behaviour when there is no configuration for that remote outside of the repository configuration file, or the behaviour where a separate remote section already exiets in the repository configuration.
Expected behavior 🤔
A new remote section is created if it does not already exist in the gix::config::File
, even if there is configuration for that remote from other sources.
In general I’m not sure when you would ever want the behaviour of the current section_mut
family of functions, and I suspect this is downstream of the fact that gix::config::File
mixes together sections from all sources, which also makes editing and saving configuration somewhat inconvenient. I think it is likely that a better design would have a gix::config::File
per configuration source, with an abstraction on top that layers them together for look‐up of values. Mutating an abstraction consisting of the combination of multiple cascading configurations doesn’t make that much sense; you want to use it to decide the effective values, but operate on one specific file for mutation and writing.
However, this could be worked around in the remote API by simply creating a new section if one with metadata that matches the actual file doesn’t already exist.
Git behavior
yuyuko:/v/f/y/m/T/tmp.vVfQbOHnTq
❭ git init
Initialized empty Git repository in /private/var/folders/yd/mh726b5d2vqfyp132jtzq9t80000gn/T/tmp.vVfQbOHnTq/.git/
yuyuko:/v/f/y/m/T/tmp.vVfQbOHnTq
❭ git -c remote.origin.prune=true remote add origin https://example.com/
yuyuko:/v/f/y/m/T/tmp.vVfQbOHnTq
❭ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://example.com/
fetch = +refs/heads/*:refs/remotes/origin/*
Steps to reproduce 🕹
use std::fs::File;
use tempfile::TempDir;
type Error = Box<dyn std::error::Error>;
fn save_config(config: &gix::config::File) -> Result<(), Error> {
let mut config_file = File::create(config.meta().path.as_ref().unwrap())?;
config.write_to(&mut config_file)?;
Ok(())
}
fn dump_remote_sections(header: &str, config: &gix::config::File) -> Result<(), Error> {
println!("# {header}");
println!("# {:?}", config.meta());
for section in config.sections_by_name("remote").unwrap() {
println!("## {:?}", section.meta());
section.write_to(&mut std::io::stdout())?;
}
println!();
Ok(())
}
fn open_repo(repo_dir: &TempDir) -> Result<gix::Repository, Error> {
Ok(gix::open::Options::default()
.with(gix::sec::Trust::Reduced)
.config_overrides([b"remote.origin.prune = true"])
.open(repo_dir.path())?
.to_thread_local())
}
fn demonstrate_bug(repo_dir: &TempDir) -> Result<(), Error> {
let repo = open_repo(repo_dir)?;
let mut config = repo.config_snapshot().clone();
dump_remote_sections("Initial state", &config)?;
repo.remote_at("https://example.com/")?
.save_as_to("origin", &mut config)?;
dump_remote_sections("After saving remote", &config)?;
save_config(&config)?;
dump_remote_sections("After reload", &open_repo(repo_dir)?.config_snapshot())?;
Ok(())
}
fn main() -> Result<(), Error> {
let repo_dir = tempfile::tempdir()?;
gix::init(&repo_dir)?;
demonstrate_bug(&repo_dir)?;
Ok(())
}
Output:
# Initial state
# Metadata { path: Some("/var/folders/yd/mh726b5d2vqfyp132jtzq9t80000gn/T/.tmpke1Cbq/.git/config"), source: Local, level: 0, trust: Reduced }
## Metadata { path: None, source: Api, level: 0, trust: Full }
[remote "origin"]
prune = true
# After saving remote
# Metadata { path: Some("/var/folders/yd/mh726b5d2vqfyp132jtzq9t80000gn/T/.tmpke1Cbq/.git/config"), source: Local, level: 0, trust: Reduced }
## Metadata { path: None, source: Api, level: 0, trust: Full }
[remote "origin"]
prune = true
url = https://example.com/
# After reload
# Metadata { path: Some("/var/folders/yd/mh726b5d2vqfyp132jtzq9t80000gn/T/.tmpke1Cbq/.git/config"), source: Local, level: 0, trust: Reduced }
## Metadata { path: Some("/var/folders/yd/mh726b5d2vqfyp132jtzq9t80000gn/T/.tmpke1Cbq/.git/config"), source: Local, level: 0, trust: Reduced }
[remote "origin"]
prune = true
url = https://example.com/
## Metadata { path: None, source: Api, level: 0, trust: Full }
[remote "origin"]
prune = true