git_odb/alternate/
mod.rs

1//! A file with directories of other git object databases to use when reading objects.
2//!
3//! This inherently makes alternates read-only.
4//!
5//! An alternate file in `<git-dir>/info/alternates` can look as follows:
6//!
7//! ```text
8//! # a comment, empty lines are also allowed
9//! # relative paths resolve relative to the parent git repository
10//! ../path/relative/to/repo/.git
11//! /absolute/path/to/repo/.git
12//!
13//! "/a/ansi-c-quoted/path/with/tabs\t/.git"
14//!
15//! # each .git directory should indeed be a directory, and not a file
16//! ```
17//!
18//! Based on the [canonical implementation](https://github.com/git/git/blob/master/sha1-file.c#L598:L609).
19use std::{fs, io, path::PathBuf};
20
21use git_path::realpath::MAX_SYMLINKS;
22
23///
24pub mod parse;
25
26/// Returned by [`resolve()`]
27#[derive(thiserror::Error, Debug)]
28#[allow(missing_docs)]
29pub enum Error {
30    #[error(transparent)]
31    Io(#[from] io::Error),
32    #[error(transparent)]
33    Realpath(#[from] git_path::realpath::Error),
34    #[error(transparent)]
35    Parse(#[from] parse::Error),
36    #[error("Alternates form a cycle: {} -> {}", .0.iter().map(|p| format!("'{}'", p.display())).collect::<Vec<_>>().join(" -> "), .0.first().expect("more than one directories").display())]
37    Cycle(Vec<PathBuf>),
38}
39
40/// Given an `objects_directory`, try to resolve alternate object directories possibly located in the
41/// `./info/alternates` file into canonical paths and resolve relative paths with the help of the `current_dir`.
42/// If no alternate object database was resolved, the resulting `Vec` is empty (it is not an error
43/// if there are no alternates).
44/// It is an error once a repository is seen again as it would lead to a cycle.
45pub fn resolve(
46    objects_directory: impl Into<PathBuf>,
47    current_dir: impl AsRef<std::path::Path>,
48) -> Result<Vec<PathBuf>, Error> {
49    let relative_base = objects_directory.into();
50    let mut dirs = vec![(0, relative_base.clone())];
51    let mut out = Vec::new();
52    let cwd = current_dir.as_ref();
53    let mut seen = vec![git_path::realpath_opts(&relative_base, cwd, MAX_SYMLINKS)?];
54    while let Some((depth, dir)) = dirs.pop() {
55        match fs::read(dir.join("info").join("alternates")) {
56            Ok(input) => {
57                for path in parse::content(&input)?.into_iter() {
58                    let path = relative_base.join(path);
59                    let path_canonicalized = git_path::realpath_opts(&path, cwd, MAX_SYMLINKS)?;
60                    if seen.contains(&path_canonicalized) {
61                        return Err(Error::Cycle(seen));
62                    }
63                    seen.push(path_canonicalized);
64                    dirs.push((depth + 1, path));
65                }
66            }
67            Err(err) if err.kind() == io::ErrorKind::NotFound => {}
68            Err(err) => return Err(err.into()),
69        };
70        if depth != 0 {
71            out.push(dir);
72        }
73    }
74    Ok(out)
75}