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