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::config::parse_git_config_maybe_bool;
22use crate::diagnostics::{DiagnosticsHandle, NullDiagnostics};
23use crate::git_date::tm::{local_tzoffset_with_tz, TzHhmm};
24use crate::ident_resolve::IdentityEnv;
25
26#[derive(Debug, Clone)]
31pub struct Environment {
32 pub cwd: PathBuf,
34 pub git_dir: Option<String>,
36 pub git_work_tree: Option<String>,
38 pub git_ceiling_directories: Option<String>,
40 pub git_index_file: Option<String>,
42 pub git_namespace: Option<String>,
44 pub git_replace_ref_base: Option<String>,
46 pub git_no_replace_objects: bool,
48 pub git_optional_locks: bool,
51 pub git_alternate_object_directories: Option<String>,
53 pub git_object_directory: Option<String>,
62 pub git_prefix: Option<String>,
64 pub git_config_nosystem: Option<String>,
66 pub git_config_system: Option<String>,
68 pub git_config_global: Option<String>,
70 pub git_config: Option<String>,
72 pub git_config_parameters: Option<String>,
74 pub git_config_count: Option<String>,
76 pub git_config_pairs: Vec<(String, String)>,
78 pub home: Option<OsString>,
80 pub xdg_config_home: Option<String>,
82 pub userprofile: Option<OsString>,
84 pub homedrive: Option<OsString>,
86 pub homepath: Option<OsString>,
88 pub git_install_root: Option<OsString>,
90 pub git_exec_path: Option<OsString>,
92 pub pwd: Option<String>,
94 pub git_test_assume_different_owner: Option<String>,
96 pub grit_invocation_cwd: Option<String>,
98 pub sudo_uid: Option<String>,
100 pub git_test_utf8_nfd_to_nfc: Option<String>,
102 pub git_test_no_write_rev_index: Option<String>,
104 pub git_test_reftable_autocompaction: Option<String>,
106 pub git_author_name: Option<String>,
108 pub git_author_email: Option<String>,
110 pub git_author_date: Option<String>,
112 pub git_committer_name: Option<String>,
114 pub git_committer_email: Option<String>,
116 pub git_committer_date: Option<String>,
118 pub tz: Option<String>,
120 pub user: Option<String>,
122 pub username: Option<String>,
124 pub program_files: Option<String>,
126 pub program_files_x86: Option<String>,
128 pub(crate) captured_vars: Option<Arc<HashMap<String, OsString>>>,
132}
133
134impl Environment {
135 #[must_use]
143 pub fn empty() -> Self {
144 Self {
145 cwd: PathBuf::from("."),
146 git_dir: None,
147 git_work_tree: None,
148 git_ceiling_directories: None,
149 git_index_file: None,
150 git_namespace: None,
151 git_replace_ref_base: None,
152 git_no_replace_objects: false,
153 git_optional_locks: true,
154 git_alternate_object_directories: None,
155 git_object_directory: None,
156 git_prefix: None,
157 git_config_nosystem: None,
158 git_config_system: None,
159 git_config_global: None,
160 git_config: None,
161 git_config_parameters: None,
162 git_config_count: None,
163 git_config_pairs: Vec::new(),
164 home: None,
165 xdg_config_home: None,
166 userprofile: None,
167 homedrive: None,
168 homepath: None,
169 git_install_root: None,
170 git_exec_path: None,
171 pwd: None,
172 git_test_assume_different_owner: None,
173 grit_invocation_cwd: None,
174 sudo_uid: None,
175 git_test_utf8_nfd_to_nfc: None,
176 git_test_no_write_rev_index: None,
177 git_test_reftable_autocompaction: None,
178 git_author_name: None,
179 git_author_email: None,
180 git_author_date: None,
181 git_committer_name: None,
182 git_committer_email: None,
183 git_committer_date: None,
184 tz: None,
185 user: None,
186 username: None,
187 program_files: None,
188 program_files_x86: None,
189 captured_vars: None,
190 }
191 }
192
193 #[must_use]
200 pub fn is_fetch_options_default(&self) -> bool {
201 let e = Self::empty();
202 self.cwd == e.cwd
203 && self.git_dir == e.git_dir
204 && self.git_work_tree == e.git_work_tree
205 && self.git_ceiling_directories == e.git_ceiling_directories
206 && self.git_index_file == e.git_index_file
207 && self.git_namespace == e.git_namespace
208 && self.git_replace_ref_base == e.git_replace_ref_base
209 && self.git_no_replace_objects == e.git_no_replace_objects
210 && self.git_optional_locks == e.git_optional_locks
211 && self.git_alternate_object_directories == e.git_alternate_object_directories
212 && self.git_object_directory == e.git_object_directory
213 && self.git_prefix == e.git_prefix
214 && self.git_config_nosystem == e.git_config_nosystem
215 && self.git_config_system == e.git_config_system
216 && self.git_config_global == e.git_config_global
217 && self.git_config == e.git_config
218 && self.git_config_parameters == e.git_config_parameters
219 && self.git_config_count == e.git_config_count
220 && self.git_config_pairs == e.git_config_pairs
221 && self.home == e.home
222 && self.xdg_config_home == e.xdg_config_home
223 && self.userprofile == e.userprofile
224 && self.homedrive == e.homedrive
225 && self.homepath == e.homepath
226 && self.git_install_root == e.git_install_root
227 && self.git_exec_path == e.git_exec_path
228 && self.pwd == e.pwd
229 && self.git_test_assume_different_owner == e.git_test_assume_different_owner
230 && self.grit_invocation_cwd == e.grit_invocation_cwd
231 && self.sudo_uid == e.sudo_uid
232 && self.git_test_utf8_nfd_to_nfc == e.git_test_utf8_nfd_to_nfc
233 && self.git_test_no_write_rev_index == e.git_test_no_write_rev_index
234 && self.git_test_reftable_autocompaction == e.git_test_reftable_autocompaction
235 && self.git_author_name == e.git_author_name
236 && self.git_author_email == e.git_author_email
237 && self.git_author_date == e.git_author_date
238 && self.git_committer_name == e.git_committer_name
239 && self.git_committer_email == e.git_committer_email
240 && self.git_committer_date == e.git_committer_date
241 && self.tz == e.tz
242 && self.user == e.user
243 && self.username == e.username
244 && self.program_files == e.program_files
245 && self.program_files_x86 == e.program_files_x86
246 && self.captured_vars == e.captured_vars
247 }
248
249 pub fn from_vars(vars: impl IntoIterator<Item = (OsString, OsString)>, cwd: PathBuf) -> Self {
254 let map: HashMap<String, OsString> = vars
255 .into_iter()
256 .map(|(k, v)| (k.to_string_lossy().into_owned(), v))
257 .collect();
258 let get = |key: &str| -> Option<String> {
259 map.get(key).and_then(|v| v.to_str().map(str::to_owned))
260 };
261 let get_os = |key: &str| -> Option<OsString> { map.get(key).cloned() };
262
263 let mut git_config_pairs = Vec::new();
264 if let Some(count_str) = get("GIT_CONFIG_COUNT") {
265 if let Ok(count) = count_str.parse::<usize>() {
266 for i in 0..count {
267 let key_var = format!("GIT_CONFIG_KEY_{i}");
268 let value_var = format!("GIT_CONFIG_VALUE_{i}");
269 if let (Some(k), Some(v)) = (get(&key_var), get(&value_var)) {
270 git_config_pairs.push((k, v));
271 }
272 }
273 }
274 }
275
276 Self {
277 cwd,
278 git_dir: get("GIT_DIR"),
279 git_work_tree: get("GIT_WORK_TREE"),
280 git_ceiling_directories: get("GIT_CEILING_DIRECTORIES"),
281 git_index_file: get("GIT_INDEX_FILE"),
282 git_namespace: get("GIT_NAMESPACE"),
283 git_replace_ref_base: get("GIT_REPLACE_REF_BASE"),
284 git_no_replace_objects: map.contains_key("GIT_NO_REPLACE_OBJECTS"),
285 git_optional_locks: git_optional_locks_from_env(get("GIT_OPTIONAL_LOCKS").as_deref()),
286 git_alternate_object_directories: get("GIT_ALTERNATE_OBJECT_DIRECTORIES"),
287 git_object_directory: get("GIT_OBJECT_DIRECTORY"),
288 git_prefix: get("GIT_PREFIX"),
289 git_config_nosystem: get("GIT_CONFIG_NOSYSTEM"),
290 git_config_system: get("GIT_CONFIG_SYSTEM"),
291 git_config_global: get("GIT_CONFIG_GLOBAL"),
292 git_config: get("GIT_CONFIG"),
293 git_config_parameters: get("GIT_CONFIG_PARAMETERS"),
294 git_config_count: get("GIT_CONFIG_COUNT"),
295 git_config_pairs,
296 home: get_os("HOME"),
297 xdg_config_home: get("XDG_CONFIG_HOME"),
298 userprofile: get_os("USERPROFILE"),
299 homedrive: get_os("HOMEDRIVE"),
300 homepath: get_os("HOMEPATH"),
301 git_install_root: get_os("GIT_INSTALL_ROOT"),
302 git_exec_path: get_os("GIT_EXEC_PATH"),
303 pwd: get("PWD"),
304 git_test_assume_different_owner: get("GIT_TEST_ASSUME_DIFFERENT_OWNER"),
305 grit_invocation_cwd: get("GRIT_INVOCATION_CWD"),
306 sudo_uid: get("SUDO_UID"),
307 git_test_utf8_nfd_to_nfc: get("GIT_TEST_UTF8_NFD_TO_NFC"),
308 git_test_no_write_rev_index: get("GIT_TEST_NO_WRITE_REV_INDEX"),
309 git_test_reftable_autocompaction: get("GIT_TEST_REFTABLE_AUTOCOMPACTION"),
310 git_author_name: get("GIT_AUTHOR_NAME"),
311 git_author_email: get("GIT_AUTHOR_EMAIL"),
312 git_author_date: get("GIT_AUTHOR_DATE"),
313 git_committer_name: get("GIT_COMMITTER_NAME"),
314 git_committer_email: get("GIT_COMMITTER_EMAIL"),
315 git_committer_date: get("GIT_COMMITTER_DATE"),
316 tz: get("TZ"),
317 user: get("USER"),
318 username: get("USERNAME"),
319 program_files: get("ProgramFiles"),
320 program_files_x86: get("ProgramFiles(x86)"),
321 captured_vars: (!map.is_empty()).then(|| Arc::new(map)),
322 }
323 }
324
325 #[must_use]
327 pub fn git_now_date_override(&self) -> Option<i64> {
328 let raw = self
329 .git_committer_date
330 .as_deref()
331 .or(self.git_author_date.as_deref())?;
332 raw.split_whitespace()
333 .next()
334 .and_then(|p| p.parse::<i64>().ok())
335 }
336
337 #[must_use]
342 pub fn reftable_autocompaction_enabled(&self) -> bool {
343 self.git_test_reftable_autocompaction
344 .as_deref()
345 .map(|value| value != "false")
346 .unwrap_or(true)
347 }
348
349 #[must_use]
351 pub fn test_assume_different_owner(&self) -> bool {
352 self.git_test_assume_different_owner
353 .as_deref()
354 .map(|v| {
355 let lower = v.to_ascii_lowercase();
356 v == "1" || lower == "true" || lower == "yes" || lower == "on"
357 })
358 .unwrap_or(false)
359 }
360
361 #[must_use]
374 pub fn with_git_dir(mut self, path: impl AsRef<Path>) -> Self {
375 self.git_dir = Some(self.absolute_from_cwd(path.as_ref()));
376 self
377 }
378
379 #[must_use]
391 pub fn with_work_tree(mut self, path: impl AsRef<Path>) -> Self {
392 self.git_work_tree = Some(self.absolute_from_cwd(path.as_ref()));
393 self
394 }
395
396 #[must_use]
411 pub fn with_var(mut self, key: impl Into<String>, value: impl Into<OsString>) -> Self {
412 let key = key.into();
413 let value = value.into();
414 let mut map = self
415 .captured_vars
416 .as_ref()
417 .map(|vars| (**vars).clone())
418 .unwrap_or_default();
419 map.insert(key, value);
420 self.captured_vars = Some(Arc::new(map));
421 self
422 }
423
424 pub fn push_config_override(&mut self, config_override: &ConfigOverride) {
437 use crate::transport::sq_quote_shell_arg;
438
439 let mut entry = sq_quote_shell_arg(&config_override.key);
440 entry.push('=');
441 if let Some(value) = &config_override.value {
442 entry.push_str(&sq_quote_shell_arg(value));
443 }
444 match &mut self.git_config_parameters {
445 Some(existing) if !existing.trim().is_empty() => {
446 existing.push(' ');
447 existing.push_str(&entry);
448 }
449 slot => *slot = Some(entry),
450 }
451 }
452
453 fn absolute_from_cwd(&self, path: &Path) -> String {
454 if path.is_absolute() {
455 path.to_string_lossy().into_owned()
456 } else {
457 self.cwd.join(path).to_string_lossy().into_owned()
458 }
459 }
460
461 #[must_use]
470 pub fn capture_process() -> Self {
471 Self::from_vars(std::env::vars_os(), process_cwd_fallback())
473 }
474
475 #[must_use]
477 pub fn config_fingerprint(&self) -> Option<Vec<(String, Option<String>)>> {
478 const VARS: [&str; 8] = [
479 "GIT_CONFIG_NOSYSTEM",
480 "GIT_CONFIG_SYSTEM",
481 "GIT_CONFIG_GLOBAL",
482 "XDG_CONFIG_HOME",
483 "HOME",
484 "GIT_CONFIG",
485 "GIT_CONFIG_PARAMETERS",
486 "GIT_CONFIG_COUNT",
487 ];
488 let mut fp: Vec<(String, Option<String>)> = VARS
489 .iter()
490 .map(|name| {
491 let val = match *name {
492 "GIT_CONFIG_NOSYSTEM" => self.git_config_nosystem.clone(),
493 "GIT_CONFIG_SYSTEM" => self.git_config_system.clone(),
494 "GIT_CONFIG_GLOBAL" => self.git_config_global.clone(),
495 "XDG_CONFIG_HOME" => self.xdg_config_home.clone(),
496 "HOME" => self
497 .home
498 .as_ref()
499 .and_then(|h| h.to_str().map(str::to_owned)),
500 "GIT_CONFIG" => self.git_config.clone(),
501 "GIT_CONFIG_PARAMETERS" => self.git_config_parameters.clone(),
502 "GIT_CONFIG_COUNT" => self.git_config_count.clone(),
503 _ => None,
504 };
505 ((*name).to_owned(), val)
506 })
507 .collect();
508 if let Some(count_str) = &self.git_config_count {
509 const MAX_TRACKED: usize = 256;
510 match count_str.parse::<usize>() {
511 Ok(n) if n <= MAX_TRACKED => {
512 for i in 0..n {
513 fp.push((
514 format!("GIT_CONFIG_KEY_{i}"),
515 self.git_config_pairs.get(i).map(|p| p.0.clone()),
516 ));
517 fp.push((
518 format!("GIT_CONFIG_VALUE_{i}"),
519 self.git_config_pairs.get(i).map(|p| p.1.clone()),
520 ));
521 }
522 }
523 Ok(_) => return None,
524 Err(_) => {}
525 }
526 }
527 fp.push((
528 "GRIT_ENV_CWD".to_owned(),
529 self.cwd.to_str().map(str::to_owned),
530 ));
531 fp.push(("GRIT_ENV_PWD".to_owned(), self.pwd.clone()));
532 Some(fp)
533 }
534
535 #[must_use]
537 pub fn var(&self, key: &str) -> Option<String> {
538 match key {
539 "GIT_DIR" => self.git_dir.clone(),
540 "GIT_WORK_TREE" => self.git_work_tree.clone(),
541 "GIT_CEILING_DIRECTORIES" => self.git_ceiling_directories.clone(),
542 "GIT_INDEX_FILE" => self.git_index_file.clone(),
543 "GIT_NAMESPACE" => self.git_namespace.clone(),
544 "GIT_REPLACE_REF_BASE" => self.git_replace_ref_base.clone(),
545 "GIT_OPTIONAL_LOCKS" => Some(if self.git_optional_locks {
546 "1".to_owned()
547 } else {
548 "0".to_owned()
549 }),
550 "GIT_ALTERNATE_OBJECT_DIRECTORIES" => self.git_alternate_object_directories.clone(),
551 "GIT_OBJECT_DIRECTORY" => self.git_object_directory.clone(),
552 "GIT_CONFIG_NOSYSTEM" => self.git_config_nosystem.clone(),
553 "GIT_CONFIG_SYSTEM" => self.git_config_system.clone(),
554 "GIT_CONFIG_GLOBAL" => self.git_config_global.clone(),
555 "GIT_CONFIG" => self.git_config.clone(),
556 "GIT_CONFIG_PARAMETERS" => self.git_config_parameters.clone(),
557 "GIT_CONFIG_COUNT" => self.git_config_count.clone(),
558 "GIT_PREFIX" => self.git_prefix.clone(),
559 "GIT_TEST_UTF8_NFD_TO_NFC" => self.git_test_utf8_nfd_to_nfc.clone(),
560 "GIT_TEST_REFTABLE_AUTOCOMPACTION" => self.git_test_reftable_autocompaction.clone(),
561 "GIT_AUTHOR_NAME" => self.git_author_name.clone(),
562 "GIT_AUTHOR_EMAIL" => self.git_author_email.clone(),
563 "GIT_AUTHOR_DATE" => self.git_author_date.clone(),
564 "GIT_COMMITTER_NAME" => self.git_committer_name.clone(),
565 "GIT_COMMITTER_EMAIL" => self.git_committer_email.clone(),
566 "GIT_COMMITTER_DATE" => self.git_committer_date.clone(),
567 "TZ" => self.tz.clone(),
568 "USER" => self.user.clone(),
569 "USERNAME" => self.username.clone(),
570 "XDG_CONFIG_HOME" => self.xdg_config_home.clone(),
571 "HOME" => self
572 .home
573 .as_ref()
574 .and_then(|h| h.to_str().map(str::to_owned)),
575 "USERPROFILE" => self
576 .userprofile
577 .as_ref()
578 .and_then(|h| h.to_str().map(str::to_owned)),
579 "HOMEDRIVE" => self
580 .homedrive
581 .as_ref()
582 .and_then(|h| h.to_str().map(str::to_owned)),
583 "HOMEPATH" => self
584 .homepath
585 .as_ref()
586 .and_then(|h| h.to_str().map(str::to_owned)),
587 key if key.starts_with("GIT_CONFIG_KEY_") => {
588 let idx: usize = key["GIT_CONFIG_KEY_".len()..].parse().ok()?;
589 self.git_config_pairs.get(idx).map(|p| p.0.clone())
590 }
591 key if key.starts_with("GIT_CONFIG_VALUE_") => {
592 let idx: usize = key["GIT_CONFIG_VALUE_".len()..].parse().ok()?;
593 self.git_config_pairs.get(idx).map(|p| p.1.clone())
594 }
595 _ => self
596 .captured_vars
597 .as_ref()
598 .and_then(|vars| vars.get(key))
599 .and_then(|v| v.to_str().map(str::to_owned)),
600 }
601 }
602
603 #[must_use]
604 pub fn var_os(&self, key: &str) -> Option<OsString> {
606 if key == "HOME" {
607 return self.home.clone();
608 }
609 if key == "GIT_NO_REPLACE_OBJECTS" && self.git_no_replace_objects {
610 return Some(OsString::new());
611 }
612 if key == "GIT_OPTIONAL_LOCKS" {
613 return Some(OsString::from(if self.git_optional_locks {
614 "1"
615 } else {
616 "0"
617 }));
618 }
619 if key == "GIT_INSTALL_ROOT" {
620 return self.git_install_root.clone();
621 }
622 if key == "GIT_EXEC_PATH" {
623 return self.git_exec_path.clone();
624 }
625 if let Some(v) = self.var(key) {
626 return Some(OsString::from(v));
627 }
628 self.captured_vars.as_ref()?.get(key).cloned()
629 }
630
631 #[must_use]
644 pub(crate) fn for_local_transport(&self) -> Self {
645 const CLIENT_REPO_VARS: &[&str] = &[
646 "GIT_DIR",
647 "GIT_WORK_TREE",
648 "GIT_IMPLICIT_WORK_TREE",
649 "GIT_COMMON_DIR",
650 "GIT_INDEX_FILE",
651 "GIT_OBJECT_DIRECTORY",
652 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
653 "GIT_GRAFT_FILE",
654 "GIT_SHALLOW_FILE",
655 "GIT_NO_REPLACE_OBJECTS",
656 "GIT_REPLACE_REF_BASE",
657 "GIT_PREFIX",
658 "GIT_CONFIG",
659 "GIT_CONFIG_PARAMETERS",
660 "GIT_CONFIG_COUNT",
661 ];
662 let is_client_repo_var = |key: &str| {
663 CLIENT_REPO_VARS.contains(&key)
664 || key.starts_with("GIT_CONFIG_KEY_")
665 || key.starts_with("GIT_CONFIG_VALUE_")
666 };
667 let mut env = self.clone();
668 env.git_dir = None;
669 env.git_work_tree = None;
670 env.git_index_file = None;
671 env.git_object_directory = None;
672 env.git_alternate_object_directories = None;
673 env.git_prefix = None;
674 env.git_no_replace_objects = false;
675 env.git_replace_ref_base = None;
676 env.git_config = None;
677 env.git_config_parameters = None;
678 env.git_config_count = None;
679 env.git_config_pairs = Vec::new();
680 env.captured_vars = self.captured_vars.as_ref().and_then(|vars| {
681 let kept: HashMap<String, OsString> = vars
682 .iter()
683 .filter(|(k, _)| !is_client_repo_var(k))
684 .map(|(k, v)| (k.clone(), v.clone()))
685 .collect();
686 (!kept.is_empty()).then(|| Arc::new(kept))
687 });
688 env
689 }
690
691 #[must_use]
693 pub fn discovery_cwd(&self) -> PathBuf {
694 self.cwd
695 .canonicalize_simplified()
696 .unwrap_or_else(|_| self.cwd.clone())
697 }
698
699 #[must_use]
706 pub fn subprocess_environment(&self) -> Vec<(OsString, OsString)> {
707 let mut passthrough: Vec<(&String, &OsString)> = self
708 .captured_vars
709 .iter()
710 .flat_map(|vars| vars.iter())
711 .filter(|(k, _)| !is_typed_environment_var(k))
712 .collect();
713 passthrough.sort_unstable_by(|a, b| a.0.cmp(b.0));
714 let mut set: Vec<(OsString, OsString)> = passthrough
715 .into_iter()
716 .map(|(k, v)| (OsString::from(k), v.clone()))
717 .collect();
718 subprocess_push_str(&mut set, "GIT_DIR", self.git_dir.as_deref());
719 subprocess_push_str(&mut set, "GIT_WORK_TREE", self.git_work_tree.as_deref());
720 subprocess_push_str(
721 &mut set,
722 "GIT_CEILING_DIRECTORIES",
723 self.git_ceiling_directories.as_deref(),
724 );
725 subprocess_push_str(&mut set, "GIT_INDEX_FILE", self.git_index_file.as_deref());
726 subprocess_push_str(&mut set, "GIT_NAMESPACE", self.git_namespace.as_deref());
727 subprocess_push_str(
728 &mut set,
729 "GIT_REPLACE_REF_BASE",
730 self.git_replace_ref_base.as_deref(),
731 );
732 subprocess_push_str(
733 &mut set,
734 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
735 self.git_alternate_object_directories.as_deref(),
736 );
737 subprocess_push_str(
738 &mut set,
739 "GIT_OBJECT_DIRECTORY",
740 self.git_object_directory.as_deref(),
741 );
742 subprocess_push_str(&mut set, "GIT_PREFIX", self.git_prefix.as_deref());
743 subprocess_push_str(
744 &mut set,
745 "GIT_CONFIG_NOSYSTEM",
746 self.git_config_nosystem.as_deref(),
747 );
748 subprocess_push_str(
749 &mut set,
750 "GIT_CONFIG_SYSTEM",
751 self.git_config_system.as_deref(),
752 );
753 subprocess_push_str(
754 &mut set,
755 "GIT_CONFIG_GLOBAL",
756 self.git_config_global.as_deref(),
757 );
758 subprocess_push_str(&mut set, "GIT_CONFIG", self.git_config.as_deref());
759 subprocess_push_str(
760 &mut set,
761 "GIT_CONFIG_PARAMETERS",
762 self.git_config_parameters.as_deref(),
763 );
764 subprocess_push_str(
765 &mut set,
766 "GIT_CONFIG_COUNT",
767 self.git_config_count.as_deref(),
768 );
769 for (i, (k, v)) in self.git_config_pairs.iter().enumerate() {
770 set.push((
771 OsString::from(format!("GIT_CONFIG_KEY_{i}")),
772 OsString::from(k),
773 ));
774 set.push((
775 OsString::from(format!("GIT_CONFIG_VALUE_{i}")),
776 OsString::from(v),
777 ));
778 }
779 subprocess_push_os(&mut set, "HOME", self.home.as_ref());
780 subprocess_push_str(&mut set, "XDG_CONFIG_HOME", self.xdg_config_home.as_deref());
781 subprocess_push_os(&mut set, "USERPROFILE", self.userprofile.as_ref());
782 subprocess_push_os(&mut set, "HOMEDRIVE", self.homedrive.as_ref());
783 subprocess_push_os(&mut set, "HOMEPATH", self.homepath.as_ref());
784 subprocess_push_os(&mut set, "GIT_INSTALL_ROOT", self.git_install_root.as_ref());
785 subprocess_push_os(&mut set, "GIT_EXEC_PATH", self.git_exec_path.as_ref());
786 subprocess_push_str(&mut set, "PWD", self.pwd.as_deref());
787 subprocess_push_str(
788 &mut set,
789 "GIT_TEST_ASSUME_DIFFERENT_OWNER",
790 self.git_test_assume_different_owner.as_deref(),
791 );
792 subprocess_push_str(
793 &mut set,
794 "GRIT_INVOCATION_CWD",
795 self.grit_invocation_cwd.as_deref(),
796 );
797 subprocess_push_str(&mut set, "SUDO_UID", self.sudo_uid.as_deref());
798 subprocess_push_str(
799 &mut set,
800 "GIT_TEST_UTF8_NFD_TO_NFC",
801 self.git_test_utf8_nfd_to_nfc.as_deref(),
802 );
803 subprocess_push_str(
804 &mut set,
805 "GIT_TEST_NO_WRITE_REV_INDEX",
806 self.git_test_no_write_rev_index.as_deref(),
807 );
808 subprocess_push_str(
809 &mut set,
810 "GIT_TEST_REFTABLE_AUTOCOMPACTION",
811 self.git_test_reftable_autocompaction.as_deref(),
812 );
813 subprocess_push_str(&mut set, "GIT_AUTHOR_NAME", self.git_author_name.as_deref());
814 subprocess_push_str(
815 &mut set,
816 "GIT_AUTHOR_EMAIL",
817 self.git_author_email.as_deref(),
818 );
819 subprocess_push_str(&mut set, "GIT_AUTHOR_DATE", self.git_author_date.as_deref());
820 subprocess_push_str(
821 &mut set,
822 "GIT_COMMITTER_NAME",
823 self.git_committer_name.as_deref(),
824 );
825 subprocess_push_str(
826 &mut set,
827 "GIT_COMMITTER_EMAIL",
828 self.git_committer_email.as_deref(),
829 );
830 subprocess_push_str(
831 &mut set,
832 "GIT_COMMITTER_DATE",
833 self.git_committer_date.as_deref(),
834 );
835 subprocess_push_str(&mut set, "TZ", self.tz.as_deref());
836 subprocess_push_str(&mut set, "USER", self.user.as_deref());
837 subprocess_push_str(&mut set, "USERNAME", self.username.as_deref());
838 subprocess_push_str(&mut set, "ProgramFiles", self.program_files.as_deref());
839 subprocess_push_str(
840 &mut set,
841 "ProgramFiles(x86)",
842 self.program_files_x86.as_deref(),
843 );
844 if self.git_no_replace_objects {
845 set.push((OsString::from("GIT_NO_REPLACE_OBJECTS"), OsString::new()));
846 }
847 if !self.git_optional_locks {
848 set.push((OsString::from("GIT_OPTIONAL_LOCKS"), OsString::from("0")));
849 }
850 set
851 }
852
853 #[must_use]
855 pub fn home_dir(&self) -> Option<PathBuf> {
856 if let Some(home) = self.home.as_ref().filter(|h| !h.is_empty()) {
857 return Some(PathBuf::from(home));
858 }
859 #[cfg(windows)]
860 {
861 if let (Some(drive), Some(path)) = (
862 self.homedrive.as_ref().filter(|d| !d.is_empty()),
863 self.homepath.as_ref().filter(|p| !p.is_empty()),
864 ) {
865 let mut combined = drive.clone();
866 combined.push(path);
867 return Some(PathBuf::from(combined));
868 }
869 if let Some(profile) = self.userprofile.as_ref().filter(|p| !p.is_empty()) {
870 return Some(PathBuf::from(profile));
871 }
872 }
873 None
874 }
875
876 #[must_use]
881 pub fn default_xdg_config_git_file(&self, name: &str) -> Option<PathBuf> {
882 if let Some(xdg) = self.xdg_config_home.as_deref().filter(|s| !s.is_empty()) {
883 return Some(PathBuf::from(xdg).join("git").join(name));
884 }
885 self.home_dir()
886 .map(|home| home.join(".config").join("git").join(name))
887 }
888}
889
890#[derive(Debug, Clone, PartialEq, Eq)]
897pub struct ConfigOverride {
898 key: String,
899 value: Option<String>,
900}
901
902impl ConfigOverride {
903 pub fn new(key: impl Into<String>, value: Option<String>) -> crate::error::Result<Self> {
914 let key = key.into();
915 crate::config::canonical_key(&key)?;
916 Ok(Self { key, value })
917 }
918
919 #[must_use]
921 pub fn key(&self) -> &str {
922 &self.key
923 }
924
925 #[must_use]
927 pub fn value(&self) -> Option<&str> {
928 self.value.as_deref()
929 }
930}
931
932impl FromStr for ConfigOverride {
933 type Err = crate::error::Error;
934
935 fn from_str(spec: &str) -> Result<Self, Self::Err> {
937 match spec.split_once('=') {
938 Some((key, value)) => Self::new(key, Some(value.to_owned())),
939 None => Self::new(spec, None),
940 }
941 }
942}
943
944impl IdentityEnv for Environment {
945 fn var(&self, key: &str) -> Option<String> {
946 Environment::var(self, key)
947 }
948
949 fn var_os(&self, key: &str) -> Option<OsString> {
950 Environment::var_os(self, key)
951 }
952}
953
954#[must_use]
959pub fn wall_clock_unix_timestamp_nanos() -> u64 {
960 u64::try_from(OffsetDateTime::now_utc().unix_timestamp_nanos()).unwrap_or(u64::MAX)
961}
962
963#[must_use]
965pub(crate) fn wall_clock_unix_timestamp_seconds() -> i64 {
966 OffsetDateTime::now_utc().unix_timestamp()
967}
968
969#[must_use]
971pub fn offset_from_epoch_and_tz(epoch: i64, tz: Option<&str>) -> OffsetDateTime {
972 let tz_hhmm = local_tzoffset_with_tz(epoch as u64, tz);
973 let offset = utc_offset_from_tz_hhmm(tz_hhmm).unwrap_or(UtcOffset::UTC);
974 OffsetDateTime::from_unix_timestamp(epoch)
975 .unwrap_or(OffsetDateTime::UNIX_EPOCH)
976 .to_offset(offset)
977}
978
979fn utc_offset_from_tz_hhmm(tz: TzHhmm) -> Option<UtcOffset> {
980 let sign: i32 = if tz < 0 { -1 } else { 1 };
981 let abs = tz.unsigned_abs();
982 let hours = (abs / 100) as i32;
983 let minutes = (abs % 100) as i32;
984 let seconds = sign * (hours * 3600 + minutes * 60);
985 UtcOffset::from_whole_seconds(seconds).ok()
986}
987
988const TYPED_ENVIRONMENT_VARS: &[&str] = &[
990 "GIT_DIR",
991 "GIT_WORK_TREE",
992 "GIT_CEILING_DIRECTORIES",
993 "GIT_INDEX_FILE",
994 "GIT_NAMESPACE",
995 "GIT_REPLACE_REF_BASE",
996 "GIT_NO_REPLACE_OBJECTS",
997 "GIT_OPTIONAL_LOCKS",
998 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
999 "GIT_OBJECT_DIRECTORY",
1000 "GIT_PREFIX",
1001 "GIT_CONFIG_NOSYSTEM",
1002 "GIT_CONFIG_SYSTEM",
1003 "GIT_CONFIG_GLOBAL",
1004 "GIT_CONFIG",
1005 "GIT_CONFIG_PARAMETERS",
1006 "GIT_CONFIG_COUNT",
1007 "HOME",
1008 "XDG_CONFIG_HOME",
1009 "USERPROFILE",
1010 "HOMEDRIVE",
1011 "HOMEPATH",
1012 "GIT_INSTALL_ROOT",
1013 "GIT_EXEC_PATH",
1014 "PWD",
1015 "GIT_TEST_ASSUME_DIFFERENT_OWNER",
1016 "GRIT_INVOCATION_CWD",
1017 "SUDO_UID",
1018 "GIT_TEST_UTF8_NFD_TO_NFC",
1019 "GIT_TEST_NO_WRITE_REV_INDEX",
1020 "GIT_TEST_REFTABLE_AUTOCOMPACTION",
1021 "GIT_AUTHOR_NAME",
1022 "GIT_AUTHOR_EMAIL",
1023 "GIT_AUTHOR_DATE",
1024 "GIT_COMMITTER_NAME",
1025 "GIT_COMMITTER_EMAIL",
1026 "GIT_COMMITTER_DATE",
1027 "TZ",
1028 "USER",
1029 "USERNAME",
1030 "ProgramFiles",
1031 "ProgramFiles(x86)",
1032];
1033
1034fn is_typed_environment_var(key: &str) -> bool {
1035 TYPED_ENVIRONMENT_VARS.contains(&key)
1036 || key.starts_with("GIT_CONFIG_KEY_")
1037 || key.starts_with("GIT_CONFIG_VALUE_")
1038}
1039
1040fn git_optional_locks_from_env(raw: Option<&str>) -> bool {
1041 match raw {
1042 None => true,
1043 Some(value) => parse_git_config_maybe_bool(value).unwrap_or(true),
1044 }
1045}
1046
1047fn subprocess_push_str(out: &mut Vec<(OsString, OsString)>, key: &str, val: Option<&str>) {
1048 if let Some(v) = val {
1049 out.push((OsString::from(key), OsString::from(v)));
1050 }
1051}
1052
1053fn subprocess_push_os(out: &mut Vec<(OsString, OsString)>, key: &str, val: Option<&OsString>) {
1054 if let Some(v) = val {
1055 out.push((OsString::from(key), v.clone()));
1056 }
1057}
1058
1059#[derive(Clone)]
1061#[non_exhaustive]
1062pub struct RepositoryOptions {
1063 pub environment: Environment,
1065 pub command_runner: Arc<dyn CommandRunner>,
1067 pub change_id_generator: Arc<dyn ChangeIdGenerator>,
1069 pub diagnostics: DiagnosticsHandle,
1071 pub network_trace: bool,
1073 pub test_assume_different_owner: bool,
1075 pub reference_unix_time: Option<i64>,
1077 pub thread_policy: crate::parallel::ThreadPolicy,
1079 pub work_tree: Option<std::path::PathBuf>,
1081 pub validate_repository_format: bool,
1083 pub open_for_serving: bool,
1085 pub promisor_fetcher: Option<std::sync::Arc<dyn crate::promisor_fetch::PromisorFetcher>>,
1087 pub hook_stdio: crate::hooks::HookStdio,
1089}
1090
1091impl std::fmt::Debug for RepositoryOptions {
1092 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1093 f.debug_struct("RepositoryOptions")
1094 .field("environment", &self.environment)
1095 .field("command_runner", &"<CommandRunner>")
1096 .field("network_trace", &self.network_trace)
1097 .finish_non_exhaustive()
1098 }
1099}
1100
1101impl Default for RepositoryOptions {
1102 fn default() -> Self {
1103 Self {
1104 environment: Environment::empty(),
1105 command_runner: system_command_runner(),
1106 change_id_generator: Arc::new(OsChangeIdGenerator),
1107 diagnostics: Arc::new(NullDiagnostics),
1108 network_trace: false,
1109 test_assume_different_owner: false,
1110 reference_unix_time: None,
1111 thread_policy: crate::parallel::ThreadPolicy::default(),
1112 work_tree: None,
1113 validate_repository_format: true,
1114 open_for_serving: false,
1115 promisor_fetcher: None,
1116 hook_stdio: crate::hooks::HookStdio::default(),
1117 }
1118 }
1119}
1120
1121impl RepositoryOptions {
1122 #[must_use]
1130 pub fn empty() -> Self {
1131 Self::default()
1132 }
1133
1134 #[must_use]
1141 pub fn from_process() -> Self {
1142 Self::with_environment(Environment::capture_process())
1144 }
1145
1146 #[must_use]
1151 pub fn with_environment(environment: Environment) -> Self {
1152 Self {
1153 environment,
1154 ..Self::default()
1155 }
1156 }
1157
1158 #[must_use]
1160 pub fn with_command_runner(mut self, runner: Arc<dyn CommandRunner>) -> Self {
1161 self.command_runner = runner;
1162 self
1163 }
1164
1165 #[must_use]
1167 pub fn with_change_id_generator(mut self, generator: Arc<dyn ChangeIdGenerator>) -> Self {
1168 self.change_id_generator = generator;
1169 self
1170 }
1171
1172 #[must_use]
1174 pub fn with_work_tree(mut self, work_tree: impl Into<std::path::PathBuf>) -> Self {
1175 self.work_tree = Some(work_tree.into());
1176 self
1177 }
1178
1179 #[must_use]
1181 pub fn without_format_validation(mut self) -> Self {
1182 self.validate_repository_format = false;
1183 self
1184 }
1185
1186 #[must_use]
1188 pub fn for_serving(mut self) -> Self {
1189 self.open_for_serving = true;
1190 self
1191 }
1192
1193 #[must_use]
1195 pub fn with_diagnostics(mut self, diagnostics: DiagnosticsHandle) -> Self {
1196 self.diagnostics = diagnostics;
1197 self
1198 }
1199
1200 #[must_use]
1204 pub fn with_hook_stdio(mut self, hook_stdio: crate::hooks::HookStdio) -> Self {
1205 self.hook_stdio = hook_stdio;
1206 self
1207 }
1208
1209 #[must_use]
1211 pub fn with_network_trace(mut self, network_trace: bool) -> Self {
1212 self.network_trace = network_trace;
1213 self
1214 }
1215
1216 #[must_use]
1218 pub fn with_promisor_fetcher(
1219 mut self,
1220 fetcher: Option<std::sync::Arc<dyn crate::promisor_fetch::PromisorFetcher>>,
1221 ) -> Self {
1222 self.promisor_fetcher = fetcher;
1223 self
1224 }
1225}
1226
1227fn process_cwd_fallback() -> PathBuf {
1228 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234 use super::*;
1235
1236 #[test]
1237 fn cloned_environment_keeps_overrides_independent() {
1238 let original = Environment::from_vars(
1239 [
1240 (OsString::from("GIT_DIR"), OsString::from("original.git")),
1241 (OsString::from("HOME"), OsString::from("/original/home")),
1242 (OsString::from("BUILD_SETTING"), OsString::from("captured")),
1243 ],
1244 PathBuf::from("/workspace"),
1245 );
1246 let sibling = original.clone();
1247 let mut changed = original.clone();
1248 drop(original);
1249 changed.git_dir = None;
1250 changed.home = Some(OsString::from("/changed/home"));
1251 changed.push_config_override(&"user.name=Changed".parse().expect("override"));
1252
1253 assert_eq!(sibling.var("GIT_DIR").as_deref(), Some("original.git"));
1254 assert_eq!(
1255 sibling.home.as_deref(),
1256 Some(std::ffi::OsStr::new("/original/home"))
1257 );
1258 assert!(sibling.git_config_parameters.is_none());
1259 let exported = changed.subprocess_environment();
1260 assert!(!exported.iter().any(|(key, _)| key == "GIT_DIR"));
1261 assert!(exported.contains(&(OsString::from("HOME"), OsString::from("/changed/home"))));
1262 assert!(exported.contains(&(OsString::from("BUILD_SETTING"), OsString::from("captured"))));
1263 }
1264
1265 #[cfg(unix)]
1266 #[test]
1267 fn cloned_environment_preserves_non_unicode_passthrough() {
1268 use std::os::unix::ffi::OsStringExt;
1269
1270 let value = OsString::from_vec(vec![b'a', 0xff, b'z']);
1271 let original = Environment::from_vars(
1272 [(OsString::from("BUILD_SETTING"), value.clone())],
1273 PathBuf::from("."),
1274 );
1275 let cloned = original.clone();
1276 drop(original);
1277 assert_eq!(cloned.var("BUILD_SETTING"), None);
1278 assert_eq!(cloned.var_os("BUILD_SETTING"), Some(value.clone()));
1279 assert_eq!(
1280 cloned.subprocess_environment(),
1281 vec![(OsString::from("BUILD_SETTING"), value)]
1282 );
1283 }
1284
1285 #[test]
1286 fn git_optional_locks_parses_like_git() {
1287 assert!(Environment::from_vars([], PathBuf::from(".")).git_optional_locks);
1288 for disabled in ["0", "false", "no", "off", "FALSE"] {
1289 let env = Environment::from_vars(
1290 [(
1291 OsString::from("GIT_OPTIONAL_LOCKS"),
1292 OsString::from(disabled),
1293 )],
1294 PathBuf::from("."),
1295 );
1296 assert!(!env.git_optional_locks, "expected disabled for {disabled}");
1297 }
1298 let enabled = Environment::from_vars(
1299 [(OsString::from("GIT_OPTIONAL_LOCKS"), OsString::from("1"))],
1300 PathBuf::from("."),
1301 );
1302 assert!(enabled.git_optional_locks);
1303 }
1304
1305 #[test]
1306 fn is_fetch_options_default_matches_empty() {
1307 assert!(Environment::empty().is_fetch_options_default());
1308 assert!(Environment::from_vars([], PathBuf::from("."))
1309 .clone()
1310 .is_fetch_options_default());
1311 let mut env = Environment::from_vars(
1312 [(
1313 OsString::from("GIT_SSH_COMMAND"),
1314 OsString::from("/bin/wrap"),
1315 )],
1316 PathBuf::from("."),
1317 );
1318 assert!(!env.is_fetch_options_default());
1319 env.captured_vars = None;
1320 assert!(env.is_fetch_options_default());
1321 }
1322
1323 #[test]
1324 fn local_transport_keeps_config_discovery_and_drops_the_client_repository() {
1325 let vars = [
1326 ("HOME", "/home/u"),
1327 ("XDG_CONFIG_HOME", "/home/u/.config"),
1328 ("GIT_CONFIG_NOSYSTEM", "1"),
1329 ("GIT_CONFIG_GLOBAL", "/home/u/alt.gitconfig"),
1330 ("GIT_DIR", "/client/.git"),
1331 ("GIT_WORK_TREE", "/client"),
1332 ("GIT_COMMON_DIR", "/client/.git"),
1333 ("GIT_OBJECT_DIRECTORY", "/client/.git/objects"),
1334 ("GIT_ALTERNATE_OBJECT_DIRECTORIES", "/elsewhere"),
1335 ("GIT_INDEX_FILE", "/client/.git/index"),
1336 ("GIT_CONFIG_PARAMETERS", "'core.bare'='true'"),
1337 ("GIT_CONFIG_COUNT", "1"),
1338 ("GIT_CONFIG_KEY_0", "uploadpack.allowfilter"),
1339 ("GIT_CONFIG_VALUE_0", "false"),
1340 ("PATH", "/usr/bin"),
1341 ]
1342 .map(|(k, v)| (OsString::from(k), OsString::from(v)));
1343 let env = Environment::from_vars(vars, PathBuf::from("/client")).for_local_transport();
1344 assert_eq!(env.var("HOME").as_deref(), Some("/home/u"));
1345 assert_eq!(
1346 env.var("XDG_CONFIG_HOME").as_deref(),
1347 Some("/home/u/.config")
1348 );
1349 assert_eq!(env.var("GIT_CONFIG_NOSYSTEM").as_deref(), Some("1"));
1350 assert_eq!(
1351 env.var("GIT_CONFIG_GLOBAL").as_deref(),
1352 Some("/home/u/alt.gitconfig")
1353 );
1354 assert_eq!(env.var("PATH").as_deref(), Some("/usr/bin"));
1355 for key in [
1356 "GIT_DIR",
1357 "GIT_WORK_TREE",
1358 "GIT_COMMON_DIR",
1359 "GIT_OBJECT_DIRECTORY",
1360 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
1361 "GIT_INDEX_FILE",
1362 "GIT_CONFIG_PARAMETERS",
1363 "GIT_CONFIG_COUNT",
1364 "GIT_CONFIG_KEY_0",
1365 "GIT_CONFIG_VALUE_0",
1366 ] {
1367 assert_eq!(env.var_os(key), None, "{key}");
1368 }
1369 assert!(env.git_config_pairs.is_empty());
1370 }
1371
1372 #[test]
1373 fn var_reads_typed_object_directory_fields() {
1374 let vars = [
1375 ("GIT_OBJECT_DIRECTORY", "/captured/objects"),
1376 ("GIT_ALTERNATE_OBJECT_DIRECTORIES", "/captured/alt"),
1377 ]
1378 .into_iter()
1379 .map(|(k, v)| (OsString::from(k), OsString::from(v)));
1380 let mut env = Environment::from_vars(vars, PathBuf::from("/tmp"));
1381 assert_eq!(
1382 env.var("GIT_OBJECT_DIRECTORY").as_deref(),
1383 Some("/captured/objects")
1384 );
1385 env.git_object_directory = Some("/typed/objects".to_owned());
1386 env.git_alternate_object_directories = None;
1387 assert_eq!(
1388 env.var("GIT_OBJECT_DIRECTORY").as_deref(),
1389 Some("/typed/objects")
1390 );
1391 assert_eq!(env.var("GIT_ALTERNATE_OBJECT_DIRECTORIES"), None);
1392 }
1393
1394 #[test]
1395 fn subprocess_environment_passes_through_untyped_vars() {
1396 let vars = [
1397 ("PATH", "/opt/tools/bin:/usr/bin"),
1398 ("SystemRoot", r"C:\Windows"),
1399 ("GIT_AUTHOR_NAME", "Ada"),
1400 ("GIT_DIR", "/stale/.git"),
1401 ("GIT_CONFIG_COUNT", "1"),
1402 ("GIT_CONFIG_KEY_0", "user.name"),
1403 ("GIT_CONFIG_VALUE_0", "Ada"),
1404 ]
1405 .into_iter()
1406 .map(|(k, v)| (OsString::from(k), OsString::from(v)));
1407 let mut env = Environment::from_vars(vars, PathBuf::from("/tmp"));
1408 env.git_dir = None;
1409 env.git_config_pairs[0].1 = "Grace".to_owned();
1410
1411 let set = env.subprocess_environment();
1412 let get = |key: &str| -> Vec<&OsString> {
1413 set.iter()
1414 .filter(|(k, _)| k == key)
1415 .map(|(_, v)| v)
1416 .collect()
1417 };
1418 assert_eq!(get("PATH"), vec!["/opt/tools/bin:/usr/bin"]);
1419 assert_eq!(get("SystemRoot"), vec![r"C:\Windows"]);
1420 assert_eq!(get("GIT_AUTHOR_NAME"), vec!["Ada"]);
1421 assert!(get("GIT_DIR").is_empty(), "cleared typed field must win");
1422 assert_eq!(get("GIT_CONFIG_VALUE_0"), vec!["Grace"]);
1423 }
1424
1425 #[test]
1426 fn subprocess_environment_of_empty_snapshot_is_empty() {
1427 assert!(Environment::empty().subprocess_environment().is_empty());
1428 }
1429
1430 #[test]
1431 fn with_var_overlays_without_resnapshotting_process() {
1432 let env = Environment::empty()
1433 .with_var("REMOTE_USER", "alice@example.com")
1434 .with_var("GRIT_USER", "alice@example.com");
1435 let set = env.subprocess_environment();
1436 let get = |key: &str| {
1437 set.iter()
1438 .find(|(k, _)| k == key)
1439 .map(|(_, v)| v.to_string_lossy().into_owned())
1440 };
1441 assert_eq!(get("REMOTE_USER").as_deref(), Some("alice@example.com"));
1442 assert_eq!(get("GRIT_USER").as_deref(), Some("alice@example.com"));
1443 }
1444
1445 #[test]
1446 fn from_vars_parses_config_count_pairs() {
1447 let vars = [
1448 ("GIT_CONFIG_COUNT", "1"),
1449 ("GIT_CONFIG_KEY_0", "user.name"),
1450 ("GIT_CONFIG_VALUE_0", "Ada"),
1451 ]
1452 .into_iter()
1453 .map(|(k, v)| (OsString::from(k), OsString::from(v)));
1454 let env = Environment::from_vars(vars, PathBuf::from("/tmp"));
1455 assert_eq!(env.git_config_count.as_deref(), Some("1"));
1456 assert_eq!(
1457 env.git_config_pairs,
1458 vec![("user.name".to_owned(), "Ada".to_owned())]
1459 );
1460 }
1461
1462 #[test]
1463 fn config_fingerprint_tracks_global_override() {
1464 let mut env = Environment::empty();
1465 env.git_config_global = Some("/tmp/global.cfg".to_owned());
1466 let fp = env.config_fingerprint().expect("fingerprint");
1467 assert!(fp
1468 .iter()
1469 .any(|(k, v)| k == "GIT_CONFIG_GLOBAL" && v.as_deref() == Some("/tmp/global.cfg")));
1470 }
1471
1472 #[test]
1473 fn default_xdg_config_git_file_prefers_xdg() {
1474 let mut env = Environment::empty();
1475 env.home = Some(OsString::from("/home/u"));
1476 env.xdg_config_home = Some("/xdg".to_owned());
1477 assert_eq!(
1478 env.default_xdg_config_git_file("ignore"),
1479 Some(PathBuf::from("/xdg/git/ignore"))
1480 );
1481 }
1482
1483 fn isolated_env(dir: &Path) -> Environment {
1484 let mut env = Environment::empty();
1485 env.cwd = dir.to_path_buf();
1486 env.home = Some(dir.as_os_str().to_owned());
1487 env.git_config_nosystem = Some("1".to_owned());
1488 env.git_config_global = Some(dir.join("no-global").to_string_lossy().into_owned());
1489 env
1490 }
1491
1492 fn override_of(spec: &str) -> ConfigOverride {
1493 spec.parse().expect("valid override")
1494 }
1495
1496 #[test]
1497 fn config_override_parses_key_value_bare_and_empty() {
1498 let o = override_of("user.name=Ada Lovelace");
1499 assert_eq!((o.key(), o.value()), ("user.name", Some("Ada Lovelace")));
1500 let o = override_of("http.extraHeader=Authorization: a=b");
1501 assert_eq!(o.value(), Some("Authorization: a=b"));
1502 let o = override_of("core.bare");
1503 assert_eq!((o.key(), o.value()), ("core.bare", None));
1504 let o = override_of("user.email=");
1505 assert_eq!(o.value(), Some(""));
1506 let o = override_of("remote.My Origin.url=x");
1507 assert_eq!(o.key(), "remote.My Origin.url");
1508 }
1509
1510 #[test]
1511 fn config_override_rejects_invalid_keys() {
1512 for spec in [
1513 "nosection",
1514 "=value",
1515 "user.=x",
1516 ".name=x",
1517 "us er.name=x",
1518 "",
1519 ] {
1520 assert!(
1521 spec.parse::<ConfigOverride>().is_err(),
1522 "{spec:?} should be rejected"
1523 );
1524 }
1525 assert!(ConfigOverride::new("a\nb.c", None).is_err());
1526 }
1527
1528 #[test]
1529 fn push_config_override_round_trips_through_parameter_parser() {
1530 let mut env = Environment::empty();
1531 let tricky = "it's a \"quote\" = !bang spaced";
1532 env.push_config_override(
1533 &ConfigOverride::new("user.name", Some(tricky.to_owned())).unwrap(),
1534 );
1535 env.push_config_override(&override_of("core.bare"));
1536 env.push_config_override(&override_of("user.email="));
1537 let raw = env.git_config_parameters.clone().expect("parameters set");
1538 assert_eq!(
1539 crate::config::parse_config_parameters(&raw),
1540 vec![
1541 format!("user.name\u{1}{tricky}"),
1542 "core.bare\u{1}".to_owned(),
1543 "user.email\u{1}".to_owned(),
1544 ]
1545 );
1546 }
1547
1548 #[test]
1549 fn push_config_override_appends_to_inherited_parameters() {
1550 let mut env = Environment::empty();
1551 env.git_config_parameters = Some("'core.editor'='vi'".to_owned());
1552 env.push_config_override(&override_of("user.name=CI"));
1553 assert_eq!(
1554 env.git_config_parameters.as_deref(),
1555 Some("'core.editor'='vi' 'user.name'='CI'")
1556 );
1557 let mut blank = Environment::empty();
1558 blank.git_config_parameters = Some(" ".to_owned());
1559 blank.push_config_override(&override_of("user.name=CI"));
1560 assert_eq!(
1561 blank.git_config_parameters.as_deref(),
1562 Some("'user.name'='CI'")
1563 );
1564 }
1565
1566 #[test]
1567 fn later_config_override_wins_over_earlier_env_pairs_and_files() {
1568 let dir = tempfile::tempdir().unwrap();
1569 let git_dir = dir.path().join(".git");
1570 std::fs::create_dir_all(&git_dir).unwrap();
1571 std::fs::write(
1572 git_dir.join("config"),
1573 "[user]\n\tname = FromRepo\n\temail = repo@x.y\n[core]\n\tbare = false\n",
1574 )
1575 .unwrap();
1576 let mut env = isolated_env(dir.path());
1577 env.git_config_count = Some("1".to_owned());
1578 env.git_config_pairs = vec![("user.name".to_owned(), "FromCount".to_owned())];
1579 env.push_config_override(&override_of("user.name=First"));
1580 env.push_config_override(&override_of("user.name=Second"));
1581 env.push_config_override(&override_of("core.bare"));
1582
1583 let config = crate::config::ConfigSet::load(&env, Some(&git_dir), false).unwrap();
1584 assert_eq!(config.get("user.name").as_deref(), Some("Second"));
1585 assert_eq!(config.get("user.email").as_deref(), Some("repo@x.y"));
1586 assert_eq!(config.get_bool("core.bare"), Some(Ok(true)));
1587 }
1588
1589 #[test]
1590 fn config_override_is_exported_to_subprocesses() {
1591 let mut env = Environment::empty();
1592 env.push_config_override(&override_of("user.name=CI"));
1593 assert!(env.subprocess_environment().contains(&(
1594 OsString::from("GIT_CONFIG_PARAMETERS"),
1595 OsString::from("'user.name'='CI'")
1596 )));
1597 }
1598
1599 #[test]
1600 fn with_git_dir_and_work_tree_resolve_relative_paths_against_cwd() {
1601 let mut env = Environment::empty();
1602 env.cwd = PathBuf::from("/base/dir");
1603 let env = env.with_git_dir("repo/.git").with_work_tree("wt");
1604 assert_eq!(
1605 env.git_dir.as_deref().map(PathBuf::from),
1606 Some(PathBuf::from("/base/dir/repo/.git"))
1607 );
1608 assert_eq!(
1609 env.git_work_tree.as_deref().map(PathBuf::from),
1610 Some(PathBuf::from("/base/dir/wt"))
1611 );
1612 let abs = std::env::temp_dir().join("abs.git");
1613 let env = env.with_git_dir(&abs);
1614 assert_eq!(env.git_dir.as_deref().map(PathBuf::from), Some(abs));
1615 }
1616
1617 #[test]
1618 fn with_git_dir_opens_that_repository_from_elsewhere() {
1619 let dir = tempfile::tempdir().unwrap();
1620 let repo_dir = dir.path().join("r");
1621 crate::repo::init_repository(
1622 &repo_dir,
1623 false,
1624 "main",
1625 None,
1626 crate::ref_storage::RefStorageFormat::Files,
1627 )
1628 .unwrap();
1629 let elsewhere = dir.path().join("elsewhere");
1630 std::fs::create_dir_all(&elsewhere).unwrap();
1631 let mut env = isolated_env(&elsewhere);
1632 env.cwd = elsewhere.clone();
1633 let env = env.with_git_dir("../r/.git").with_work_tree("../r");
1634 let repo =
1635 crate::repo::Repository::discover(None, &RepositoryOptions::with_environment(env))
1636 .unwrap();
1637 assert_eq!(
1638 repo.git_dir().canonicalize_simplified().unwrap(),
1639 repo_dir.join(".git").canonicalize_simplified().unwrap()
1640 );
1641 assert_eq!(
1642 repo.work_tree()
1643 .map(|p| p.canonicalize_simplified().unwrap()),
1644 Some(repo_dir.canonicalize_simplified().unwrap())
1645 );
1646 }
1647
1648 #[test]
1649 fn default_xdg_config_git_file_falls_back_to_home_config() {
1650 let mut env = Environment::empty();
1651 env.home = Some(OsString::from("/home/u"));
1652 assert_eq!(
1653 env.default_xdg_config_git_file("ignore"),
1654 Some(PathBuf::from("/home/u/.config/git/ignore"))
1655 );
1656 }
1657
1658 #[test]
1659 fn empty_options_have_no_home_or_system_config_roots() {
1660 let env = RepositoryOptions::empty().environment;
1661 assert!(env.home.is_none());
1662 assert!(env.userprofile.is_none());
1663 assert!(env.homedrive.is_none() && env.homepath.is_none());
1664 assert!(env.xdg_config_home.is_none());
1665 assert!(env.program_files.is_none() && env.program_files_x86.is_none());
1666 assert!(env.git_install_root.is_none());
1667 assert!(env.git_config_system.is_none() && env.git_config_global.is_none());
1668 assert!(env.captured_vars.is_none());
1669 }
1670
1671 #[test]
1672 fn from_process_snapshots_the_process_environment() {
1673 let options = RepositoryOptions::from_process();
1674 let process = Environment::capture_process();
1675 assert_eq!(options.environment.home, process.home);
1676 assert_eq!(options.environment.program_files, process.program_files);
1677 assert_eq!(options.environment.cwd, process.cwd);
1678 assert_eq!(options.environment.home, std::env::var_os("HOME"));
1679 assert!(options.environment.captured_vars.is_some());
1680 }
1681}