gix_config/file/includes/
mod.rs

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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
use std::{
    borrow::Cow,
    path::{Path, PathBuf},
};

use bstr::{BStr, BString, ByteSlice, ByteVec};
use gix_features::threading::OwnShared;
use gix_ref::Category;

use crate::{
    file,
    file::{includes, init, Metadata, SectionId},
    path, File,
};

impl File<'static> {
    /// Traverse all `include` and `includeIf` directives found in this instance and follow them, loading the
    /// referenced files from their location and adding their content right past the value that included them.
    ///
    /// # Limitations
    ///
    /// - Note that this method is _not idempotent_ and calling it multiple times will resolve includes multiple
    ///   times. It's recommended use is as part of a multi-step bootstrapping which needs fine-grained control,
    ///   and unless that's given one should prefer one of the other ways of initialization that resolve includes
    ///   at the right time.
    /// - included values are added after the _section_ that included them, not directly after the value. This is
    ///   a deviation from how git does it, as it technically adds new value right after the include path itself,
    ///   technically 'splitting' the section. This can only make a difference if the `include` section also has values
    ///   which later overwrite portions of the included file, which seems unusual as these would be related to `includes`.
    ///   We can fix this by 'splitting' the include section if needed so the included sections are put into the right place.
    pub fn resolve_includes(&mut self, options: init::Options<'_>) -> Result<(), Error> {
        if options.includes.max_depth == 0 {
            return Ok(());
        }
        let mut buf = Vec::new();
        resolve(self, &mut buf, options)
    }
}

pub(crate) fn resolve(config: &mut File<'static>, buf: &mut Vec<u8>, options: init::Options<'_>) -> Result<(), Error> {
    resolve_includes_recursive(config, 0, buf, options)
}

fn resolve_includes_recursive(
    target_config: &mut File<'static>,
    depth: u8,
    buf: &mut Vec<u8>,
    options: init::Options<'_>,
) -> Result<(), Error> {
    if depth == options.includes.max_depth {
        return if options.includes.err_on_max_depth_exceeded {
            Err(Error::IncludeDepthExceeded {
                max_depth: options.includes.max_depth,
            })
        } else {
            Ok(())
        };
    }

    let mut section_ids_and_include_paths = Vec::new();
    for (id, section) in target_config
        .section_order
        .iter()
        .map(|id| (*id, &target_config.sections[id]))
    {
        let header = &section.header;
        let header_name = header.name.as_ref();
        if header_name == "include" && header.subsection_name.is_none() {
            detach_include_paths(&mut section_ids_and_include_paths, section, id);
        } else if header_name == "includeIf" {
            if let Some(condition) = &header.subsection_name {
                let target_config_path = section.meta.path.as_deref();
                if include_condition_match(condition.as_ref(), target_config_path, options.includes)? {
                    detach_include_paths(&mut section_ids_and_include_paths, section, id);
                }
            }
        }
    }

    append_followed_includes_recursively(section_ids_and_include_paths, target_config, depth, options, buf)
}

