1use crate::git_path::CanonicalizeExt;
7use std::collections::HashSet;
8use std::fs;
9use std::path::{Component, Path, PathBuf};
10
11use crate::config::{ConfigFile, ConfigScope};
12use crate::error::{Error, Result};
13use crate::hash;
14use crate::index::Index;
15use crate::objects::{ObjectId, ObjectKind};
16use crate::odb::Odb;
17
18#[must_use]
24pub fn submodule_modules_git_dir(super_git_dir: &Path, submodule_relpath: &str) -> PathBuf {
25 let mut out = super_git_dir.to_path_buf();
26 out.push("modules");
27 for seg in submodule_relpath.split(['/', '\\']) {
28 if seg.is_empty() || seg == "." {
29 continue;
30 }
31 out.push(seg);
32 }
33 out
34}
35
36pub fn submodule_path_config_enabled(git_dir: &Path) -> bool {
41 let config_path = git_dir.join("config");
42 let Ok(content) = fs::read_to_string(&config_path) else {
43 return false;
44 };
45 let mut in_extensions = false;
46 for line in content.lines() {
47 let trimmed = line.trim();
48 if trimmed.starts_with('[') {
49 in_extensions = trimmed.eq_ignore_ascii_case("[extensions]");
50 continue;
51 }
52 if in_extensions {
53 if let Some((k, v)) = trimmed.split_once('=') {
54 if k.trim().eq_ignore_ascii_case("submodulepathconfig") {
55 return parse_bool(v.trim());
56 }
57 }
58 }
59 }
60 false
61}
62
63fn parse_bool(s: &str) -> bool {
64 matches!(s.to_ascii_lowercase().as_str(), "true" | "yes" | "on" | "1")
65}
66
67fn is_rfc3986_unreserved(b: u8) -> bool {
68 b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~')
69}
70
71fn is_casefolding_rfc3986_unreserved(b: u8) -> bool {
72 matches!(b, b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~')
73}
74
75fn percent_encode(name: &str, pred: fn(u8) -> bool) -> String {
76 let mut out = String::new();
77 for &b in name.as_bytes() {
78 if pred(b) {
79 out.push(b as char);
80 } else {
81 out.push_str(&format!("%{:02x}", b));
82 }
83 }
84 out
85}
86
87pub fn is_git_directory(path: &Path) -> bool {
89 path.join("HEAD").is_file() && path.join("objects").is_dir()
90}
91
92#[must_use]
98pub(crate) fn is_valid_git_dir(git_dir: &Path) -> bool {
99 if !git_head_ref_valid(&git_dir.join("HEAD")) {
100 return false;
101 }
102 let object_store = crate::repo::common_git_dir_for_config(git_dir);
103 object_store.join("objects").is_dir() && object_store.join("refs").is_dir()
104}
105
106#[cfg(test)]
107mod embedded_git_dir_tests {
108 use super::*;
109 use std::fs;
110
111 #[test]
112 fn placeholder_dot_git_dir_is_not_a_repository() {
113 let tmp = tempfile::tempdir().unwrap();
114 let git = tmp.path().join(".git");
115 fs::create_dir_all(&git).unwrap();
116 assert!(!is_valid_git_dir(&git));
117 }
118
119 #[test]
120 fn minimal_init_layout_is_a_repository() {
121 let tmp = tempfile::tempdir().unwrap();
122 let git = tmp.path().join(".git");
123 fs::create_dir_all(git.join("objects")).unwrap();
124 fs::create_dir_all(git.join("refs/heads")).unwrap();
125 fs::write(git.join("HEAD"), "ref: refs/heads/main\n").unwrap();
126 assert!(is_valid_git_dir(&git));
127 }
128
129 #[test]
130 fn linked_worktree_admin_dir_uses_commondir_for_stores() {
131 let tmp = tempfile::tempdir().unwrap();
132 let common = tmp.path().join("repo.git");
133 fs::create_dir_all(common.join("objects")).unwrap();
134 fs::create_dir_all(common.join("refs/heads")).unwrap();
135 let admin = common.join("worktrees/linked");
136 fs::create_dir_all(&admin).unwrap();
137 fs::write(admin.join("HEAD"), "ref: refs/heads/main\n").unwrap();
138 fs::write(admin.join("commondir"), "../..\n").unwrap();
139 assert!(is_valid_git_dir(&admin));
140 assert!(
141 !admin.join("objects").is_dir(),
142 "admin dir must not need local objects/"
143 );
144 }
145}
146
147fn git_head_ref_valid(head_path: &Path) -> bool {
148 let Ok(meta) = fs::symlink_metadata(head_path) else {
149 return false;
150 };
151 if meta.file_type().is_symlink() {
152 if let Ok(target) = fs::read_link(head_path) {
153 return target.to_string_lossy().starts_with("refs/");
154 }
155 return false;
156 }
157 let Ok(content) = fs::read_to_string(head_path) else {
158 return false;
159 };
160 let trimmed = content.trim();
161 if let Some(rest) = trimmed.strip_prefix("ref:") {
162 return rest.trim().starts_with("refs/");
163 }
164 ObjectId::from_hex(trimmed).is_ok()
165}
166
167pub fn validate_submodule_path(work_tree: &Path, rel: &str) -> Result<()> {
172 if rel.is_empty() {
173 return Err(Error::Config("empty submodule path".into()));
174 }
175 let mut cur = work_tree.to_path_buf();
176 #[cfg(windows)]
177 let parts = rel.split(|c| c == '/' || c == '\\');
178 #[cfg(not(windows))]
179 let parts = rel.split('/');
180 for comp in parts.filter(|s| !s.is_empty()) {
181 cur.push(comp);
182 let meta = match fs::symlink_metadata(&cur) {
183 Ok(m) => m,
184 Err(_) => continue,
185 };
186 if meta.file_type().is_symlink() {
187 return Err(Error::Config(
188 format!("expected '{comp}' in submodule path '{rel}' not to be a symbolic link")
189 .into(),
190 ));
191 }
192 }
193 Ok(())
194}
195
196fn last_modules_segment(git_dir_abs: &Path) -> Option<String> {
197 let s = git_dir_abs.to_string_lossy();
198 let marker = "/modules/";
199 let mut p = 0usize;
200 let mut last_start = None;
201 while let Some(idx) = s[p..].find(marker) {
202 let start = p + idx + marker.len();
203 last_start = Some(start);
204 p = start + 1;
205 }
206 last_start.map(|start| s[start..].to_string())
207}
208
209fn path_inside_other_gitdir(git_dir: &Path, submodule_name: &str) -> bool {
210 submodule_gitdir_outer_conflict(git_dir, submodule_name).is_some()
211}
212
213#[must_use]
217pub fn submodule_gitdir_outer_conflict(git_dir: &Path, submodule_name: &str) -> Option<PathBuf> {
218 let suffix = submodule_name.as_bytes();
219 let gd = git_dir.to_string_lossy();
220 let gd_bytes = gd.as_bytes();
221 if gd_bytes.len() <= suffix.len() {
222 return None;
223 }
224 let cut = gd_bytes.len() - suffix.len();
225 if gd_bytes[cut - 1] != b'/' {
226 return None;
227 }
228 if &gd_bytes[cut..] != suffix {
229 return None;
230 }
231 for i in cut..gd_bytes.len() {
232 if gd_bytes[i] == b'/' {
233 let prefix = Path::new(std::str::from_utf8(&gd_bytes[..i]).unwrap_or(""));
234 if is_git_directory(prefix) {
235 return Some(prefix.to_path_buf());
236 }
237 }
238 }
239 None
240}
241
242fn resolve_gitdir_value(work_tree: &Path, gitdir_cfg: &str) -> PathBuf {
243 let p = Path::new(gitdir_cfg.trim());
244 if p.is_absolute() {
245 p.to_path_buf()
246 } else {
247 work_tree.join(p)
248 }
249}
250
251fn canonical_abs(path: &Path) -> PathBuf {
252 path.canonicalize_simplified()
253 .unwrap_or_else(|_| path.to_path_buf())
254}
255
256fn existing_gitdir_abs_paths(
257 work_tree: &Path,
258 cfg: &ConfigFile,
259 except_name: &str,
260) -> Result<HashSet<PathBuf>> {
261 let mut set = HashSet::new();
262 let suffix = ".gitdir";
263 for e in &cfg.entries {
264 if !e.key.starts_with("submodule.") || !e.key.ends_with(suffix) {
265 continue;
266 }
267 let inner = &e.key["submodule.".len()..e.key.len() - suffix.len()];
268 if inner == except_name {
269 continue;
270 }
271 if let Some(v) = e.value.as_deref() {
272 let abs = canonical_abs(&resolve_gitdir_value(work_tree, v));
273 set.insert(abs);
274 }
275 }
276 Ok(set)
277}
278
279fn gitdir_conflicts_with_existing(
280 work_tree: &Path,
281 cfg: &ConfigFile,
282 abs_gitdir: &Path,
283 submodule_name: &str,
284) -> Result<bool> {
285 let canon = canonical_abs(abs_gitdir);
286 let existing = existing_gitdir_abs_paths(work_tree, cfg, submodule_name)?;
287 Ok(existing.contains(&canon))
288}
289
290fn ignore_case_from_config(git_dir: &Path) -> bool {
291 let config_path = git_dir.join("config");
292 let Ok(content) = fs::read_to_string(&config_path) else {
293 return false;
294 };
295 let mut in_core = false;
296 for line in content.lines() {
297 let trimmed = line.trim();
298 if trimmed.starts_with('[') {
299 in_core = trimmed.eq_ignore_ascii_case("[core]");
300 continue;
301 }
302 if in_core {
303 if let Some((k, v)) = trimmed.split_once('=') {
304 if k.trim().eq_ignore_ascii_case("ignorecase") {
305 return parse_bool(v.trim());
306 }
307 }
308 }
309 }
310 false
311}
312
313fn fold_case_git_path(s: &str) -> String {
314 s.to_ascii_lowercase()
315}
316
317fn check_casefolding_conflict(
318 proposed_abs: &Path,
319 submodule_name: &str,
320 suffixes_match: bool,
321 taken_folded: &HashSet<String>,
322) -> bool {
323 let last = last_modules_segment(proposed_abs).unwrap_or_default();
324 let folded_last = fold_case_git_path(&last);
325 let folded_name = fold_case_git_path(submodule_name);
326 if suffixes_match {
327 taken_folded.contains(&folded_last)
328 } else {
329 taken_folded.contains(&folded_name) || taken_folded.contains(&folded_last)
330 }
331}
332
333pub fn validate_legacy_submodule_git_dir(git_dir: &Path, submodule_name: &str) -> Result<()> {
335 let gd = git_dir.to_string_lossy();
336 let suffix = submodule_name;
337 if gd.len() <= suffix.len() {
338 return Err(Error::Config(
339 "submodule name not a suffix of git dir".into(),
340 ));
341 }
342 let cut = gd.len() - suffix.len();
343 if gd
344 .as_bytes()
345 .get(cut.wrapping_sub(1))
346 .is_none_or(|&b| b != b'/')
347 {
348 return Err(Error::Config(
349 "submodule name not a suffix of git dir".into(),
350 ));
351 }
352 if &gd[cut..] != suffix {
353 return Err(Error::Config(
354 "submodule name not a suffix of git dir".into(),
355 ));
356 }
357 if path_inside_other_gitdir(git_dir, submodule_name) {
358 return Err(Error::Config(
359 "submodule git dir inside another submodule git dir".into(),
360 ));
361 }
362 Ok(())
363}
364
365pub fn validate_encoded_submodule_git_dir(
367 work_tree: &Path,
368 cfg: &ConfigFile,
369 git_dir: &Path,
370 submodule_name: &str,
371 super_git_dir: &Path,
372) -> Result<()> {
373 let last = last_modules_segment(git_dir)
374 .ok_or_else(|| Error::Config("submodule gitdir missing /modules/ segment".into()))?;
375 if last.contains('/') {
376 return Err(Error::Config(
377 "encoded submodule gitdir must not contain '/' in module segment".into(),
378 ));
379 }
380 if is_git_directory(git_dir)
381 && gitdir_conflicts_with_existing(work_tree, cfg, git_dir, submodule_name)?
382 {
383 return Err(Error::Config(
384 "submodule gitdir conflicts with existing".into(),
385 ));
386 }
387 if cfg!(unix) && ignore_case_from_config(super_git_dir) {
388 let mut taken: HashSet<String> = HashSet::new();
389 let suffix = ".gitdir";
390 for e in &cfg.entries {
391 if !e.key.starts_with("submodule.") || !e.key.ends_with(suffix) {
392 continue;
393 }
394 let inner = &e.key["submodule.".len()..e.key.len() - suffix.len()];
395 if inner == submodule_name {
396 continue;
397 }
398 if let Some(v) = e.value.as_deref() {
399 let abs = canonical_abs(&resolve_gitdir_value(work_tree, v));
400 if let Some(seg) = last_modules_segment(&abs) {
401 taken.insert(fold_case_git_path(&seg));
402 }
403 }
404 }
405 let suffixes_match = last == submodule_name;
406 if check_casefolding_conflict(git_dir, submodule_name, suffixes_match, &taken) {
407 return Err(Error::Config(
408 "case-folding conflict for submodule gitdir".into(),
409 ));
410 }
411 }
412 Ok(())
413}
414
415fn repo_git_path_append(git_dir: &Path, tail: &str) -> PathBuf {
416 let mut buf = git_dir.to_path_buf();
417 if !tail.is_empty() {
418 buf.push(tail);
419 }
420 buf
421}
422
423pub fn hash_blob_sha1_hex(data: &[u8]) -> Result<String> {
425 Ok(hash::hash_object(crate::objects::HashAlgo::Sha1, ObjectKind::Blob, data)?.to_hex())
426}
427
428pub fn compute_default_submodule_gitdir(
430 work_tree: &Path,
431 git_dir: &Path,
432 cfg: &ConfigFile,
433 submodule_name: &str,
434) -> Result<String> {
435 let key = format!("submodule.{submodule_name}.gitdir");
436 for e in &cfg.entries {
437 if e.key == key {
438 if let Some(v) = e.value.as_deref() {
439 return Ok(v.to_string());
440 }
441 }
442 }
443
444 let try_set = |rel_under_git: &str| -> Option<String> {
445 let abs = repo_git_path_append(git_dir, rel_under_git);
446 if validate_encoded_submodule_git_dir(work_tree, cfg, &abs, submodule_name, git_dir)
447 .is_err()
448 {
449 return None;
450 }
451 Some(format!(".git/{}", rel_under_git.replace('\\', "/")))
452 };
453
454 let rel_plain = format!("modules/{}", submodule_name.replace('\\', "/"));
458 if !submodule_name.contains('/') && !submodule_name.contains('\\') {
459 if let Some(v) = try_set(&rel_plain) {
460 return Ok(v);
461 }
462 }
463
464 let enc = percent_encode(submodule_name, is_rfc3986_unreserved);
465 let rel_enc = format!("modules/{enc}");
466 if let Some(v) = try_set(&rel_enc) {
467 return Ok(v);
468 }
469
470 let enc_cf = percent_encode(submodule_name, is_casefolding_rfc3986_unreserved);
471 let rel_cf = format!("modules/{enc_cf}");
472 if let Some(v) = try_set(&rel_cf) {
473 return Ok(v);
474 }
475
476 for c in b'0'..=b'9' {
477 let rel = format!("modules/{}{}", enc, c as char);
478 if let Some(v) = try_set(&rel) {
479 return Ok(v);
480 }
481 let rel2 = format!("modules/{}{}", enc_cf, c as char);
482 if let Some(v) = try_set(&rel2) {
483 return Ok(v);
484 }
485 }
486
487 let hex = hash_blob_sha1_hex(submodule_name.as_bytes())?;
488 let rel_h = format!("modules/{hex}");
489 if let Some(v) = try_set(&rel_h) {
490 return Ok(v);
491 }
492
493 Err(Error::Config(
494 "failed to allocate submodule gitdir path".into(),
495 ))
496}
497
498pub fn ensure_submodule_gitdir_config(
500 work_tree: &Path,
501 git_dir: &Path,
502 cfg: &mut ConfigFile,
503 submodule_name: &str,
504) -> Result<String> {
505 let key = format!("submodule.{submodule_name}.gitdir");
506 if let Some(existing) = cfg.entries.iter().find(|e| e.key == key) {
507 if let Some(v) = existing.value.as_deref() {
508 return Ok(v.to_string());
509 }
510 }
511 let value = compute_default_submodule_gitdir(work_tree, git_dir, cfg, submodule_name)?;
512 cfg.set(&key, &value)?;
513 cfg.write()?;
514 Ok(value)
515}
516
517pub fn submodule_gitdir_filesystem_path(
519 work_tree: &Path,
520 git_dir: &Path,
521 cfg: &ConfigFile,
522 submodule_name: &str,
523) -> Result<PathBuf> {
524 if submodule_path_config_enabled(git_dir) {
525 let key = format!("submodule.{submodule_name}.gitdir");
526 let value = cfg
527 .entries
528 .iter()
529 .find(|e| e.key == key)
530 .and_then(|e| e.value.clone())
531 .ok_or_else(|| {
532 Error::Config(
533 format!(
534 "submodule.{submodule_name}.gitdir is not set (submodulePathConfig enabled)"
535 )
536 .into(),
537 )
538 })?;
539 Ok(resolve_gitdir_value(work_tree, &value))
540 } else {
541 Ok(git_dir.join("modules").join(submodule_name))
542 }
543}
544
545pub fn migrate_gitdir_configs(work_tree: &Path, git_dir: &Path) -> Result<()> {
547 let modules_root = git_dir.join("modules");
548 if !modules_root.is_dir() {
549 return Ok(());
550 }
551
552 let config_path = git_dir.join("config");
553 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
554 let mut cfg = ConfigFile::parse(&config_path, &content, ConfigScope::Local)?;
555
556 for entry in fs::read_dir(&modules_root).map_err(Error::Io)? {
557 let entry = entry.map_err(Error::Io)?;
558 let name = entry.file_name();
559 let name_str = name.to_string_lossy();
560 if name_str == "." || name_str == ".." {
561 continue;
562 }
563 let gd_path = modules_root.join(&name);
564 if !is_git_directory(&gd_path) {
565 continue;
566 }
567 let key = format!("submodule.{name_str}.gitdir");
568 if cfg.entries.iter().any(|e| e.key == key) {
569 continue;
570 }
571 let _ = ensure_submodule_gitdir_config(work_tree, git_dir, &mut cfg, &name_str)?;
572 }
573
574 let mut repo_version = 0u32;
575 if let Some(v) = cfg
576 .entries
577 .iter()
578 .find(|e| e.key == "core.repositoryformatversion")
579 {
580 if let Some(s) = v.value.as_deref() {
581 repo_version = s.parse().unwrap_or(0);
582 }
583 }
584 if repo_version == 0 {
585 cfg.set("core.repositoryformatversion", "1")?;
586 }
587 cfg.set("extensions.submodulePathConfig", "true")?;
588 cfg.write()?;
589 Ok(())
590}
591
592pub fn path_inside_indexed_submodule(index: &Index, new_path: &str) -> bool {
594 let new_norm = new_path.replace('\\', "/");
595 for e in &index.entries {
596 if e.mode != 0o160000 || e.stage() != 0 {
597 continue;
598 }
599 let ce = String::from_utf8_lossy(&e.path).replace('\\', "/");
600 let ce_len = ce.len();
601 if new_norm.len() <= ce_len {
602 continue;
603 }
604 if new_norm.as_bytes().get(ce_len) != Some(&b'/') {
605 continue;
606 }
607 if !new_norm.starts_with(&ce) {
608 continue;
609 }
610 if new_norm.len() == ce_len + 1 {
611 continue;
612 }
613 return true;
614 }
615 false
616}
617
618pub fn path_inside_registered_submodule(work_tree: &Path, new_path: &str) -> bool {
620 let gitmodules = work_tree.join(".gitmodules");
621 let Ok(content) = fs::read_to_string(&gitmodules) else {
622 return false;
623 };
624 let Ok(mf) = ConfigFile::parse(&gitmodules, &content, ConfigScope::Local) else {
625 return false;
626 };
627 let mut paths: Vec<String> = Vec::new();
628 for e in &mf.entries {
629 if e.key.starts_with("submodule.") && e.key.ends_with(".path") {
630 if let Some(p) = e.value.as_deref() {
631 paths.push(p.replace('\\', "/"));
632 }
633 }
634 }
635 let new_norm = new_path.replace('\\', "/");
636 for p in paths {
637 if new_norm == p || new_norm.starts_with(&format!("{p}/")) {
638 return true;
639 }
640 }
641 false
642}
643
644pub fn path_inside_registered_submodule_name(work_tree: &Path, new_path: &str) -> bool {
650 let gitmodules = work_tree.join(".gitmodules");
651 let Ok(content) = fs::read_to_string(&gitmodules) else {
652 return false;
653 };
654 let Ok(mf) = ConfigFile::parse(&gitmodules, &content, ConfigScope::Local) else {
655 return false;
656 };
657 let mut names: Vec<String> = Vec::new();
658 for e in &mf.entries {
659 if !e.key.starts_with("submodule.") {
660 continue;
661 }
662 let rest = &e.key["submodule.".len()..];
663 if let Some(last_dot) = rest.rfind('.') {
664 let name = rest[..last_dot].replace('\\', "/");
665 if !name.is_empty() {
666 names.push(name);
667 }
668 }
669 }
670 names.sort();
671 names.dedup();
672 let new_norm = new_path.replace('\\', "/");
673 for n in names {
674 if new_norm == n || new_norm.starts_with(&format!("{n}/")) {
675 return true;
676 }
677 }
678 false
679}
680
681pub fn die_path_inside_submodule_when_disabled(
685 git_dir: &Path,
686 work_tree: &Path,
687 new_path: &str,
688 index: Option<&Index>,
689) -> Result<()> {
690 if submodule_path_config_enabled(git_dir) {
691 return Ok(());
692 }
693 if path_inside_registered_submodule(work_tree, new_path) {
694 return Err(Error::Config(
695 "cannot add submodule: path inside existing submodule".into(),
696 ));
697 }
698 if let Some(ix) = index {
699 if path_inside_indexed_submodule(ix, new_path) {
700 return Err(Error::Config(
701 "cannot add submodule: path inside existing submodule".into(),
702 ));
703 }
704 }
705 Ok(())
706}
707
708pub fn set_submodule_repo_worktree(modules_dir: &Path, sub_worktree: &Path) -> Result<()> {
713 let wt_rel = pathdiff_relative(modules_dir, sub_worktree);
714 let config_path = modules_dir.join("config");
715 let mut file = match ConfigFile::from_path(&config_path, ConfigScope::Local)? {
716 Some(f) => f,
717 None => ConfigFile::parse(&config_path, "", ConfigScope::Local)?,
718 };
719 file.set("core.worktree", &wt_rel)?;
720 file.write()?;
721 Ok(())
722}
723
724pub fn write_submodule_gitfile(sub_worktree: &Path, modules_dir: &Path) -> Result<()> {
726 let rel = pathdiff_relative(sub_worktree, modules_dir);
727 let line = format!("gitdir: {rel}\n");
728 fs::write(sub_worktree.join(".git"), line).map_err(Error::Io)?;
729 Ok(())
730}
731
732fn pathdiff_relative(from: &Path, to: &Path) -> String {
733 let from_c = from
734 .canonicalize_simplified()
735 .unwrap_or_else(|_| from.to_path_buf());
736 let to_c = to
737 .canonicalize_simplified()
738 .unwrap_or_else(|_| to.to_path_buf());
739 let from_comp: Vec<Component<'_>> = from_c.components().collect();
740 let to_comp: Vec<Component<'_>> = to_c.components().collect();
741 let mut i = 0usize;
742 while i < from_comp.len() && i < to_comp.len() && from_comp[i] == to_comp[i] {
743 i += 1;
744 }
745 let mut out = PathBuf::new();
746 for _ in i..from_comp.len() {
747 out.push("..");
748 }
749 for c in &to_comp[i..] {
750 out.push(c.as_os_str());
751 }
752 out.to_string_lossy().replace('\\', "/")
753}
754
755pub fn connect_submodule_work_tree_and_git_dir(
757 work_tree: &Path,
758 super_git_dir: &Path,
759 cfg: &ConfigFile,
760 submodule_name: &str,
761 sub_worktree: &Path,
762) -> Result<()> {
763 let modules_dir =
764 submodule_gitdir_filesystem_path(work_tree, super_git_dir, cfg, submodule_name)?;
765 write_submodule_gitfile(sub_worktree, &modules_dir)?;
766 set_submodule_repo_worktree(&modules_dir, sub_worktree)?;
767 Ok(())
768}
769
770pub fn init_submodule_head_from_gitlink(modules_dir: &Path, oid_hex: &str) -> Result<()> {
772 let head = modules_dir.join("HEAD");
773 if head.exists() {
774 return Ok(());
775 }
776 let obj_dir = modules_dir.join("objects");
777 if !obj_dir.is_dir() {
778 return Ok(());
779 }
780 let odb = Odb::new(&obj_dir);
781 let oid = ObjectId::from_hex(oid_hex)?;
782 let obj = odb.read(&oid)?;
783 if obj.kind != ObjectKind::Commit {
784 return Ok(());
785 }
786 fs::write(&head, format!("{oid_hex}\n")).map_err(Error::Io)?;
787 Ok(())
788}
789
790#[cfg(test)]
791mod submodule_modules_git_dir_tests {
792 use super::submodule_modules_git_dir;
793 use std::path::Path;
794
795 #[test]
796 fn nested_path_under_single_modules_prefix() {
797 let super_git = Path::new("/repo/.git");
798 assert_eq!(
799 submodule_modules_git_dir(super_git, "sub1/sub2"),
800 Path::new("/repo/.git/modules/sub1/sub2")
801 );
802 assert_eq!(
803 submodule_modules_git_dir(super_git, r"..\foo"),
804 Path::new("/repo/.git/modules/../foo")
805 );
806 }
807
808 #[test]
809 fn single_segment_one_modules_join() {
810 let super_git = Path::new("/repo/.git");
811 assert_eq!(
812 submodule_modules_git_dir(super_git, "sub1"),
813 Path::new("/repo/.git/modules/sub1")
814 );
815 }
816}