|
1 | 1 | //! Utilities for mutating Git configuration entries by dotted key. |
2 | 2 |
|
3 | | -use std::path::{Path, PathBuf}; |
| 3 | +use std::path::PathBuf; |
4 | 4 |
|
5 | | -use anyhow::{Context as _, Result}; |
| 5 | +use anyhow::{Context as _, Result, bail}; |
6 | 6 | use bstr::ByteSlice as _; |
7 | 7 | use gix::config::AsKey as _; |
8 | 8 |
|
9 | | -/// Open the user-global Git config for editing, creating it first if needed. |
10 | | -pub fn open_user_global_config_for_editing() -> Result<(gix::config::File<'static>, PathBuf)> { |
11 | | - let path = gix::config::Source::User |
| 9 | +fn config_path(repo: Option<&gix::Repository>, source: gix::config::Source) -> Result<PathBuf> { |
| 10 | + let path = source |
12 | 11 | .storage_location(&mut |name| std::env::var_os(name)) |
13 | | - .context("failed to determine global git config location")? |
14 | | - .into_owned(); |
| 12 | + .with_context(|| format!("failed to determine {source:?} git config location"))?; |
| 13 | + let path = if path.is_relative() { |
| 14 | + let repo = repo.with_context(|| { |
| 15 | + format!("determining the {source:?} git config location requires a repository") |
| 16 | + })?; |
| 17 | + if source == gix::config::Source::Local { |
| 18 | + repo.common_dir().join(path.as_ref()) |
| 19 | + } else { |
| 20 | + repo.git_dir().join(path.as_ref()) |
| 21 | + } |
| 22 | + } else { |
| 23 | + path.into_owned() |
| 24 | + }; |
| 25 | + Ok(path) |
| 26 | +} |
| 27 | + |
| 28 | +/// Open the Git config for `source` for editing, creating it first if needed. |
| 29 | +/// `repo` is used to resolve repo-local paths, depending on `source`. |
| 30 | +/// Return `(config, lock)`. |
| 31 | +/// Write it back with [`write_locked_config()`]. |
| 32 | +pub fn open_config_for_editing( |
| 33 | + repo: Option<&gix::Repository>, |
| 34 | + source: gix::config::Source, |
| 35 | +) -> Result<(gix::config::File<'static>, gix::lock::File)> { |
| 36 | + let path = config_path(repo, source)?; |
| 37 | + std::fs::create_dir_all(path.parent().context("git config path has no parent")?)?; |
| 38 | + let lock = gix::lock::File::acquire_to_update_resource( |
| 39 | + &path, |
| 40 | + gix::lock::acquire::Fail::Immediately, |
| 41 | + None, |
| 42 | + )?; |
15 | 43 | if !path.exists() { |
16 | | - std::fs::create_dir_all( |
17 | | - path.parent() |
18 | | - .context("global git config path has no parent")?, |
19 | | - )?; |
20 | 44 | std::fs::File::create(&path)?; |
21 | 45 | } |
22 | | - let config = gix::config::File::from_path_no_includes(path.clone(), gix::config::Source::User) |
23 | | - .with_context(|| format!("failed to open global git config at {}", path.display()))?; |
24 | | - Ok((config, path)) |
| 46 | + let config = gix::config::File::from_path_no_includes(path.clone(), source) |
| 47 | + .with_context(|| format!("failed to open {source:?} git config at {}", path.display()))?; |
| 48 | + Ok((config, lock)) |
| 49 | +} |
| 50 | + |
| 51 | +/// Open the user-global Git config for reading without acquiring a write lock. |
| 52 | +/// |
| 53 | +/// If the config file doesn't exist yet, an empty in-memory config is returned. |
| 54 | +pub fn open_global_config_for_reading() -> Result<gix::config::File<'static>> { |
| 55 | + let path = config_path(None, gix::config::Source::User)?; |
| 56 | + if !path.exists() { |
| 57 | + return Ok(gix::config::File::new(gix::config::file::Metadata::from( |
| 58 | + gix::config::Source::User, |
| 59 | + ))); |
| 60 | + } |
| 61 | + gix::config::File::from_path_no_includes(path.clone(), gix::config::Source::User) |
| 62 | + .with_context(|| format!("failed to open User git config at {}", path.display())) |
25 | 63 | } |
26 | 64 |
|
27 | | -/// Serialize a Git `config` file back to disk at `path`. |
28 | | -pub fn write_config(path: &Path, config: &gix::config::File<'_>) -> Result<()> { |
29 | | - let mut file = std::io::BufWriter::new( |
30 | | - std::fs::OpenOptions::new() |
31 | | - .write(true) |
32 | | - .truncate(true) |
33 | | - .create(true) |
34 | | - .open(path) |
35 | | - .with_context(|| { |
36 | | - format!( |
37 | | - "failed to open git config for writing at {}", |
38 | | - path.display() |
39 | | - ) |
40 | | - })?, |
41 | | - ); |
| 65 | +/// Serialize a Git `config` file back to disk at `lock`. |
| 66 | +pub fn write_locked_config( |
| 67 | + config: &gix::config::File<'_>, |
| 68 | + mut lock: gix::lock::File, |
| 69 | +) -> Result<()> { |
| 70 | + let path = lock.resource_path(); |
42 | 71 | config |
43 | | - .write_to(&mut file) |
| 72 | + .write_to(&mut lock) |
44 | 73 | .with_context(|| format!("failed to serialize git config at {}", path.display()))?; |
45 | | - std::io::Write::flush(&mut file) |
| 74 | + std::io::Write::flush(&mut lock) |
46 | 75 | .with_context(|| format!("failed to flush git config at {}", path.display()))?; |
| 76 | + lock.commit() |
| 77 | + .map_err(|err| err.error) |
| 78 | + .with_context(|| format!("failed to commit git config at {}", path.display()))?; |
47 | 79 | Ok(()) |
48 | 80 | } |
49 | 81 |
|
| 82 | +/// Open the Git config for `source` using `repo` when needed, let `edit` mutate it, and |
| 83 | +/// write it back if the edited configuration differs from its original state. |
| 84 | +/// Return `true` if the file changed and was written, `false` otherwise. |
| 85 | +pub fn edit_config( |
| 86 | + repo: Option<&gix::Repository>, |
| 87 | + source: gix::config::Source, |
| 88 | + edit: impl FnOnce(&mut gix::config::File<'static>) -> Result<()>, |
| 89 | +) -> Result<bool> { |
| 90 | + let (mut config, lock) = open_config_for_editing(repo, source)?; |
| 91 | + let previous_contents = config.to_bstring(); |
| 92 | + edit(&mut config)?; |
| 93 | + let changed = config.to_bstring() != previous_contents; |
| 94 | + if changed { |
| 95 | + write_locked_config(&config, lock)?; |
| 96 | + } |
| 97 | + Ok(changed) |
| 98 | +} |
| 99 | + |
| 100 | +/// Open the Git config for `source` relative to `repo`, let `edit` mutate it, and write it back |
| 101 | +/// if the edited configuration differs from its original state. |
| 102 | +pub fn edit_repo_config( |
| 103 | + repo: &gix::Repository, |
| 104 | + source: gix::config::Source, |
| 105 | + edit: impl FnOnce(&mut gix::config::File<'static>) -> Result<()>, |
| 106 | +) -> Result<bool> { |
| 107 | + if matches!( |
| 108 | + source, |
| 109 | + gix::config::Source::System | gix::config::Source::GitInstallation |
| 110 | + ) { |
| 111 | + bail!("editing {source:?} config through a repository is not supported"); |
| 112 | + } |
| 113 | + edit_config(Some(repo), source, edit) |
| 114 | +} |
| 115 | + |
50 | 116 | /// Set the entry in `config` identified by the dotted `key` (like `section.value` or `section.subsection.value`) to `value`. |
51 | | -/// This will create sections as needed. |
| 117 | +/// This will create sections as needed, and remove all previous values under the same section with the same name. |
52 | 118 | pub fn set_config_value( |
53 | 119 | config: &mut gix::config::File<'static>, |
54 | 120 | key: &str, |
|
0 commit comments