Skip to content

New remotes save with unexpected metadata in the presence of global configuration #1951

Open
@emilazy

Description

@emilazy

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    acknowledgedan issue is accepted as shortcoming to be fixedhelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions