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