summaryrefslogtreecommitdiffstats
path: root/vendor/gix-config/src/source.rs
blob: b1991e6b49125a0543f5ddc7ad5cb3b6a1159cff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
use std::{
    borrow::Cow,
    ffi::OsString,
    path::{Path, PathBuf},
};

use crate::Source;

/// The category of a [`Source`], in order of ascending precedence.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub enum Kind {
    /// A special configuration file that ships with the git installation, and is thus tied to the used git binary.
    GitInstallation,
    /// A source shared for the entire system.
    System,
    /// Application specific configuration unique for each user of the `System`.
    Global,
    /// Configuration relevant only to the repository, possibly including the worktree.
    Repository,
    /// Configuration specified after all other configuration was loaded for the purpose of overrides.
    Override,
}

impl Kind {
    /// Return a list of sources associated with this `Kind` of source, in order of ascending precedence.
    pub fn sources(self) -> &'static [Source] {
        let src = match self {
            Kind::GitInstallation => &[Source::GitInstallation] as &[_],
            Kind::System => &[Source::System],
            Kind::Global => &[Source::Git, Source::User],
            Kind::Repository => &[Source::Local, Source::Worktree],
            Kind::Override => &[Source::Env, Source::Cli, Source::Api],
        };
        debug_assert!(
            src.iter().all(|src| src.kind() == self),
            "BUG: classification of source has to match the ordering here, see `Source::kind()`"
        );
        src
    }
}

impl Source {
    /// Return true if the source indicates a location within a file of a repository.
    pub const fn kind(self) -> Kind {
        use Source::*;
        match self {
            GitInstallation => Kind::GitInstallation,
            System => Kind::System,
            Git | User => Kind::Global,
            Local | Worktree => Kind::Repository,
            Env | Cli | Api | EnvOverride => Kind::Override,
        }
    }

    /// Returns the location at which a file of this type would be stored, or `None` if
    /// there is no notion of persistent storage for this source, with `env_var` to obtain environment variables.
    /// Note that the location can be relative for repository-local sources like `Local` and `Worktree`,
    /// and the caller has to known which base it it relative to, namely the `common_dir` in the `Local` case
    /// and the `git_dir` in the `Worktree` case.
    /// Be aware that depending on environment overrides, multiple scopes might return the same path, which should
    /// only be loaded once nonetheless.
    ///
    /// With `env_var` it becomes possible to prevent accessing environment variables entirely to comply with `gix-sec`
    /// permissions for example.
    pub fn storage_location(self, env_var: &mut dyn FnMut(&str) -> Option<OsString>) -> Option<Cow<'static, Path>> {
        use Source::*;
        match self {
            GitInstallation => git::install_config_path().map(gix_path::from_bstr),
            System => env_var("GIT_CONFIG_NO_SYSTEM")
                .is_none()
                .then(|| PathBuf::from(env_var("GIT_CONFIG_SYSTEM").unwrap_or_else(|| "/etc/gitconfig".into())).into()),
            Git => match env_var("GIT_CONFIG_GLOBAL") {
                Some(global_override) => Some(PathBuf::from(global_override).into()),
                None => env_var("XDG_CONFIG_HOME")
                    .map(|home| {
                        let mut p = PathBuf::from(home);
                        p.push("git");
                        p.push("config");
                        p
                    })
                    .or_else(|| {
                        env_var("HOME").map(|home| {
                            let mut p = PathBuf::from(home);
                            p.push(".config");
                            p.push("git");
                            p.push("config");
                            p
                        })
                    })
                    .map(Cow::Owned),
            },
            User => env_var("GIT_CONFIG_GLOBAL")
                .map(|global_override| PathBuf::from(global_override).into())
                .or_else(|| {
                    env_var("HOME").map(|home| {
                        let mut p = PathBuf::from(home);
                        p.push(".gitconfig");
                        p.into()
                    })
                }),
            Local => Some(Path::new("config").into()),
            Worktree => Some(Path::new("config.worktree").into()),
            Env | Cli | Api | EnvOverride => None,
        }
    }
}

/// Environment information involving the `git` program itself.
mod git {
    use std::process::{Command, Stdio};

    use bstr::{BStr, BString, ByteSlice};

    /// Returns the file that contains git configuration coming with the installation of the `git` file in the current `PATH`, or `None`
    /// if no `git` executable was found or there were other errors during execution.
    pub fn install_config_path() -> Option<&'static BStr> {
        static PATH: once_cell::sync::Lazy<Option<BString>> = once_cell::sync::Lazy::new(|| {
            let mut cmd = Command::new(if cfg!(windows) { "git.exe" } else { "git" });
            cmd.args(["config", "-l", "--show-origin"])
                .stdin(Stdio::null())
                .stderr(Stdio::null());
            first_file_from_config_with_origin(cmd.output().ok()?.stdout.as_slice().into()).map(ToOwned::to_owned)
        });
        PATH.as_ref().map(|b| b.as_ref())
    }

    fn first_file_from_config_with_origin(source: &BStr) -> Option<&BStr> {
        let file = source.strip_prefix(b"file:")?;
        let end_pos = file.find_byte(b'\t')?;
        file[..end_pos].as_bstr().into()
    }

    #[cfg(test)]
    mod tests {
        #[test]
        fn first_file_from_config_with_origin() {
            let macos = "file:/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig	credential.helper=osxkeychain\nfile:/Users/byron/.gitconfig	push.default=simple\n";
            let win_msys =
                "file:C:/git-sdk-64/etc/gitconfig	core.symlinks=false\r\nfile:C:/git-sdk-64/etc/gitconfig	core.autocrlf=true";
            let win_cmd = "file:C:/Program Files/Git/etc/gitconfig	diff.astextplain.textconv=astextplain\r\nfile:C:/Program Files/Git/etc/gitconfig	filter.lfs.clean=gix-lfs clean -- %f\r\n";
            let linux = "file:/home/parallels/.gitconfig	core.excludesfile=~/.gitignore\n";
            let bogus = "something unexpected";
            let empty = "";

            for (source, expected) in [
                (
                    macos,
                    Some("/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig"),
                ),
                (win_msys, Some("C:/git-sdk-64/etc/gitconfig")),
                (win_cmd, Some("C:/Program Files/Git/etc/gitconfig")),
                (linux, Some("/home/parallels/.gitconfig")),
                (bogus, None),
                (empty, None),
            ] {
                assert_eq!(
                    super::first_file_from_config_with_origin(source.into()),
                    expected.map(Into::into)
                );
            }
        }
    }
}