workerlib/
git_config.rs

1//!
2//! Configure the global settings of all git-binary based operations.
3//!
4
5use git2::{Config as GitConfig, Error as GitError};
6use std::fs::{metadata, write};
7use std::io::Error as IoError;
8use std::path::{Path, PathBuf};
9
10/// The default branch to use when initializing and creating commits, this
11/// currently only affects LFS backed raw-file repositories.
12pub const DEFAULT_GIT_BRANCH: &str = "main";
13
14/// Default path used to setup Lorry's git configuration file.
15/// TODO: Should support XDG style configuration and maybe others.
16pub const DEFAULT_GIT_CONFIG_PATH: &str = "lorry.gitconfig";
17
18/// Environment variable that specifies where the global git configuration is
19/// located. This needs to be configured each time we shell out to Git.
20pub const GIT_CONFIG_GLOBAL: &str = "GIT_CONFIG_GLOBAL";
21
22/// Default username used for basic authentication during LFS operations
23pub const GITLAB_OAUTH_USER: &str = "oauth2";
24
25/// An error that occurred while access the global git configuration.
26#[derive(thiserror::Error, Debug)]
27pub enum Error {
28    /// Could not serialize git configuration
29    #[error("Git Configuration Invalid: {0}")]
30    Git(#[from] GitError),
31
32    /// IO error
33    #[error("IO Failure: {0}")]
34    Io(#[from] IoError),
35}
36
37/// Ensure that the global git configuration is initialized and has the correct
38/// contents. Each time Lorry starts up this file will be setup with required
39/// values but additional options can be added as desired.
40pub struct Config(pub PathBuf);
41
42impl Config {
43    /// Setup the Lorry specific git configuration, admin_contact should be a
44    /// valid e-mail address and will be used in automated commits made by
45    /// Lorry.
46    #[tracing::instrument(skip_all, fields(git_cfg_path = self.0.to_str()))]
47    #[allow(clippy::too_many_arguments)]
48    pub fn setup(
49        &self,
50        git_identity: (&str, &str),
51        n_threads: i64,
52        no_ssl_verify: bool,
53        http_version: Option<&str>,
54        ask_pass_program: &Path,
55        git_credentials_file: Option<&Path>,
56        lfs_activity_timeout: Option<i64>,
57        lfs_dial_timeout: Option<i64>,
58        lfs_tls_timeout: Option<i64>,
59    ) -> Result<(), Error> {
60        if metadata(&self.0).is_err() {
61            write(&self.0, [])?;
62        }
63        let mut cfg = GitConfig::open(&self.0)?;
64        cfg.set_str("user.name", git_identity.0)?;
65        cfg.set_str("user.email", git_identity.1)?;
66
67        // Do not fork a background process for doing garbage collection
68        cfg.set_bool("gc.autodetach", false)?;
69
70        // Number of threads used when pushing to a remote
71        cfg.set_i64("pack.threads", n_threads)?;
72
73        // If ssl cerficates from http sources should be verified
74        cfg.set_bool("http.sslVerify", !no_ssl_verify)?;
75
76        // Specify the HTTP version
77        if let Some(version) = http_version {
78            cfg.set_str("http.version", version)?;
79        }
80
81        // Extra header exposed by Lorry
82        cfg.set_str("http.extraHeader", crate::LORRY_VERSION_HEADER)?;
83
84        // Default branch when initializing repositories
85        cfg.set_str("init.defaultBranch", DEFAULT_GIT_BRANCH)?;
86
87        // Global credential specifier used only for LFS operations with
88        // downstream gitlab, defaults to reading LORRY_GITLAB_PRIVATE_TOKEN
89        cfg.set_str("credential.username", GITLAB_OAUTH_USER)?;
90
91        // Program responsible for reading the downstream oauth password
92        cfg.set_str("core.askPass", &ask_pass_program.to_string_lossy())?;
93
94        // Workerlib access to git credentials
95        if let Some(gitcredentials_path) = git_credentials_file {
96            cfg.set_str(
97                "credential.helper",
98                &format!("store --file {}", gitcredentials_path.display()),
99            )?;
100        }
101
102        if let Some(activity_timeout) = lfs_activity_timeout {
103            cfg.set_i64("lfs.activityTimeout", activity_timeout)?;
104        }
105
106        if let Some(dial_timeout) = lfs_dial_timeout {
107            cfg.set_i64("lfs.dialTimeout", dial_timeout)?;
108        }
109
110        if let Some(tls_timeout) = lfs_tls_timeout {
111            cfg.set_i64("lfs.tlsTimeout", tls_timeout)?;
112        }
113
114        tracing::debug!("Initialized git config");
115        Ok(())
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::git_config::Config as GitConfig;
123    use tempfile::tempdir;
124
125    #[test]
126    pub fn test_rawfile_init() {
127        let test_dir = tempdir().unwrap();
128        let git_config_path = test_dir.path().join("gitconfig");
129        let git_config = GitConfig(git_config_path);
130        git_config
131            .setup(
132                ("Lorry", "hello@example.org"),
133                1,
134                false,
135                Some("HTTP/1.1"),
136                Path::new("/dev/null"),
137                None,
138                None,
139                None,
140                None,
141            )
142            .unwrap();
143    }
144
145    #[test]
146    pub fn test_lfs_timeouts_written() {
147        let test_dir = tempdir().unwrap();
148        let git_config_path = test_dir.path().join("gitconfig");
149        let git_config = GitConfig(git_config_path.clone());
150        git_config
151            .setup(
152                ("Lorry", "hello@example.org"),
153                1,
154                false,
155                Some("HTTP/1.1"),
156                Path::new("/dev/null"),
157                None,
158                Some(30),
159                Some(5),
160                None,
161            )
162            .unwrap();
163
164        let cfg = git2::Config::open(&git_config_path).unwrap();
165        assert_eq!(cfg.get_i64("lfs.activityTimeout").unwrap(), 30);
166        assert_eq!(cfg.get_i64("lfs.dialTimeout").unwrap(), 5);
167    }
168
169    #[test]
170    pub fn test_lfs_timeouts_unset_by_default() {
171        let test_dir = tempdir().unwrap();
172        let git_config_path = test_dir.path().join("gitconfig");
173        let git_config = GitConfig(git_config_path.clone());
174        git_config
175            .setup(
176                ("Lorry", "hello@example.org"),
177                1,
178                false,
179                Some("HTTP/1.1"),
180                Path::new("/dev/null"),
181                None,
182                None,
183                None,
184                None,
185            )
186            .unwrap();
187
188        let cfg = git2::Config::open(&git_config_path).unwrap();
189        // When not configured the keys should be absent so git-lfs falls back
190        // to its own defaults.
191        assert!(cfg.get_i64("lfs.activityTimeout").is_err());
192        assert!(cfg.get_i64("lfs.dialTimeout").is_err());
193    }
194}