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
327
328
use crate::{Result, bstr::ByteSlice, config};
use std::{collections::BTreeSet, ffi::OsString};
/// General Configuration
impl crate::Repository {
/// Return the compression level used when writing loose objects.
pub fn loose_compression(&self) -> gix_zlib::Compression {
self.config.loose_compression
}
/// Return the effective compression level used when writing pack entries.
pub fn pack_compression(&self) -> Result<gix_zlib::Compression> {
config::cache::access::pack_compression(
&self.config.resolved,
self.config.lenient_config,
self.filter_config_section(),
)
}
/// Return a snapshot of the configuration as seen upon opening the repository.
///
/// Use [`reload()`](Self::reload()) to refresh it from disk.
pub fn config_snapshot(&self) -> config::Snapshot<'_> {
config::Snapshot { repo: self }
}
/// Return the path at which a configuration file is expected, selected by its source.
///
/// [`Local`](config::Source::Local) selects `config` in the [`common_dir()`](Self::common_dir), and
/// [`Worktree`](config::Source::Worktree) selects `config.worktree` in the [`git_dir()`](Self::git_dir),
/// even if `extensions.worktreeConfig` is disabled. Other sources use the same path selection as [`crate::config_path()`]
/// with this repository's [`open_options()`](Self::open_options), including their permissions and explicit paths.
/// Relative paths are resolved against the current directory captured when this repository was opened.
/// Sources without a physical file or a permitted path return an error.
///
/// The file and its parent directories do not have to exist. No [configuration transaction](config::FileTransaction)
/// is opened, no lock is acquired, and no directories are created. Pass the path to
/// [`config_file_mut()`](Self::config_file_mut) to edit it.
pub fn config_path(&self, source: config::Source) -> Result<std::path::PathBuf> {
use config::Source;
use gix_error::{OptionExt, bail, message};
let path = match source {
Source::Local => self.common_dir().join("config"),
Source::Worktree => self.git_dir().join("config.worktree"),
Source::GitInstallation | Source::System | Source::Git | Source::User => {
let options = self.open_options();
config::cache::source_path(
source,
options.git_installation_config_path.as_deref(),
options.system_config_path.as_deref(),
options.permissions.config,
&mut config::Cache::make_source_env(options.permissions.env),
)
.ok_or_raise(|| message!("Configuration source {source:?} has no available path with these options"))?
}
_ => {
bail!("Configuration source {source:?} requires a repository or has no physical file");
}
};
Ok(self.current_dir().join(path))
}
/// Lock and open `path` as one physical configuration file without expanding its includes.
///
/// Relative paths are resolved against the current directory captured when this repository was opened. Dropping the
/// returned transaction releases the lock and discards its changes. Committing it only updates the file; call
/// [`reload()`](Self::reload()) explicitly to rebuild this repository from the changed configuration.
pub fn config_file_mut(&self, path: impl Into<std::path::PathBuf>) -> Result<config::FileTransaction> {
let path = path.into();
let path = if path.is_absolute() {
path
} else {
self.current_dir().join(path)
};
let lock_mode = self.config.config_lock_timeout()?;
let shared_repository_permissions =
config::file_mut::shared_repository_permissions(&self.config.resolved, self.filter_config_section())?;
config::FileTransaction::open(
path,
config::Source::Local,
self.git_dir_trust(),
lock_mode,
shared_repository_permissions,
)
}
/// Return the editor program selected by Git's precedence rules.
///
/// `GIT_EDITOR` takes precedence over `core.editor`. If the terminal isn't dumb, `VISUAL` is considered next,
/// followed by `EDITOR`. If none are set, a bundled `vi` (or its `vim` implementation) is returned when available
/// unless `TERM` is unset or `dumb`, in which case there is no usable editor.
/// Return an error if the selected editor cannot be represented as a native program name.
///
/// Use [`editor_command()`](Self::editor_command) to obtain a command prepared for execution.
pub fn editor(&self) -> Result<Option<OsString>> {
use crate::config::tree::{Core, Gitoxide};
let config = self.config_snapshot();
let terminal_is_dumb = config.string(Gitoxide::TERM).is_none_or(|terminal| terminal == "dumb");
let mut editor = config.trusted_program(Core::EDITOR)?;
if editor.is_none() && !terminal_is_dumb {
editor = config.trusted_program(Gitoxide::VISUAL)?;
}
if editor.is_none() {
editor = config.trusted_program(Gitoxide::EDITOR)?;
}
Ok(editor
.or_else(|| {
(!terminal_is_dumb).then(|| {
gix_path::env::installation_program("vi")
// Current Git for Windows versions provide `vi` as a shell script that delegates to `vim.exe`.
// Select the directly executable implementation when no `vi.exe` is installed.
.or_else(|| {
cfg!(windows)
.then(|| gix_path::env::installation_program("vim"))
.flatten()
})
.unwrap_or_else(|| "vi".into())
.into_os_string()
})
})
.filter(|editor| !editor.is_empty()))
}
/// Return the prepared [`editor`](Self::editor) command.
///
/// The returned command has repository context and inherited standard streams. Add the paths to edit as arguments
/// before spawning it.
#[cfg(feature = "command")]
pub fn editor_command(&self) -> Result<Option<gix_command::Prepare>> {
use std::{path::Path, process::Stdio};
let Some(editor) = self.editor()? else {
return Ok(None);
};
let mut command = gix_command::prepare(&editor);
if editor.to_string_lossy().trim_ascii() == ":" {
command = command.with_shell();
} else if !Path::new(&editor).is_file() {
command = command.command_may_be_shell_script();
}
Ok(Some(
command
.with_context(self.command_context()?)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit()),
))
}
/// Resolve all Git configuration needed to sign a commit with [`gix_object::Commit::sign()`].
///
/// The returned plumbing options may be adjusted before use, for example to disable GPG pinentry by adding
/// `--pinentry-mode=error` to `program_arguments`.
#[cfg(feature = "command")]
pub fn commit_signing_options(&self) -> Result<gix_object::signature::sign::Options> {
crate::commit::sign::signing_options(self)
}
/// Resolve all Git configuration needed to sign a commit if `commit.gpgSign` enables signing.
///
/// If signing is disabled, signer-specific configuration isn't resolved or validated.
#[cfg(feature = "command")]
pub fn commit_signing_options_if_enabled(&self) -> Result<Option<gix_object::signature::sign::Options>> {
crate::commit::sign::signing_options_if_enabled(self)
}
/// Return a mutable snapshot of the configuration as seen upon opening the repository, starting a transaction.
/// When the returned instance is dropped, it is applied in full, even if the reason for the drop is an error.
///
/// Note that changes to the configuration are in-memory only and are observed only this instance
/// of the [`Repository`](crate::Repository). Use [`reload()`](Self::reload()) to discard them and
/// refresh the snapshot from disk.
///
/// Values used to locate repository files are fixed when the repository is opened and aren't reapplied by
/// committing changes here. This includes `core.worktree` and `gitoxide.core.indexFile`; `GIT_DIR` likewise
/// cannot retarget an existing repository. Reload after changing persisted configuration or open another
/// repository to change these locations.
pub fn config_snapshot_mut(&mut self) -> config::SnapshotMut<'_> {
let config = self.config.resolved.as_ref().clone();
config::SnapshotMut {
repo: Some(self),
config,
}
}
/// Return filesystem options as retrieved from the repository configuration.
///
/// Note that these values have not been [probed](gix_fs::Capabilities::probe()).
pub fn filesystem_options(&self) -> Result<gix_fs::Capabilities> {
self.config.fs_capabilities()
}
/// Return filesystem options on how to perform stat-checks, typically in relation to the index.
///
/// Note that these values have not been [probed](gix_fs::Capabilities::probe()).
#[cfg(feature = "index")]
pub fn stat_options(&self) -> Result<gix_index::entry::stat::Options> {
self.config.stat_options()
}
/// The options used to open the repository.
pub fn open_options(&self) -> &crate::open::Options {
&self.options
}
/// Return the big-file threshold above which Git will not perform a diff anymore or try to delta-diff packs,
/// as configured by `core.bigFileThreshold`, or the default value.
pub fn big_file_threshold(&self) -> Result<u64> {
self.config.big_file_threshold()
}
/// Create a low-level parser for ignore patterns, for instance for use in [`excludes()`](crate::Repository::excludes()).
///
/// Depending on the configuration, precious-file parsing in `.gitignore-files` is supported.
/// This means that `$` prefixed files will be interpreted as precious, which is a backwards-incompatible change.
#[cfg(feature = "excludes")]
pub fn ignore_pattern_parser(&self) -> Result<gix_ignore::search::Ignore> {
self.config.ignore_pattern_parser()
}
/// Obtain options for use when connecting via `ssh`.
#[cfg(feature = "blocking-network-client")]
pub fn ssh_connect_options(&self) -> Result<gix_protocol::transport::client::blocking_io::ssh::connect::Options> {
use crate::config::{
cache::util::ApplyLeniency,
tree::{Core, Ssh, gitoxide},
};
let config = &self.config.resolved;
let mut trusted = self.filter_config_section();
let mut fallback_active = false;
let ssh_command = config
.string_filter(Core::SSH_COMMAND, &mut trusted)
.or_else(|| {
fallback_active = true;
config.string_filter(gitoxide::Ssh::COMMAND_WITHOUT_SHELL_FALLBACK, &mut trusted)
})
.map(|cmd| gix_path::from_bstr(cmd).map(|cmd| cmd.into_owned().into()))
.transpose()?;
let opts = gix_protocol::transport::client::blocking_io::ssh::connect::Options {
disallow_shell: fallback_active,
command: ssh_command,
kind: config
.string_filter("ssh.variant", &mut trusted)
.and_then(|variant| Ssh::VARIANT.try_into_variant(variant).transpose())
.transpose()
.with_leniency(self.options.lenient_config)?,
};
Ok(opts)
}
/// Return the context to be passed to any spawned program that is supposed to interact with the repository, like
/// hooks or filters.
#[cfg(feature = "command")]
pub fn command_context(&self) -> Result<gix_command::Context> {
use crate::config::{cache::util::ApplyLeniency, tree::gitoxide};
let pathspec_boolean = |key: &'static config::tree::keys::Boolean| {
key.enrich_error(self.config.resolved.boolean(key))
.with_leniency(self.config.lenient_config)
};
Ok(gix_command::Context {
stderr: {
gitoxide::Core::EXTERNAL_COMMAND_STDERR
.enrich_error(self.config.resolved.boolean(gitoxide::Core::EXTERNAL_COMMAND_STDERR))
.with_leniency(self.config.lenient_config)?
.unwrap_or(true)
.into()
},
git_dir: self.git_dir().to_owned().into(),
worktree_dir: self.workdir().map(ToOwned::to_owned),
no_replace_objects: config::shared::is_replace_refs_enabled(
&self.config.resolved,
self.config.lenient_config,
self.filter_config_section(),
)?
.map(|enabled| !enabled),
ref_namespace: self.refs.namespace.as_ref().map(|ns| ns.as_bstr().to_owned()),
literal_pathspecs: pathspec_boolean(&gitoxide::Pathspec::LITERAL)?,
glob_pathspecs: pathspec_boolean(&gitoxide::Pathspec::GLOB)?
.or(pathspec_boolean(&gitoxide::Pathspec::NOGLOB)?),
icase_pathspecs: pathspec_boolean(&gitoxide::Pathspec::ICASE)?,
})
}
/// The kind of object hash the repository is configured to use.
pub fn object_hash(&self) -> gix_hash::Kind {
self.config.object_hash
}
/// Return the algorithm to perform diffs or merges with.
///
/// In case of merges, a diff is performed under the hood in order to learn which hunks need merging.
#[cfg(feature = "blob-diff")]
pub fn diff_algorithm(&self) -> Result<gix_diff::blob::Algorithm> {
self.config.diff_algorithm()
}
}
mod branch;
mod remote;
#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
mod transport;
impl crate::Repository {
pub(crate) fn filter_config_section(&self) -> fn(&gix_config::file::Metadata) -> bool {
self.options
.filter_config_section
.unwrap_or(config::section::is_trusted)
}
fn subsection_str_names_of<'a>(&'a self, header_name: &'a str) -> BTreeSet<&'a str> {
self.config
.resolved
.sections_by_name(header_name)
.map(|it| {
let filter = self.filter_config_section();
it.filter(move |s| filter(s.meta()))
.filter_map(|section| section.header().subsection_name().and_then(|b| b.to_str().ok()))
.collect()
})
.unwrap_or_default()
}
}