fn append_followed_includes_recursively(
    section_ids_and_include_paths: Vec<(SectionId, crate::Path<'_>)>,
    target_config: &mut File<'static>,
    depth: u8,
    options: init::Options<'_>,
    buf: &mut Vec<u8>,
) -> Result<(), Error> {
    for (section_id, config_path) in section_ids_and_include_paths {
        let meta = OwnShared::clone(&target_config.sections[&section_id].meta);
        let target_config_path = meta.path.as_deref();
        let config_path = match resolve_path(config_path, target_config_path, options.includes)? {
            Some(p) => p,
            None => continue,
        };
        if !config_path.is_file() {
            continue;
        }

        buf.clear();
        std::io::copy(
            &mut std::fs::File::open(&config_path).map_err(|err| Error::Io {
                source: err,
                path: config_path.to_owned(),
            })?,
            buf,
        )
        .map_err(Error::CopyBuffer)?;
        let config_meta = Metadata {
            path: Some(config_path),
            trust: meta.trust,
            level: meta.level + 1,
            source: meta.source,
        };
        let no_follow_options = init::Options {
            includes: includes::Options::no_follow(),
            ..options
        };

        let mut include_config =
            File::from_bytes_owned(buf, config_meta, no_follow_options).map_err(|err| match err {
                init::Error::Parse(err) => Error::Parse(err),
                init::Error::Interpolate(err) => Error::Interpolate(err),
                init::Error::Includes(_) => unreachable!("BUG: {:?} not possible due to no-follow options", err),
            })?;
        resolve_includes_recursive(&mut include_config, depth + 1, buf, options)?;

        target_config.append_or_insert(include_config, Some(section_id));
    }
    Ok(())
}

fn detach_include_paths(
    include_paths: &mut Vec<(SectionId, crate::Path<'static>)>,
    section: &file::Section<'_>,
    id: SectionId,
) {
    include_paths.extend(
        section
            .body
            .values("path")
            .into_iter()
            .map(|path| (id, crate::Path::from(Cow::Owned(path.into_owned())))),
    );
}

fn include_condition_match(
    condition: &BStr,
    target_config_path: Option<&Path>,
    options: Options<'_>,
) -> Result<bool, Error> {
    let mut tokens = condition.splitn(2, |b| *b == b':');
    let (prefix, condition) = match (tokens.next(), tokens.next()) {
        (Some(a), Some(b)) => (a, b),
        _ => return Ok(false),
    };
    let condition = condition.as_bstr();
    match prefix {
        b"gitdir" => gitdir_matches(
            condition,
            target_config_path,
            options,
            gix_glob::wildmatch::Mode::empty(),
        ),
        b"gitdir/i" => gitdir_matches(
            condition,
            target_config_path,
            options,
            gix_glob::wildmatch::Mode::IGNORE_CASE,
        ),
        b"onbranch" => Ok(onbranch_matches(condition, options.conditional).is_some()),
        _ => Ok(false),
    }
}

fn onbranch_matches(
    condition: &BStr,
    conditional::Context { branch_name, .. }: conditional::Context<'_>,
) -> Option<()> {
    let branch_name = branch_name?;
    let (_, branch_name) = branch_name
        .category_and_short_name()
        .filter(|(cat, _)| *cat == Category::LocalBranch)?;

    let condition = if condition.ends_with(b"/") {
        let mut condition: BString = condition.into();
        condition.push_str("**");
        Cow::Owned(condition)
    } else {
        condition.into()
    };

    gix_glob::wildmatch(
        condition.as_ref(),
        branch_name,
        gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
    )
    .then_some(())
}

fn gitdir_matches(
    condition_path: &BStr,
    target_config_path: Option<&Path>,
    Options {
        conditional: conditional::Context { git_dir, .. },
        interpolate: context,
        err_on_interpolation_failure,
        err_on_missing_config_path,
        ..
    }: Options<'_>,
    wildmatch_mode: gix_glob::wildmatch::Mode,
) -> Result<bool, Error> {
    if !err_on_interpolation_failure && git_dir.is_none() {
        return Ok(false);
    }
    let git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_dir.ok_or(Error::MissingGitDir)?));

    let mut pattern_path: Cow<'_, _> = {
        let path = match check_interpolation_result(
            err_on_interpolation_failure,
            crate::Path::from(Cow::Borrowed(condition_path)).interpolate(context),
        )? {
            Some(p) => p,
            None => return Ok(false),
        };
        gix_path::into_bstr(path).into_owned().into()
    };
    // NOTE: yes, only if we do path interpolation will the slashes be forced to unix separators on windows
    if pattern_path != condition_path {
        pattern_path = gix_path::to_unix_separators_on_windows(pattern_path);
    }

    if let Some(relative_pattern_path) = pattern_path.strip_prefix(b"./") {
        if !err_on_missing_config_path && target_config_path.is_none() {
            return Ok(false);
        }
        let parent_dir = target_config_path
            .ok_or(Error::MissingConfigPath)?
            .parent()
            .expect("config path can never be /");
        let mut joined_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(parent_dir)).into_owned();
        joined_path.push(b'/');
        joined_path.extend_from_slice(relative_pattern_path);
        pattern_path = joined_path.into();
    }

    // NOTE: this special handling of leading backslash is needed to do it like git does
    if pattern_path.iter().next() != Some(&(std::path::MAIN_SEPARATOR as u8))
        && !gix_path::from_bstr(pattern_path.clone()).is_absolute()
    {
        let mut prefixed = pattern_path.into_owned();
        prefixed.insert_str(0, "**/");
        pattern_path = prefixed.into();
    }
    if pattern_path.ends_with(b"/") {
        let mut suffixed = pattern_path.into_owned();
        suffixed.push_str("**");
        pattern_path = suffixed.into();
    }

    let match_mode = gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL | wildmatch_mode;
    let is_match = gix_glob::wildmatch(pattern_path.as_bstr(), git_dir.as_bstr(), match_mode);
    if is_match {
        return Ok(true);
    }

    let expanded_git_dir = gix_path::into_bstr(gix_path::realpath(gix_path::from_byte_slice(&git_dir))?);
    Ok(gix_glob::wildmatch(
        pattern_path.as_bstr(),
        expanded_git_dir.as_bstr(),
        match_mode,
    ))
}

fn check_interpolation_result(
    disable: bool,
    res: Result<Cow<'_, std::path::Path>, path::interpolate::Error>,
) -> Result<Option<Cow<'_, std::path::Path>>, path::interpolate::Error> {
    if disable {
        return res.map(Some);
    }
    match res {
        Ok(good) => Ok(good.into()),
        Err(err) => match err {
            path::interpolate::Error::Missing { .. } | path::interpolate::Error::UserInterpolationUnsupported => {
                Ok(None)
            }
            path::interpolate::Error::UsernameConversion(_) | path::interpolate::Error::Utf8Conversion { .. } => {
                Err(err)
            }
        },
    }
}

fn resolve_path(
    path: crate::Path<'_>,
    target_config_path: Option<&Path>,
    includes::Options {
        interpolate: context,
        err_on_interpolation_failure,
        err_on_missing_config_path,
        ..
    }: includes::Options<'_>,
) -> Result<Option<PathBuf>, Error> {
    let path = match check_interpolation_result(err_on_interpolation_failure, path.interpolate(context))? {
        Some(p) => p,
        None => return Ok(None),
    };
    let path: PathBuf = if path.is_relative() {
        if !err_on_missing_config_path && target_config_path.is_none() {
            return Ok(None);
        }
        target_config_path
            .ok_or(Error::MissingConfigPath)?
            .parent()
            .expect("path is a config file which naturally lives in a directory")
            .join(path)
    } else {
        path.into()
    };
    Ok(Some(path))
}

mod types;
pub use types::{conditional, Error, Options};