1use std::collections::HashMap;
11use std::ffi::OsString;
12use std::path::{Path, PathBuf};
13use std::str::FromStr;
14use std::sync::Arc;
15
16use time::{OffsetDateTime, UtcOffset};
17
18use crate::change_id::{ChangeIdGenerator, OsChangeIdGenerator};
19use crate::command_runner::{system_command_runner, CommandRunner};
20use crate::diagnostics::{DiagnosticsHandle, NullDiagnostics};
21use crate::git_date::tm::{local_tzoffset_with_tz, TzHhmm};
22use crate::ident_resolve::IdentityEnv;
23
24#[derive(Debug, Clone)]
26pub struct Environment {
27 pub cwd: PathBuf,
29 pub git_dir: Option<String>,
31 pub git_work_tree: Option<String>,
33 pub git_ceiling_directories: Option<String>,
35 pub git_index_file: Option<String>,
37 pub git_namespace: Option<String>,
39 pub git_replace_ref_base: Option<String>,
41 pub git_no_replace_objects: bool,
43 pub git_alternate_object_directories: Option<String>,
45 pub git_object_directory: Option<String>,
54 pub git_prefix: Option<String>,
56 pub git_config_nosystem: Option<String>,
58 pub git_config_system: Option<String>,
60 pub git_config_global: Option<String>,
62 pub git_config: Option<String>,
64 pub git_config_parameters: Option<String>,
66 pub git_config_count: Option<String>,
68 pub git_config_pairs: Vec<(String, String)>,
70 pub home: Option<OsString>,
72 pub xdg_config_home: Option<String>,
74 pub userprofile: Option<OsString>,
76 pub homedrive: Option<OsString>,
78 pub homepath: Option<OsString>,
80 pub git_install_root: Option<OsString>,
82 pub git_exec_path: Option<OsString>,
84 pub pwd: Option<String>,
86 pub git_test_assume_different_owner: Option<String>,
88 pub grit_debug_safe_dir: bool,
90 pub git_trace_setup: Option<String>,
92 pub git_trace2_perf: Option<String>,
94 pub grit_invocation_cwd: Option<String>,
96 pub sudo_uid: Option<String>,
98 pub git_test_utf8_nfd_to_nfc: Option<String>,
100 pub git_test_no_write_rev_index: Option<String>,
102 pub git_test_reftable_autocompaction: Option<String>,
104 pub git_author_name: Option<String>,
106 pub git_author_email: Option<String>,
108 pub git_author_date: Option<String>,
110 pub git_committer_name: Option<String>,
112 pub git_committer_email: Option<String>,
114 pub git_committer_date: Option<String>,
116 pub tz: Option<String>,
118 pub user: Option<String>,
120 pub username: Option<String>,
122 pub program_files: Option<String>,
124 pub program_files_x86: Option<String>,
126 pub(crate) captured_vars: HashMap<String, OsString>,
128}
129
130impl Environment {
131 #[must_use]
133 pub fn empty() -> Self {
134 Self {
135 cwd: PathBuf::from("."),
136 git_dir: None,
137 git_work_tree: None,
138 git_ceiling_directories: None,
139 git_index_file: None,
140 git_namespace: None,
141 git_replace_ref_base: None,
142 git_no_replace_objects: false,
143 git_alternate_object_directories: None,
144 git_object_directory: None,
145 git_prefix: None,
146 git_config_nosystem: None,
147 git_config_system: None,
148 git_config_global: None,
149 git_config: None,
150 git_config_parameters: None,
151 git_config_count: None,
152 git_config_pairs: Vec::new(),
153 home: None,
154 xdg_config_home: None,
155 userprofile: None,
156 homedrive: None,
157 homepath: None,
158 git_install_root: None,
159 git_exec_path: None,
160 pwd: None,
161 git_test_assume_different_owner: None,
162 grit_debug_safe_dir: false,
163 git_trace_setup: None,
164 git_trace2_perf: None,
165 grit_invocation_cwd: None,
166 sudo_uid: None,
167 git_test_utf8_nfd_to_nfc: None,
168 git_test_no_write_rev_index: None,
169 git_test_reftable_autocompaction: None,
170 git_author_name: None,
171 git_author_email: None,
172 git_author_date: None,
173 git_committer_name: None,
174 git_committer_email: None,
175 git_committer_date: None,
176 tz: None,
177 user: None,
178 username: None,
179 program_files: None,
180 program_files_x86: None,
181 captured_vars: HashMap::new(),
182 }
183 }
184
185 #[must_use]
192 pub fn is_fetch_options_default(&self) -> bool {
193 let e = Self::empty();
194 self.cwd == e.cwd
195 && self.git_dir == e.git_dir
196 && self.git_work_tree == e.git_work_tree
197 && self.git_ceiling_directories == e.git_ceiling_directories
198 && self.git_index_file == e.git_index_file
199 && self.git_namespace == e.git_namespace
200 && self.git_replace_ref_base == e.git_replace_ref_base
201 && self.git_no_replace_objects == e.git_no_replace_objects
202 && self.git_alternate_object_directories == e.git_alternate_object_directories
203 && self.git_object_directory == e.git_object_directory
204 && self.git_prefix == e.git_prefix
205 && self.git_config_nosystem == e.git_config_nosystem
206 && self.git_config_system == e.git_config_system
207 && self.git_config_global == e.git_config_global
208 && self.git_config == e.git_config
209 && self.git_config_parameters == e.git_config_parameters
210 && self.git_config_count == e.git_config_count
211 && self.git_config_pairs == e.git_config_pairs
212 && self.home == e.home
213 && self.xdg_config_home == e.xdg_config_home
214 && self.userprofile == e.userprofile
215 && self.homedrive == e.homedrive
216 && self.homepath == e.homepath
217 && self.git_install_root == e.git_install_root
218 && self.git_exec_path == e.git_exec_path
219 && self.pwd == e.pwd
220 && self.git_test_assume_different_owner == e.git_test_assume_different_owner
221 && self.grit_debug_safe_dir == e.grit_debug_safe_dir
222 && self.git_trace_setup == e.git_trace_setup
223 && self.git_trace2_perf == e.git_trace2_perf
224 && self.grit_invocation_cwd == e.grit_invocation_cwd
225 && self.sudo_uid == e.sudo_uid
226 && self.git_test_utf8_nfd_to_nfc == e.git_test_utf8_nfd_to_nfc
227 && self.git_test_no_write_rev_index == e.git_test_no_write_rev_index
228 && self.git_test_reftable_autocompaction == e.git_test_reftable_autocompaction
229 && self.git_author_name == e.git_author_name
230 && self.git_author_email == e.git_author_email
231 && self.git_author_date == e.git_author_date
232 && self.git_committer_name == e.git_committer_name
233 && self.git_committer_email == e.git_committer_email
234 && self.git_committer_date == e.git_committer_date
235 && self.tz == e.tz
236 && self.user == e.user
237 && self.username == e.username
238 && self.program_files == e.program_files
239 && self.program_files_x86 == e.program_files_x86
240 && self.captured_vars == e.captured_vars
241 }
242
243 pub fn from_vars(vars: impl IntoIterator<Item = (OsString, OsString)>, cwd: PathBuf) -> Self {
248 let map: HashMap<String, OsString> = vars
249 .into_iter()
250 .map(|(k, v)| (k.to_string_lossy().into_owned(), v))
251 .collect();
252 let get = |key: &str| -> Option<String> {
253 map.get(key).and_then(|v| v.to_str().map(str::to_owned))
254 };
255 let get_os = |key: &str| -> Option<OsString> { map.get(key).cloned() };
256
257 let mut git_config_pairs = Vec::new();
258 if let Some(count_str) = get("GIT_CONFIG_COUNT") {
259 if let Ok(count) = count_str.parse::<usize>() {
260 for i in 0..count {
261 let key_var = format!("GIT_CONFIG_KEY_{i}");
262 let value_var = format!("GIT_CONFIG_VALUE_{i}");
263 if let (Some(k), Some(v)) = (get(&key_var), get(&value_var)) {
264 git_config_pairs.push((k, v));
265 }
266 }
267 }
268 }
269
270 Self {
271 cwd,
272 git_dir: get("GIT_DIR"),
273 git_work_tree: get("GIT_WORK_TREE"),
274 git_ceiling_directories: get("GIT_CEILING_DIRECTORIES"),
275 git_index_file: get("GIT_INDEX_FILE"),
276 git_namespace: get("GIT_NAMESPACE"),
277 git_replace_ref_base: get("GIT_REPLACE_REF_BASE"),
278 git_no_replace_objects: map.contains_key("GIT_NO_REPLACE_OBJECTS"),
279 git_alternate_object_directories: get("GIT_ALTERNATE_OBJECT_DIRECTORIES"),
280 git_object_directory: get("GIT_OBJECT_DIRECTORY"),
281 git_prefix: get("GIT_PREFIX"),
282 git_config_nosystem: get("GIT_CONFIG_NOSYSTEM"),
283 git_config_system: get("GIT_CONFIG_SYSTEM"),
284 git_config_global: get("GIT_CONFIG_GLOBAL"),
285 git_config: get("GIT_CONFIG"),
286 git_config_parameters: get("GIT_CONFIG_PARAMETERS"),
287 git_config_count: get("GIT_CONFIG_COUNT"),
288 git_config_pairs,
289 home: get_os("HOME"),
290 xdg_config_home: get("XDG_CONFIG_HOME"),
291 userprofile: get_os("USERPROFILE"),
292 homedrive: get_os("HOMEDRIVE"),
293 homepath: get_os("HOMEPATH"),
294 git_install_root: get_os("GIT_INSTALL_ROOT"),
295 git_exec_path: get_os("GIT_EXEC_PATH"),
296 pwd: get("PWD"),
297 git_test_assume_different_owner: get("GIT_TEST_ASSUME_DIFFERENT_OWNER"),
298 grit_debug_safe_dir: map.contains_key("GRIT_DEBUG_SAFE_DIR"),
299 git_trace_setup: get("GIT_TRACE_SETUP"),
300 git_trace2_perf: get("GIT_TRACE2_PERF"),
301 grit_invocation_cwd: get("GRIT_INVOCATION_CWD"),
302 sudo_uid: get("SUDO_UID"),
303 git_test_utf8_nfd_to_nfc: get("GIT_TEST_UTF8_NFD_TO_NFC"),
304 git_test_no_write_rev_index: get("GIT_TEST_NO_WRITE_REV_INDEX"),
305 git_test_reftable_autocompaction: get("GIT_TEST_REFTABLE_AUTOCOMPACTION"),
306 git_author_name: get("GIT_AUTHOR_NAME"),
307 git_author_email: get("GIT_AUTHOR_EMAIL"),
308 git_author_date: get("GIT_AUTHOR_DATE"),
309 git_committer_name: get("GIT_COMMITTER_NAME"),
310 git_committer_email: get("GIT_COMMITTER_EMAIL"),
311 git_committer_date: get("GIT_COMMITTER_DATE"),
312 tz: get("TZ"),
313 user: get("USER"),
314 username: get("USERNAME"),
315 program_files: get("ProgramFiles"),
316 program_files_x86: get("ProgramFiles(x86)"),
317 captured_vars: map,
318 }
319 }
320
321 #[must_use]
323 pub fn git_now_date_override(&self) -> Option<i64> {
324 let raw = self
325 .git_committer_date
326 .as_deref()
327 .or(self.git_author_date.as_deref())?;
328 raw.split_whitespace()
329 .next()
330 .and_then(|p| p.parse::<i64>().ok())
331 }
332
333 #[must_use]
338 pub fn reftable_autocompaction_enabled(&self) -> bool {
339 self.git_test_reftable_autocompaction
340 .as_deref()
341 .map(|value| value != "false")
342 .unwrap_or(true)
343 }
344
345 #[must_use]
347 pub fn test_assume_different_owner(&self) -> bool {
348 self.git_test_assume_different_owner
349 .as_deref()
350 .map(|v| {
351 let lower = v.to_ascii_lowercase();
352 v == "1" || lower == "true" || lower == "yes" || lower == "on"
353 })
354 .unwrap_or(false)
355 }
356
357 #[must_use]
370 pub fn with_git_dir(mut self, path: impl AsRef<Path>) -> Self {
371 self.git_dir = Some(self.absolute_from_cwd(path.as_ref()));
372 self
373 }
374
375 #[must_use]
387 pub fn with_work_tree(mut self, path: impl AsRef<Path>) -> Self {
388 self.git_work_tree = Some(self.absolute_from_cwd(path.as_ref()));
389 self
390 }
391
392 pub fn push_config_override(&mut self, config_override: &ConfigOverride) {
405 use crate::transport::sq_quote_shell_arg;
406
407 let mut entry = sq_quote_shell_arg(&config_override.key);
408 entry.push('=');
409 if let Some(value) = &config_override.value {
410 entry.push_str(&sq_quote_shell_arg(value));
411 }
412 match &mut self.git_config_parameters {
413 Some(existing) if !existing.trim().is_empty() => {
414 existing.push(' ');
415 existing.push_str(&entry);
416 }
417 slot => *slot = Some(entry),
418 }
419 }
420
421 fn absolute_from_cwd(&self, path: &Path) -> String {
422 if path.is_absolute() {
423 path.to_string_lossy().into_owned()
424 } else {
425 self.cwd.join(path).to_string_lossy().into_owned()
426 }
427 }
428
429 #[must_use]
431 pub fn capture_process() -> Self {
432 Self::from_vars(std::env::vars_os(), process_cwd_fallback())
434 }
435
436 #[must_use]
438 pub fn config_fingerprint(&self) -> Option<Vec<(String, Option<String>)>> {
439 const VARS: [&str; 8] = [
440 "GIT_CONFIG_NOSYSTEM",
441 "GIT_CONFIG_SYSTEM",
442 "GIT_CONFIG_GLOBAL",
443 "XDG_CONFIG_HOME",
444 "HOME",
445 "GIT_CONFIG",
446 "GIT_CONFIG_PARAMETERS",
447 "GIT_CONFIG_COUNT",
448 ];
449 let mut fp: Vec<(String, Option<String>)> = VARS
450 .iter()
451 .map(|name| {
452 let val = match *name {
453 "GIT_CONFIG_NOSYSTEM" => self.git_config_nosystem.clone(),
454 "GIT_CONFIG_SYSTEM" => self.git_config_system.clone(),
455 "GIT_CONFIG_GLOBAL" => self.git_config_global.clone(),
456 "XDG_CONFIG_HOME" => self.xdg_config_home.clone(),
457 "HOME" => self
458 .home
459 .as_ref()
460 .and_then(|h| h.to_str().map(str::to_owned)),
461 "GIT_CONFIG" => self.git_config.clone(),
462 "GIT_CONFIG_PARAMETERS" => self.git_config_parameters.clone(),
463 "GIT_CONFIG_COUNT" => self.git_config_count.clone(),
464 _ => None,
465 };
466 ((*name).to_owned(), val)
467 })
468 .collect();
469 if let Some(count_str) = &self.git_config_count {
470 const MAX_TRACKED: usize = 256;
471 match count_str.parse::<usize>() {
472 Ok(n) if n <= MAX_TRACKED => {
473 for i in 0..n {
474 fp.push((
475 format!("GIT_CONFIG_KEY_{i}"),
476 self.git_config_pairs.get(i).map(|p| p.0.clone()),
477 ));
478 fp.push((
479 format!("GIT_CONFIG_VALUE_{i}"),
480 self.git_config_pairs.get(i).map(|p| p.1.clone()),
481 ));
482 }
483 }
484 Ok(_) => return None,
485 Err(_) => {}
486 }
487 }
488 fp.push((
489 "GRIT_ENV_CWD".to_owned(),
490 self.cwd.to_str().map(str::to_owned),
491 ));
492 fp.push(("GRIT_ENV_PWD".to_owned(), self.pwd.clone()));
493 Some(fp)
494 }
495
496 #[must_use]
498 pub fn var(&self, key: &str) -> Option<String> {
499 match key {
500 "GIT_DIR" => self.git_dir.clone(),
501 "GIT_WORK_TREE" => self.git_work_tree.clone(),
502 "GIT_CEILING_DIRECTORIES" => self.git_ceiling_directories.clone(),
503 "GIT_INDEX_FILE" => self.git_index_file.clone(),
504 "GIT_NAMESPACE" => self.git_namespace.clone(),
505 "GIT_REPLACE_REF_BASE" => self.git_replace_ref_base.clone(),
506 "GIT_ALTERNATE_OBJECT_DIRECTORIES" => self.git_alternate_object_directories.clone(),
507 "GIT_OBJECT_DIRECTORY" => self.git_object_directory.clone(),
508 "GIT_CONFIG_NOSYSTEM" => self.git_config_nosystem.clone(),
509 "GIT_CONFIG_SYSTEM" => self.git_config_system.clone(),
510 "GIT_CONFIG_GLOBAL" => self.git_config_global.clone(),
511 "GIT_CONFIG" => self.git_config.clone(),
512 "GIT_CONFIG_PARAMETERS" => self.git_config_parameters.clone(),
513 "GIT_CONFIG_COUNT" => self.git_config_count.clone(),
514 "GIT_PREFIX" => self.git_prefix.clone(),
515 "GIT_TEST_UTF8_NFD_TO_NFC" => self.git_test_utf8_nfd_to_nfc.clone(),
516 "GIT_TEST_REFTABLE_AUTOCOMPACTION" => self.git_test_reftable_autocompaction.clone(),
517 "GIT_AUTHOR_NAME" => self.git_author_name.clone(),
518 "GIT_AUTHOR_EMAIL" => self.git_author_email.clone(),
519 "GIT_AUTHOR_DATE" => self.git_author_date.clone(),
520 "GIT_COMMITTER_NAME" => self.git_committer_name.clone(),
521 "GIT_COMMITTER_EMAIL" => self.git_committer_email.clone(),
522 "GIT_COMMITTER_DATE" => self.git_committer_date.clone(),
523 "TZ" => self.tz.clone(),
524 "USER" => self.user.clone(),
525 "USERNAME" => self.username.clone(),
526 "XDG_CONFIG_HOME" => self.xdg_config_home.clone(),
527 "HOME" => self
528 .home
529 .as_ref()
530 .and_then(|h| h.to_str().map(str::to_owned)),
531 "USERPROFILE" => self
532 .userprofile
533 .as_ref()
534 .and_then(|h| h.to_str().map(str::to_owned)),
535 "HOMEDRIVE" => self
536 .homedrive
537 .as_ref()
538 .and_then(|h| h.to_str().map(str::to_owned)),
539 "HOMEPATH" => self
540 .homepath
541 .as_ref()
542 .and_then(|h| h.to_str().map(str::to_owned)),
543 key if key.starts_with("GIT_CONFIG_KEY_") => {
544 let idx: usize = key["GIT_CONFIG_KEY_".len()..].parse().ok()?;
545 self.git_config_pairs.get(idx).map(|p| p.0.clone())
546 }
547 key if key.starts_with("GIT_CONFIG_VALUE_") => {
548 let idx: usize = key["GIT_CONFIG_VALUE_".len()..].parse().ok()?;
549 self.git_config_pairs.get(idx).map(|p| p.1.clone())
550 }
551 _ => self
552 .captured_vars
553 .get(key)
554 .and_then(|v| v.to_str().map(str::to_owned)),
555 }
556 }
557
558 #[must_use]
559 pub fn var_os(&self, key: &str) -> Option<OsString> {
561 if key == "HOME" {
562 return self.home.clone();
563 }
564 if key == "GIT_NO_REPLACE_OBJECTS" && self.git_no_replace_objects {
565 return Some(OsString::new());
566 }
567 if key == "GIT_INSTALL_ROOT" {
568 return self.git_install_root.clone();
569 }
570 if key == "GIT_EXEC_PATH" {
571 return self.git_exec_path.clone();
572 }
573 if let Some(v) = self.var(key) {
574 return Some(OsString::from(v));
575 }
576 self.captured_vars.get(key).cloned()
577 }
578
579 #[must_use]
581 pub fn discovery_cwd(&self) -> PathBuf {
582 self.cwd.canonicalize().unwrap_or_else(|_| self.cwd.clone())
583 }
584
585 #[must_use]
592 pub fn subprocess_environment(&self) -> Vec<(OsString, OsString)> {
593 let mut passthrough: Vec<(&String, &OsString)> = self
594 .captured_vars
595 .iter()
596 .filter(|(k, _)| !is_typed_environment_var(k))
597 .collect();
598 passthrough.sort_unstable_by(|a, b| a.0.cmp(b.0));
599 let mut set: Vec<(OsString, OsString)> = passthrough
600 .into_iter()
601 .map(|(k, v)| (OsString::from(k), v.clone()))
602 .collect();
603 subprocess_push_str(&mut set, "GIT_DIR", self.git_dir.as_deref());
604 subprocess_push_str(&mut set, "GIT_WORK_TREE", self.git_work_tree.as_deref());
605 subprocess_push_str(
606 &mut set,
607 "GIT_CEILING_DIRECTORIES",
608 self.git_ceiling_directories.as_deref(),
609 );
610 subprocess_push_str(&mut set, "GIT_INDEX_FILE", self.git_index_file.as_deref());
611 subprocess_push_str(&mut set, "GIT_NAMESPACE", self.git_namespace.as_deref());
612 subprocess_push_str(
613 &mut set,
614 "GIT_REPLACE_REF_BASE",
615 self.git_replace_ref_base.as_deref(),
616 );
617 subprocess_push_str(
618 &mut set,
619 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
620 self.git_alternate_object_directories.as_deref(),
621 );
622 subprocess_push_str(
623 &mut set,
624 "GIT_OBJECT_DIRECTORY",
625 self.git_object_directory.as_deref(),
626 );
627 subprocess_push_str(&mut set, "GIT_PREFIX", self.git_prefix.as_deref());
628 subprocess_push_str(
629 &mut set,
630 "GIT_CONFIG_NOSYSTEM",
631 self.git_config_nosystem.as_deref(),
632 );
633 subprocess_push_str(
634 &mut set,
635 "GIT_CONFIG_SYSTEM",
636 self.git_config_system.as_deref(),
637 );
638 subprocess_push_str(
639 &mut set,
640 "GIT_CONFIG_GLOBAL",
641 self.git_config_global.as_deref(),
642 );
643 subprocess_push_str(&mut set, "GIT_CONFIG", self.git_config.as_deref());
644 subprocess_push_str(
645 &mut set,
646 "GIT_CONFIG_PARAMETERS",
647 self.git_config_parameters.as_deref(),
648 );
649 subprocess_push_str(
650 &mut set,
651 "GIT_CONFIG_COUNT",
652 self.git_config_count.as_deref(),
653 );
654 for (i, (k, v)) in self.git_config_pairs.iter().enumerate() {
655 set.push((
656 OsString::from(format!("GIT_CONFIG_KEY_{i}")),
657 OsString::from(k),
658 ));
659 set.push((
660 OsString::from(format!("GIT_CONFIG_VALUE_{i}")),
661 OsString::from(v),
662 ));
663 }
664 subprocess_push_os(&mut set, "HOME", self.home.as_ref());
665 subprocess_push_str(&mut set, "XDG_CONFIG_HOME", self.xdg_config_home.as_deref());
666 subprocess_push_os(&mut set, "USERPROFILE", self.userprofile.as_ref());
667 subprocess_push_os(&mut set, "HOMEDRIVE", self.homedrive.as_ref());
668 subprocess_push_os(&mut set, "HOMEPATH", self.homepath.as_ref());
669 subprocess_push_os(&mut set, "GIT_INSTALL_ROOT", self.git_install_root.as_ref());
670 subprocess_push_os(&mut set, "GIT_EXEC_PATH", self.git_exec_path.as_ref());
671 subprocess_push_str(&mut set, "PWD", self.pwd.as_deref());
672 subprocess_push_str(
673 &mut set,
674 "GIT_TEST_ASSUME_DIFFERENT_OWNER",
675 self.git_test_assume_different_owner.as_deref(),
676 );
677 subprocess_push_str(&mut set, "GIT_TRACE_SETUP", self.git_trace_setup.as_deref());
678 subprocess_push_str(&mut set, "GIT_TRACE2_PERF", self.git_trace2_perf.as_deref());
679 subprocess_push_str(
680 &mut set,
681 "GRIT_INVOCATION_CWD",
682 self.grit_invocation_cwd.as_deref(),
683 );
684 subprocess_push_str(&mut set, "SUDO_UID", self.sudo_uid.as_deref());
685 subprocess_push_str(
686 &mut set,
687 "GIT_TEST_UTF8_NFD_TO_NFC",
688 self.git_test_utf8_nfd_to_nfc.as_deref(),
689 );
690 subprocess_push_str(
691 &mut set,
692 "GIT_TEST_NO_WRITE_REV_INDEX",
693 self.git_test_no_write_rev_index.as_deref(),
694 );
695 subprocess_push_str(
696 &mut set,
697 "GIT_TEST_REFTABLE_AUTOCOMPACTION",
698 self.git_test_reftable_autocompaction.as_deref(),
699 );
700 subprocess_push_str(&mut set, "GIT_AUTHOR_NAME", self.git_author_name.as_deref());
701 subprocess_push_str(
702 &mut set,
703 "GIT_AUTHOR_EMAIL",
704 self.git_author_email.as_deref(),
705 );
706 subprocess_push_str(&mut set, "GIT_AUTHOR_DATE", self.git_author_date.as_deref());
707 subprocess_push_str(
708 &mut set,
709 "GIT_COMMITTER_NAME",
710 self.git_committer_name.as_deref(),
711 );
712 subprocess_push_str(
713 &mut set,
714 "GIT_COMMITTER_EMAIL",
715 self.git_committer_email.as_deref(),
716 );
717 subprocess_push_str(
718 &mut set,
719 "GIT_COMMITTER_DATE",
720 self.git_committer_date.as_deref(),
721 );
722 subprocess_push_str(&mut set, "TZ", self.tz.as_deref());
723 subprocess_push_str(&mut set, "USER", self.user.as_deref());
724 subprocess_push_str(&mut set, "USERNAME", self.username.as_deref());
725 subprocess_push_str(&mut set, "ProgramFiles", self.program_files.as_deref());
726 subprocess_push_str(
727 &mut set,
728 "ProgramFiles(x86)",
729 self.program_files_x86.as_deref(),
730 );
731 if self.git_no_replace_objects {
732 set.push((OsString::from("GIT_NO_REPLACE_OBJECTS"), OsString::new()));
733 }
734 if self.grit_debug_safe_dir {
735 set.push((OsString::from("GRIT_DEBUG_SAFE_DIR"), OsString::new()));
736 }
737 set
738 }
739
740 #[must_use]
742 pub fn home_dir(&self) -> Option<PathBuf> {
743 if let Some(home) = self.home.as_ref().filter(|h| !h.is_empty()) {
744 return Some(PathBuf::from(home));
745 }
746 #[cfg(windows)]
747 {
748 if let (Some(drive), Some(path)) = (
749 self.homedrive.as_ref().filter(|d| !d.is_empty()),
750 self.homepath.as_ref().filter(|p| !p.is_empty()),
751 ) {
752 let mut combined = drive.clone();
753 combined.push(path);
754 return Some(PathBuf::from(combined));
755 }
756 if let Some(profile) = self.userprofile.as_ref().filter(|p| !p.is_empty()) {
757 return Some(PathBuf::from(profile));
758 }
759 }
760 None
761 }
762
763 #[must_use]
768 pub fn default_xdg_config_git_file(&self, name: &str) -> Option<PathBuf> {
769 if let Some(xdg) = self.xdg_config_home.as_deref().filter(|s| !s.is_empty()) {
770 return Some(PathBuf::from(xdg).join("git").join(name));
771 }
772 self.home_dir()
773 .map(|home| home.join(".config").join("git").join(name))
774 }
775}
776
777#[derive(Debug, Clone, PartialEq, Eq)]
784pub struct ConfigOverride {
785 key: String,
786 value: Option<String>,
787}
788
789impl ConfigOverride {
790 pub fn new(key: impl Into<String>, value: Option<String>) -> crate::error::Result<Self> {
801 let key = key.into();
802 crate::config::canonical_key(&key)?;
803 Ok(Self { key, value })
804 }
805
806 #[must_use]
808 pub fn key(&self) -> &str {
809 &self.key
810 }
811
812 #[must_use]
814 pub fn value(&self) -> Option<&str> {
815 self.value.as_deref()
816 }
817}
818
819impl FromStr for ConfigOverride {
820 type Err = crate::error::Error;
821
822 fn from_str(spec: &str) -> Result<Self, Self::Err> {
824 match spec.split_once('=') {
825 Some((key, value)) => Self::new(key, Some(value.to_owned())),
826 None => Self::new(spec, None),
827 }
828 }
829}
830
831impl IdentityEnv for Environment {
832 fn var(&self, key: &str) -> Option<String> {
833 Environment::var(self, key)
834 }
835
836 fn var_os(&self, key: &str) -> Option<OsString> {
837 Environment::var_os(self, key)
838 }
839}
840
841#[must_use]
846pub fn wall_clock_unix_timestamp_nanos() -> u64 {
847 u64::try_from(OffsetDateTime::now_utc().unix_timestamp_nanos()).unwrap_or(u64::MAX)
848}
849
850#[must_use]
852pub(crate) fn wall_clock_unix_timestamp_seconds() -> i64 {
853 OffsetDateTime::now_utc().unix_timestamp()
854}
855
856#[must_use]
858pub fn offset_from_epoch_and_tz(epoch: i64, tz: Option<&str>) -> OffsetDateTime {
859 let tz_hhmm = local_tzoffset_with_tz(epoch as u64, tz);
860 let offset = utc_offset_from_tz_hhmm(tz_hhmm).unwrap_or(UtcOffset::UTC);
861 OffsetDateTime::from_unix_timestamp(epoch)
862 .unwrap_or(OffsetDateTime::UNIX_EPOCH)
863 .to_offset(offset)
864}
865
866fn utc_offset_from_tz_hhmm(tz: TzHhmm) -> Option<UtcOffset> {
867 let sign: i32 = if tz < 0 { -1 } else { 1 };
868 let abs = tz.unsigned_abs();
869 let hours = (abs / 100) as i32;
870 let minutes = (abs % 100) as i32;
871 let seconds = sign * (hours * 3600 + minutes * 60);
872 UtcOffset::from_whole_seconds(seconds).ok()
873}
874
875const TYPED_ENVIRONMENT_VARS: &[&str] = &[
877 "GIT_DIR",
878 "GIT_WORK_TREE",
879 "GIT_CEILING_DIRECTORIES",
880 "GIT_INDEX_FILE",
881 "GIT_NAMESPACE",
882 "GIT_REPLACE_REF_BASE",
883 "GIT_NO_REPLACE_OBJECTS",
884 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
885 "GIT_OBJECT_DIRECTORY",
886 "GIT_PREFIX",
887 "GIT_CONFIG_NOSYSTEM",
888 "GIT_CONFIG_SYSTEM",
889 "GIT_CONFIG_GLOBAL",
890 "GIT_CONFIG",
891 "GIT_CONFIG_PARAMETERS",
892 "GIT_CONFIG_COUNT",
893 "HOME",
894 "XDG_CONFIG_HOME",
895 "USERPROFILE",
896 "HOMEDRIVE",
897 "HOMEPATH",
898 "GIT_INSTALL_ROOT",
899 "GIT_EXEC_PATH",
900 "PWD",
901 "GIT_TEST_ASSUME_DIFFERENT_OWNER",
902 "GRIT_DEBUG_SAFE_DIR",
903 "GIT_TRACE_SETUP",
904 "GIT_TRACE2_PERF",
905 "GRIT_INVOCATION_CWD",
906 "SUDO_UID",
907 "GIT_TEST_UTF8_NFD_TO_NFC",
908 "GIT_TEST_NO_WRITE_REV_INDEX",
909 "GIT_TEST_REFTABLE_AUTOCOMPACTION",
910 "GIT_AUTHOR_NAME",
911 "GIT_AUTHOR_EMAIL",
912 "GIT_AUTHOR_DATE",
913 "GIT_COMMITTER_NAME",
914 "GIT_COMMITTER_EMAIL",
915 "GIT_COMMITTER_DATE",
916 "TZ",
917 "USER",
918 "USERNAME",
919 "ProgramFiles",
920 "ProgramFiles(x86)",
921];
922
923fn is_typed_environment_var(key: &str) -> bool {
924 TYPED_ENVIRONMENT_VARS.contains(&key)
925 || key.starts_with("GIT_CONFIG_KEY_")
926 || key.starts_with("GIT_CONFIG_VALUE_")
927}
928
929fn subprocess_push_str(out: &mut Vec<(OsString, OsString)>, key: &str, val: Option<&str>) {
930 if let Some(v) = val {
931 out.push((OsString::from(key), OsString::from(v)));
932 }
933}
934
935fn subprocess_push_os(out: &mut Vec<(OsString, OsString)>, key: &str, val: Option<&OsString>) {
936 if let Some(v) = val {
937 out.push((OsString::from(key), v.clone()));
938 }
939}
940
941#[derive(Clone)]
943#[non_exhaustive]
944pub struct RepositoryOptions {
945 pub environment: Environment,
947 pub command_runner: Arc<dyn CommandRunner>,
949 pub change_id_generator: Arc<dyn ChangeIdGenerator>,
951 pub diagnostics: DiagnosticsHandle,
953 pub network_trace: bool,
955 pub test_assume_different_owner: bool,
957 pub reference_unix_time: Option<i64>,
959 pub thread_policy: crate::parallel::ThreadPolicy,
961 pub work_tree: Option<std::path::PathBuf>,
963 pub validate_repository_format: bool,
965 pub open_for_serving: bool,
967 pub promisor_fetcher: Option<std::sync::Arc<dyn crate::promisor_fetch::PromisorFetcher>>,
969 pub hook_stdio: crate::hooks::HookStdio,
971}
972
973impl std::fmt::Debug for RepositoryOptions {
974 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
975 f.debug_struct("RepositoryOptions")
976 .field("environment", &self.environment)
977 .field("command_runner", &"<CommandRunner>")
978 .field("network_trace", &self.network_trace)
979 .finish_non_exhaustive()
980 }
981}
982
983impl Default for RepositoryOptions {
984 fn default() -> Self {
985 Self {
986 environment: Environment::empty(),
987 command_runner: system_command_runner(),
988 change_id_generator: Arc::new(OsChangeIdGenerator),
989 diagnostics: Arc::new(NullDiagnostics),
990 network_trace: false,
991 test_assume_different_owner: false,
992 reference_unix_time: None,
993 thread_policy: crate::parallel::ThreadPolicy::default(),
994 work_tree: None,
995 validate_repository_format: true,
996 open_for_serving: false,
997 promisor_fetcher: None,
998 hook_stdio: crate::hooks::HookStdio::default(),
999 }
1000 }
1001}
1002
1003impl RepositoryOptions {
1004 #[must_use]
1006 pub fn empty() -> Self {
1007 Self::default()
1008 }
1009
1010 #[must_use]
1012 pub fn with_environment(environment: Environment) -> Self {
1013 Self {
1014 environment,
1015 ..Self::default()
1016 }
1017 }
1018
1019 #[must_use]
1021 pub fn with_command_runner(mut self, runner: Arc<dyn CommandRunner>) -> Self {
1022 self.command_runner = runner;
1023 self
1024 }
1025
1026 #[must_use]
1028 pub fn with_change_id_generator(mut self, generator: Arc<dyn ChangeIdGenerator>) -> Self {
1029 self.change_id_generator = generator;
1030 self
1031 }
1032
1033 #[must_use]
1035 pub fn with_work_tree(mut self, work_tree: impl Into<std::path::PathBuf>) -> Self {
1036 self.work_tree = Some(work_tree.into());
1037 self
1038 }
1039
1040 #[must_use]
1042 pub fn without_format_validation(mut self) -> Self {
1043 self.validate_repository_format = false;
1044 self
1045 }
1046
1047 #[must_use]
1049 pub fn for_serving(mut self) -> Self {
1050 self.open_for_serving = true;
1051 self
1052 }
1053
1054 #[must_use]
1056 pub fn with_diagnostics(mut self, diagnostics: DiagnosticsHandle) -> Self {
1057 self.diagnostics = diagnostics;
1058 self
1059 }
1060
1061 #[must_use]
1065 pub fn with_hook_stdio(mut self, hook_stdio: crate::hooks::HookStdio) -> Self {
1066 self.hook_stdio = hook_stdio;
1067 self
1068 }
1069
1070 #[must_use]
1072 pub fn with_network_trace(mut self, network_trace: bool) -> Self {
1073 self.network_trace = network_trace;
1074 self
1075 }
1076
1077 #[must_use]
1079 pub fn with_promisor_fetcher(
1080 mut self,
1081 fetcher: Option<std::sync::Arc<dyn crate::promisor_fetch::PromisorFetcher>>,
1082 ) -> Self {
1083 self.promisor_fetcher = fetcher;
1084 self
1085 }
1086}
1087
1088fn process_cwd_fallback() -> PathBuf {
1089 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
1091}
1092
1093#[cfg(test)]
1094mod tests {
1095 use super::*;
1096
1097 #[test]
1098 fn is_fetch_options_default_matches_empty() {
1099 assert!(Environment::empty().is_fetch_options_default());
1100 let mut env = Environment::from_vars(
1101 [(
1102 OsString::from("GIT_SSH_COMMAND"),
1103 OsString::from("/bin/wrap"),
1104 )],
1105 PathBuf::from("."),
1106 );
1107 assert!(!env.is_fetch_options_default());
1108 env.captured_vars.clear();
1109 assert!(env.is_fetch_options_default());
1110 }
1111
1112 #[test]
1113 fn var_reads_typed_object_directory_fields() {
1114 let vars = [
1115 ("GIT_OBJECT_DIRECTORY", "/captured/objects"),
1116 ("GIT_ALTERNATE_OBJECT_DIRECTORIES", "/captured/alt"),
1117 ]
1118 .into_iter()
1119 .map(|(k, v)| (OsString::from(k), OsString::from(v)));
1120 let mut env = Environment::from_vars(vars, PathBuf::from("/tmp"));
1121 assert_eq!(
1122 env.var("GIT_OBJECT_DIRECTORY").as_deref(),
1123 Some("/captured/objects")
1124 );
1125 env.git_object_directory = Some("/typed/objects".to_owned());
1126 env.git_alternate_object_directories = None;
1127 assert_eq!(
1128 env.var("GIT_OBJECT_DIRECTORY").as_deref(),
1129 Some("/typed/objects")
1130 );
1131 assert_eq!(env.var("GIT_ALTERNATE_OBJECT_DIRECTORIES"), None);
1132 }
1133
1134 #[test]
1135 fn subprocess_environment_passes_through_untyped_vars() {
1136 let vars = [
1137 ("PATH", "/opt/tools/bin:/usr/bin"),
1138 ("SystemRoot", r"C:\Windows"),
1139 ("GIT_AUTHOR_NAME", "Ada"),
1140 ("GIT_DIR", "/stale/.git"),
1141 ("GIT_CONFIG_COUNT", "1"),
1142 ("GIT_CONFIG_KEY_0", "user.name"),
1143 ("GIT_CONFIG_VALUE_0", "Ada"),
1144 ]
1145 .into_iter()
1146 .map(|(k, v)| (OsString::from(k), OsString::from(v)));
1147 let mut env = Environment::from_vars(vars, PathBuf::from("/tmp"));
1148 env.git_dir = None;
1149 env.git_config_pairs[0].1 = "Grace".to_owned();
1150
1151 let set = env.subprocess_environment();
1152 let get = |key: &str| -> Vec<&OsString> {
1153 set.iter()
1154 .filter(|(k, _)| k == key)
1155 .map(|(_, v)| v)
1156 .collect()
1157 };
1158 assert_eq!(get("PATH"), vec!["/opt/tools/bin:/usr/bin"]);
1159 assert_eq!(get("SystemRoot"), vec![r"C:\Windows"]);
1160 assert_eq!(get("GIT_AUTHOR_NAME"), vec!["Ada"]);
1161 assert!(get("GIT_DIR").is_empty(), "cleared typed field must win");
1162 assert_eq!(get("GIT_CONFIG_VALUE_0"), vec!["Grace"]);
1163 }
1164
1165 #[test]
1166 fn subprocess_environment_of_empty_snapshot_is_empty() {
1167 assert!(Environment::empty().subprocess_environment().is_empty());
1168 }
1169
1170 #[test]
1171 fn from_vars_parses_config_count_pairs() {
1172 let vars = [
1173 ("GIT_CONFIG_COUNT", "1"),
1174 ("GIT_CONFIG_KEY_0", "user.name"),
1175 ("GIT_CONFIG_VALUE_0", "Ada"),
1176 ]
1177 .into_iter()
1178 .map(|(k, v)| (OsString::from(k), OsString::from(v)));
1179 let env = Environment::from_vars(vars, PathBuf::from("/tmp"));
1180 assert_eq!(env.git_config_count.as_deref(), Some("1"));
1181 assert_eq!(
1182 env.git_config_pairs,
1183 vec![("user.name".to_owned(), "Ada".to_owned())]
1184 );
1185 }
1186
1187 #[test]
1188 fn config_fingerprint_tracks_global_override() {
1189 let mut env = Environment::empty();
1190 env.git_config_global = Some("/tmp/global.cfg".to_owned());
1191 let fp = env.config_fingerprint().expect("fingerprint");
1192 assert!(fp
1193 .iter()
1194 .any(|(k, v)| k == "GIT_CONFIG_GLOBAL" && v.as_deref() == Some("/tmp/global.cfg")));
1195 }
1196
1197 #[test]
1198 fn default_xdg_config_git_file_prefers_xdg() {
1199 let mut env = Environment::empty();
1200 env.home = Some(OsString::from("/home/u"));
1201 env.xdg_config_home = Some("/xdg".to_owned());
1202 assert_eq!(
1203 env.default_xdg_config_git_file("ignore"),
1204 Some(PathBuf::from("/xdg/git/ignore"))
1205 );
1206 }
1207
1208 fn isolated_env(dir: &Path) -> Environment {
1209 let mut env = Environment::empty();
1210 env.cwd = dir.to_path_buf();
1211 env.home = Some(dir.as_os_str().to_owned());
1212 env.git_config_nosystem = Some("1".to_owned());
1213 env.git_config_global = Some(dir.join("no-global").to_string_lossy().into_owned());
1214 env
1215 }
1216
1217 fn override_of(spec: &str) -> ConfigOverride {
1218 spec.parse().expect("valid override")
1219 }
1220
1221 #[test]
1222 fn config_override_parses_key_value_bare_and_empty() {
1223 let o = override_of("user.name=Ada Lovelace");
1224 assert_eq!((o.key(), o.value()), ("user.name", Some("Ada Lovelace")));
1225 let o = override_of("http.extraHeader=Authorization: a=b");
1226 assert_eq!(o.value(), Some("Authorization: a=b"));
1227 let o = override_of("core.bare");
1228 assert_eq!((o.key(), o.value()), ("core.bare", None));
1229 let o = override_of("user.email=");
1230 assert_eq!(o.value(), Some(""));
1231 let o = override_of("remote.My Origin.url=x");
1232 assert_eq!(o.key(), "remote.My Origin.url");
1233 }
1234
1235 #[test]
1236 fn config_override_rejects_invalid_keys() {
1237 for spec in [
1238 "nosection",
1239 "=value",
1240 "user.=x",
1241 ".name=x",
1242 "us er.name=x",
1243 "",
1244 ] {
1245 assert!(
1246 spec.parse::<ConfigOverride>().is_err(),
1247 "{spec:?} should be rejected"
1248 );
1249 }
1250 assert!(ConfigOverride::new("a\nb.c", None).is_err());
1251 }
1252
1253 #[test]
1254 fn push_config_override_round_trips_through_parameter_parser() {
1255 let mut env = Environment::empty();
1256 let tricky = "it's a \"quote\" = !bang spaced";
1257 env.push_config_override(
1258 &ConfigOverride::new("user.name", Some(tricky.to_owned())).unwrap(),
1259 );
1260 env.push_config_override(&override_of("core.bare"));
1261 env.push_config_override(&override_of("user.email="));
1262 let raw = env.git_config_parameters.clone().expect("parameters set");
1263 assert_eq!(
1264 crate::config::parse_config_parameters(&raw),
1265 vec![
1266 format!("user.name\u{1}{tricky}"),
1267 "core.bare\u{1}".to_owned(),
1268 "user.email\u{1}".to_owned(),
1269 ]
1270 );
1271 }
1272
1273 #[test]
1274 fn push_config_override_appends_to_inherited_parameters() {
1275 let mut env = Environment::empty();
1276 env.git_config_parameters = Some("'core.editor'='vi'".to_owned());
1277 env.push_config_override(&override_of("user.name=CI"));
1278 assert_eq!(
1279 env.git_config_parameters.as_deref(),
1280 Some("'core.editor'='vi' 'user.name'='CI'")
1281 );
1282 let mut blank = Environment::empty();
1283 blank.git_config_parameters = Some(" ".to_owned());
1284 blank.push_config_override(&override_of("user.name=CI"));
1285 assert_eq!(
1286 blank.git_config_parameters.as_deref(),
1287 Some("'user.name'='CI'")
1288 );
1289 }
1290
1291 #[test]
1292 fn later_config_override_wins_over_earlier_env_pairs_and_files() {
1293 let dir = tempfile::tempdir().unwrap();
1294 let git_dir = dir.path().join(".git");
1295 std::fs::create_dir_all(&git_dir).unwrap();
1296 std::fs::write(
1297 git_dir.join("config"),
1298 "[user]\n\tname = FromRepo\n\temail = repo@x.y\n[core]\n\tbare = false\n",
1299 )
1300 .unwrap();
1301 let mut env = isolated_env(dir.path());
1302 env.git_config_count = Some("1".to_owned());
1303 env.git_config_pairs = vec![("user.name".to_owned(), "FromCount".to_owned())];
1304 env.push_config_override(&override_of("user.name=First"));
1305 env.push_config_override(&override_of("user.name=Second"));
1306 env.push_config_override(&override_of("core.bare"));
1307
1308 let config = crate::config::ConfigSet::load(&env, Some(&git_dir), false).unwrap();
1309 assert_eq!(config.get("user.name").as_deref(), Some("Second"));
1310 assert_eq!(config.get("user.email").as_deref(), Some("repo@x.y"));
1311 assert_eq!(config.get_bool("core.bare"), Some(Ok(true)));
1312 }
1313
1314 #[test]
1315 fn config_override_is_exported_to_subprocesses() {
1316 let mut env = Environment::empty();
1317 env.push_config_override(&override_of("user.name=CI"));
1318 assert!(env.subprocess_environment().contains(&(
1319 OsString::from("GIT_CONFIG_PARAMETERS"),
1320 OsString::from("'user.name'='CI'")
1321 )));
1322 }
1323
1324 #[test]
1325 fn with_git_dir_and_work_tree_resolve_relative_paths_against_cwd() {
1326 let mut env = Environment::empty();
1327 env.cwd = PathBuf::from("/base/dir");
1328 let env = env.with_git_dir("repo/.git").with_work_tree("wt");
1329 assert_eq!(
1330 env.git_dir.as_deref().map(PathBuf::from),
1331 Some(PathBuf::from("/base/dir/repo/.git"))
1332 );
1333 assert_eq!(
1334 env.git_work_tree.as_deref().map(PathBuf::from),
1335 Some(PathBuf::from("/base/dir/wt"))
1336 );
1337 let abs = std::env::temp_dir().join("abs.git");
1338 let env = env.with_git_dir(&abs);
1339 assert_eq!(env.git_dir.as_deref().map(PathBuf::from), Some(abs));
1340 }
1341
1342 #[test]
1343 fn with_git_dir_opens_that_repository_from_elsewhere() {
1344 let dir = tempfile::tempdir().unwrap();
1345 let repo_dir = dir.path().join("r");
1346 crate::repo::init_repository(
1347 &repo_dir,
1348 false,
1349 "main",
1350 None,
1351 crate::ref_storage::RefStorageFormat::Files,
1352 )
1353 .unwrap();
1354 let elsewhere = dir.path().join("elsewhere");
1355 std::fs::create_dir_all(&elsewhere).unwrap();
1356 let mut env = isolated_env(&elsewhere);
1357 env.cwd = elsewhere.clone();
1358 let env = env.with_git_dir("../r/.git").with_work_tree("../r");
1359 let repo =
1360 crate::repo::Repository::discover(None, &RepositoryOptions::with_environment(env))
1361 .unwrap();
1362 assert_eq!(
1363 repo.git_dir().canonicalize().unwrap(),
1364 repo_dir.join(".git").canonicalize().unwrap()
1365 );
1366 assert_eq!(
1367 repo.work_tree().map(|p| p.canonicalize().unwrap()),
1368 Some(repo_dir.canonicalize().unwrap())
1369 );
1370 }
1371
1372 #[test]
1373 fn default_xdg_config_git_file_falls_back_to_home_config() {
1374 let mut env = Environment::empty();
1375 env.home = Some(OsString::from("/home/u"));
1376 assert_eq!(
1377 env.default_xdg_config_git_file("ignore"),
1378 Some(PathBuf::from("/home/u/.config/git/ignore"))
1379 );
1380 }
1381}