328 lines · 11.7 KB · Rust
RawDownload
use std::{    fs::{self, OpenOptions},    io::Write,    path::{Path, PathBuf},}; use gix_discover::DOT_GIT_DIR; /// The error used in [`into()`].#[derive(Debug, thiserror::Error)]#[expect(missing_docs)]pub enum Error {    #[error("Could not obtain the current directory")]    CurrentDir(#[from] std::io::Error),    #[error("Could not open data at '{}'", .path.display())]    IoOpen { source: std::io::Error, path: PathBuf },    #[error("Could not write data at '{}'", .path.display())]    IoWrite { source: std::io::Error, path: PathBuf },    #[error("Refusing to initialize the existing '{}' directory", .path.display())]    DirectoryExists { path: PathBuf },    #[error("Refusing to initialize the non-empty directory as '{}'", .path.display())]    DirectoryNotEmpty { path: PathBuf },    #[error("Could not create directory at '{}'", .path.display())]    CreateDirectory { source: std::io::Error, path: PathBuf },    #[error(transparent)]    Span(#[from] gix_config::parse::span::Error),    #[error(transparent)]    ConfigValue(#[from] gix_config::file::section::value::Error),} /// The kind of repository to create.#[derive(Debug, Copy, Clone)]pub enum Kind {    /// An empty repository with a `.git` folder, setup to contain files in its worktree.    WithWorktree,    /// A bare repository without a worktree.    Bare,} const TPL_INFO_EXCLUDE: &[u8] = include_bytes!("assets/init/info/exclude");const TPL_HOOKS_APPLYPATCH_MSG: &[u8] = include_bytes!("assets/init/hooks/applypatch-msg.sample");const TPL_HOOKS_COMMIT_MSG: &[u8] = include_bytes!("assets/init/hooks/commit-msg.sample");const TPL_HOOKS_FSMONITOR_WATCHMAN: &[u8] = include_bytes!("assets/init/hooks/fsmonitor-watchman.sample");const TPL_HOOKS_POST_UPDATE: &[u8] = include_bytes!("assets/init/hooks/post-update.sample");const TPL_HOOKS_PRE_APPLYPATCH: &[u8] = include_bytes!("assets/init/hooks/pre-applypatch.sample");const TPL_HOOKS_PRE_COMMIT: &[u8] = include_bytes!("assets/init/hooks/pre-commit.sample");const TPL_HOOKS_PRE_MERGE_COMMIT: &[u8] = include_bytes!("assets/init/hooks/pre-merge-commit.sample");const TPL_HOOKS_PRE_PUSH: &[u8] = include_bytes!("assets/init/hooks/pre-push.sample");const TPL_HOOKS_PRE_REBASE: &[u8] = include_bytes!("assets/init/hooks/pre-rebase.sample");const TPL_HOOKS_PREPARE_COMMIT_MSG: &[u8] = include_bytes!("assets/init/hooks/prepare-commit-msg.sample");const TPL_HOOKS_DOCS_URL: &[u8] = include_bytes!("assets/init/hooks/docs.url");const TPL_DESCRIPTION: &[u8] = include_bytes!("assets/init/description");const TPL_HEAD: &[u8] = include_bytes!("assets/init/HEAD"); struct PathCursor<'a>(&'a mut PathBuf); struct NewDir<'a>(&'a mut PathBuf); impl PathCursor<'_> {    fn at(&mut self, component: &str) -> &Path {        self.0.push(component);        self.0.as_path()    }} impl NewDir<'_> {    fn at(self, component: &str) -> Result<Self, Error> {        self.0.push(component);        create_dir(self.0)?;        Ok(self)    }    fn as_mut(&mut self) -> &mut PathBuf {        self.0    }} impl Drop for NewDir<'_> {    fn drop(&mut self) {        self.0.pop();    }} impl Drop for PathCursor<'_> {    fn drop(&mut self) {        self.0.pop();    }} fn write_file(data: &[u8], path: &Path) -> Result<(), Error> {    let mut file = OpenOptions::new()        .write(true)        .create(true)        .truncate(true)        .append(false)        .open(path)        .map_err(|e| Error::IoOpen {            source: e,            path: path.to_owned(),        })?;    file.write_all(data).map_err(|e| Error::IoWrite {        source: e,        path: path.to_owned(),    })} fn create_dir(p: &Path) -> Result<(), Error> {    fs::create_dir_all(p).map_err(|e| Error::CreateDirectory {        source: e,        path: p.to_owned(),    })} /// Options for use in [`into()`];#[derive(Copy, Clone)]pub struct Options {    /// Control whether the destination directory must be empty when creating a repository with a worktree.    ///    /// - `None` (default): initialize like Git and allow a non-empty destination directory, as long as no `.git`    ///   directory is present.    /// - `Some(true)`: require an empty destination directory.    /// - `Some(false)`: explicitly allow initialization into a non-empty destination directory (still requires that no    ///   `.git` directory is present).    ///    /// For clones, checkout failure cleanup is based on whether the destination was already present and non-empty before    /// initialization began, not on this option alone. In particular, if the destination was empty or had to be created,    /// cleanup may remove the entire destination, including the created `.git` directory. Preservation of the destination    /// for inspection or manual cleanup is only guaranteed when the destination was non-empty before the clone started.    ///    /// Bare repositories always require an empty destination, regardless of this option.    pub destination_must_be_empty: Option<bool>,    /// If set, use these filesystem capabilities to populate the respective git-config fields.    /// If `None`, the directory will be probed.    pub fs_capabilities: Option<gix_fs::Capabilities>,    /// If set to `Some(Sha256)`, write `extensions.objectFormat=sha256`.    /// Otherwise, create a repository without an explicit object-format extension,    /// which is interpreted as legacy SHA-1.    pub object_hash: Option<gix_hash::Kind>,} impl Default for Options {    fn default() -> Self {        Options {            destination_must_be_empty: None,            fs_capabilities: None,            object_hash: default_object_hash(),        }    }} fn default_object_hash() -> Option<gix_hash::Kind> {    #[cfg(feature = "sha1")]    {        None    }    #[cfg(all(not(feature = "sha1"), feature = "sha256"))]    {        Some(gix_hash::Kind::Sha256)    }    #[cfg(all(not(feature = "sha1"), not(feature = "sha256")))]    {        unreachable!("hash support features are validated by gix-hash")    }} /// Create a new `.git` repository of `kind` within the possibly non-existing `directory`/// and return its path./// Note that this is a simple template-based initialization routine which should be accompanied with additional corrections/// to respect git configuration, which is accomplished by [its callers][crate::ThreadSafeRepository::init_opts()]/// that return a [Repository][crate::Repository].pub fn into(    directory: impl Into<PathBuf>,    kind: Kind,    Options {        fs_capabilities,        destination_must_be_empty,        object_hash,    }: Options,) -> Result<gix_discover::repository::Path, Error> {    let mut dot_git = directory.into();    let bare = matches!(kind, Kind::Bare);     if bare || destination_must_be_empty.unwrap_or(false) {        let num_entries_in_dot_git = fs::read_dir(&dot_git)            .or_else(|err| {                if err.kind() == std::io::ErrorKind::NotFound {                    fs::create_dir(&dot_git).and_then(|_| fs::read_dir(&dot_git))                } else {                    Err(err)                }            })            .map_err(|err| Error::IoOpen {                source: err,                path: dot_git.clone(),            })?            .count();        if num_entries_in_dot_git != 0 {            return Err(Error::DirectoryNotEmpty { path: dot_git });        }    }     if !bare {        dot_git.push(DOT_GIT_DIR);         if dot_git.is_dir() {            return Err(Error::DirectoryExists { path: dot_git });        }    }    create_dir(&dot_git)?;     {        let mut cursor = NewDir(&mut dot_git).at("info")?;        write_file(TPL_INFO_EXCLUDE, PathCursor(cursor.as_mut()).at("exclude"))?;    }     {        let mut cursor = NewDir(&mut dot_git).at("hooks")?;        for (tpl, filename) in &[            (TPL_HOOKS_DOCS_URL, "docs.url"),            (TPL_HOOKS_PREPARE_COMMIT_MSG, "prepare-commit-msg.sample"),            (TPL_HOOKS_PRE_REBASE, "pre-rebase.sample"),            (TPL_HOOKS_PRE_PUSH, "pre-push.sample"),            (TPL_HOOKS_PRE_COMMIT, "pre-commit.sample"),            (TPL_HOOKS_PRE_MERGE_COMMIT, "pre-merge-commit.sample"),            (TPL_HOOKS_PRE_APPLYPATCH, "pre-applypatch.sample"),            (TPL_HOOKS_POST_UPDATE, "post-update.sample"),            (TPL_HOOKS_FSMONITOR_WATCHMAN, "fsmonitor-watchman.sample"),            (TPL_HOOKS_COMMIT_MSG, "commit-msg.sample"),            (TPL_HOOKS_APPLYPATCH_MSG, "applypatch-msg.sample"),        ] {            write_file(tpl, PathCursor(cursor.as_mut()).at(filename))?;        }    }     {        let mut cursor = NewDir(&mut dot_git).at("objects")?;        create_dir(PathCursor(cursor.as_mut()).at("info"))?;        create_dir(PathCursor(cursor.as_mut()).at("pack"))?;    }     {        let mut cursor = NewDir(&mut dot_git).at("refs")?;        create_dir(PathCursor(cursor.as_mut()).at("heads"))?;        create_dir(PathCursor(cursor.as_mut()).at("tags"))?;    }     for (tpl, filename) in &[(TPL_HEAD, "HEAD"), (TPL_DESCRIPTION, "description")] {        write_file(tpl, PathCursor(&mut dot_git).at(filename))?;    }     let caps = {        let (mut config_file, config_path) = {            let mut cursor = PathCursor(&mut dot_git);            let config_path = cursor.at("config");            (fs::File::create(config_path)?, config_path.to_owned())        };        let mut config = gix_config::File::default();        let caps = {            let caps = fs_capabilities.unwrap_or_else(|| gix_fs::Capabilities::probe(&dot_git));            let mut core = config.new_section("core", None).expect("valid section name");             core.push("filemode", bool(caps.executable_bit))?;            core.push("bare", bool(bare))?;            core.push("logallrefupdates", bool(!bare))?;            core.push("symlinks", bool(caps.symlink))?;            core.push("ignorecase", bool(caps.ignore_case))?;            core.push("precomposeunicode", bool(caps.precompose_unicode))?;             match object_hash {                #[cfg(feature = "sha256")]                Some(gix_hash::Kind::Sha256) => {                    core.push("repositoryformatversion", "1")?;                     let mut extensions = config.new_section("extensions", None).expect("valid section name");                    extensions.push("objectformat", gix_hash::Kind::Sha256.to_string())?;                }                _ => {                    core.push("repositoryformatversion", "0")?;                }            }             caps        };        config_file            .write_all(&config.to_bstring())            .map_err(|err| Error::IoWrite {                source: err,                path: config_path,            })?;        caps    };     Ok(gix_discover::repository::Path::from_dot_git_dir(        dot_git,        if bare {            gix_discover::repository::Kind::PossiblyBare        } else {            gix_discover::repository::Kind::WorkTree { linked_git_dir: None }        },        &gix_fs::current_dir(caps.precompose_unicode)?,    )    .expect("by now the `dot_git` dir is valid as we have accessed it"))} fn bool(v: bool) -> &'static str {    match v {        true => "true",        false => "false",    }} #[cfg(test)]mod tests {    #[test]    fn default_object_hash_matches_available_hash_support() {        let object_hash = super::Options::default().object_hash;        #[cfg(feature = "sha1")]        assert_eq!(            object_hash, None,            "SHA1-capable builds keep Git's implicit legacy object format"        );        #[cfg(all(not(feature = "sha1"), feature = "sha256"))]        assert_eq!(            object_hash,            Some(gix_hash::Kind::Sha256),            "SHA256-only builds must initialize repositories that can be reopened"        );    }}