use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use thiserror::Error;
use super::discover::has_marker;
use super::{OpenError, Repository};
use crate::refs::RefName;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum InitKind {
Worktree,
Bare,
}
#[derive(Clone, Debug)]
pub struct InitOptions {
kind: InitKind,
format: crate::ObjectFormat,
backend: crate::refs::Backend,
initial_branch: RefName,
}
impl InitOptions {
pub fn new(kind: InitKind) -> Self {
Self {
kind,
format: crate::ObjectFormat::Sha1,
backend: crate::refs::Backend::Files,
initial_branch: RefName::new("refs/heads/master").expect("valid branch name"),
}
}
#[must_use]
pub fn with_object_format(mut self, format: crate::ObjectFormat) -> Self {
self.format = format;
self
}
#[must_use]
pub fn with_reference_backend(mut self, backend: crate::refs::Backend) -> Self {
self.backend = backend;
self
}
pub fn with_initial_branch(mut self, name: &str) -> Result<Self, InitError> {
self.initial_branch = RefName::new(format!("refs/heads/{name}"))
.map_err(|_| InitError::InvalidBranch(name.to_owned()))?;
Ok(self)
}
pub fn defaults_from(self, config: &crate::Config) -> Result<Self, InitError> {
let Some(name) = config.value("init", None, "defaultbranch") else {
return Ok(self);
};
let name = name.unwrap_or_default();
let invalid = || InitError::InvalidBranch(String::from_utf8_lossy(name).into_owned());
let name = std::str::from_utf8(name).map_err(|_| invalid())?;
if name.is_empty() {
return Err(invalid());
}
self.with_initial_branch(name)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum InitError {
#[error("initialization destination already exists: {0}")]
AlreadyExists(PathBuf),
#[error("invalid initial branch name: {0}")]
InvalidBranch(String),
#[error("cannot initialize {path}")]
Io {
path: PathBuf,
#[source]
source: io::Error,
},
#[error(transparent)]
Reftable(#[from] crate::refs::reftable::Error),
#[error(transparent)]
Open(#[from] OpenError),
}
impl Repository {
pub(crate) fn init_reserved_clone(
path: &Path,
kind: InitKind,
format: crate::ObjectFormat,
) -> Result<Self, InitError> {
let git_dir = match kind {
InitKind::Bare => path.to_path_buf(),
InitKind::Worktree => {
let git_dir = path.join(".git");
create_directory(&git_dir)?;
git_dir
}
};
populate_minimal(&git_dir, kind, format)?;
Ok(Self::open(path)?)
}
pub fn init(
format: crate::ObjectFormat,
path: impl AsRef<Path>,
kind: InitKind,
) -> Result<Self, InitError> {
Self::init_with_backend(format, path, kind, crate::refs::Backend::Files)
}
pub fn init_with_options(
path: impl AsRef<Path>,
options: &InitOptions,
) -> Result<Self, InitError> {
Self::init_inner(path.as_ref(), options)
}
pub fn init_with_backend(
format: crate::ObjectFormat,
path: impl AsRef<Path>,
kind: InitKind,
backend: crate::refs::Backend,
) -> Result<Self, InitError> {
let mut options = InitOptions::new(kind)
.with_object_format(format)
.with_reference_backend(backend);
options.initial_branch = initial_branch();
Self::init_inner(path.as_ref(), &options)
}
fn init_inner(path: &Path, options: &InitOptions) -> Result<Self, InitError> {
let InitOptions {
kind,
format,
backend,
initial_branch,
} = options.clone();
if has_marker(path)? {
Self::open(path)?;
return Err(InitError::AlreadyExists(path.into()));
}
let git_dir = match kind {
InitKind::Bare => {
create_directory(path)?;
path.to_path_buf()
}
InitKind::Worktree => {
match fs::metadata(path) {
Ok(metadata) if metadata.is_dir() => (),
Ok(_) => return Err(InitError::AlreadyExists(path.into())),
Err(error) if error.kind() == io::ErrorKind::NotFound => {
create_directory(path)?
}
Err(source) => return Err(init_io(path, source)),
}
let git_dir = path.join(".git");
create_directory(&git_dir)?;
git_dir
}
};
populate(&git_dir, kind, format, backend, &initial_branch)?;
Ok(Self::open(path)?)
}
}
fn populate_minimal(
git_dir: &Path,
kind: InitKind,
format: crate::ObjectFormat,
) -> Result<(), InitError> {
for name in [
"objects",
"objects/info",
"objects/pack",
"refs",
"refs/heads",
"refs/tags",
] {
create_directory(&git_dir.join(name))?;
}
create_file(&git_dir.join("config"), &initial_config(kind, format))?;
let mut head = b"ref: ".to_vec();
head.extend_from_slice(initial_branch().as_bytes());
head.push(b'\n');
create_file(&git_dir.join("HEAD"), &head)
}
fn populate(
git_dir: &Path,
kind: InitKind,
format: crate::ObjectFormat,
backend: crate::refs::Backend,
initial_branch: &RefName,
) -> Result<(), InitError> {
if backend == crate::refs::Backend::Reftable {
return populate_reftable(git_dir, kind, format, initial_branch);
}
for name in [
"objects",
"objects/info",
"objects/pack",
"refs",
"refs/heads",
"refs/tags",
"hooks",
"info",
] {
create_directory(&git_dir.join(name))?;
}
create_file(&git_dir.join("info/exclude"), b"")?;
create_file(
&git_dir.join("config"),
&git_config(git_dir, kind, format, false),
)?;
let mut head = b"ref: ".to_vec();
head.extend_from_slice(initial_branch.as_bytes());
head.push(b'\n');
create_file(&git_dir.join("HEAD"), &head)
}
fn git_config(
git_dir: &Path,
kind: InitKind,
format: crate::ObjectFormat,
reftable: bool,
) -> Vec<u8> {
let mut config = String::new();
let extensions = format == crate::ObjectFormat::Sha256 || reftable;
if extensions {
config.push_str("[extensions]\n");
if format == crate::ObjectFormat::Sha256 {
config.push_str("\tobjectformat = sha256\n");
}
if reftable {
config.push_str("\trefStorage = reftable\n");
}
}
config.push_str("[core]\n");
config.push_str(&format!(
"\trepositoryformatversion = {}\n",
u8::from(extensions)
));
config.push_str(&format!("\tfilemode = {}\n", probe_filemode(git_dir)));
config.push_str(&format!("\tbare = {}\n", kind == InitKind::Bare));
if kind == InitKind::Worktree {
config.push_str("\tlogallrefupdates = true\n");
}
if probe_ignorecase(git_dir) {
config.push_str("\tignorecase = true\n");
}
if cfg!(target_os = "macos") {
config.push_str("\tprecomposeunicode = true\n");
}
config.into_bytes()
}
fn probe_filemode(git_dir: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let probe = git_dir.join("config.filemode-probe");
let result = (|| -> io::Result<bool> {
fs::write(&probe, b"")?;
let mut permissions = fs::metadata(&probe)?.permissions();
permissions.set_mode(permissions.mode() ^ 0o100);
let expected = permissions.mode();
fs::set_permissions(&probe, permissions)?;
Ok(fs::metadata(&probe)?.permissions().mode() == expected)
})();
let _ = fs::remove_file(&probe);
result.unwrap_or(false)
}
#[cfg(not(unix))]
{
let _ = git_dir;
false
}
}
fn probe_ignorecase(git_dir: &Path) -> bool {
git_dir.join("OBJECTS").exists()
}
fn populate_reftable(
git_dir: &Path,
kind: InitKind,
format: crate::ObjectFormat,
initial_branch: &RefName,
) -> Result<(), InitError> {
use crate::refs::Target;
use crate::refs::reftable::{Limits, RefRecord, Table, table_name};
for name in [
"objects",
"objects/info",
"objects/pack",
"refs",
"reftable",
] {
create_directory(&git_dir.join(name))?;
}
let config = format!(
"[core]\n\trepositoryformatversion = 1\n\tbare = {}\n[extensions]\n\tobjectformat = {}\n\trefStorage = reftable\n",
kind == InitKind::Bare,
if format == crate::ObjectFormat::Sha1 {
"sha1"
} else {
"sha256"
}
);
create_file(&git_dir.join("config"), config.as_bytes())?;
let table = Table {
format,
min_update_index: 1,
max_update_index: 1,
references: vec![RefRecord {
name: RefName::HEAD.into(),
update_index: 1,
target: Some(Target::Symbolic(initial_branch.clone())),
peeled: None,
}],
logs: Vec::new(),
};
let name = table_name(1, 1);
create_file(
&git_dir.join("reftable").join(&name),
&table.encode(Limits::default())?,
)?;
create_file(
&git_dir.join("reftable/tables.list"),
format!("{name}\n").as_bytes(),
)?;
create_file(
&git_dir.join("refs/heads"),
b"this repository uses the reftable format\n",
)?;
create_file(&git_dir.join("HEAD"), b"ref: refs/heads/.invalid\n")
}
pub(crate) fn initial_config(kind: InitKind, format: crate::ObjectFormat) -> Vec<u8> {
format_config(kind, format)
}
fn format_config(kind: InitKind, format: crate::ObjectFormat) -> Vec<u8> {
if format == crate::ObjectFormat::Sha256 {
return format!("[core]\n\trepositoryformatversion = 1\n\tbare = {}\n[extensions]\n\tobjectformat = sha256\n", kind == InitKind::Bare).into_bytes();
}
format!(
"[core]\n\trepositoryformatversion = 0\n\tbare = {}\n",
kind == InitKind::Bare
)
.into_bytes()
}
pub(crate) fn initial_branch() -> RefName {
RefName::new("refs/heads/main").expect("fixed initial branch")
}
fn create_directory(path: &Path) -> Result<(), InitError> {
fs::create_dir(path).map_err(|source| {
if source.kind() == io::ErrorKind::AlreadyExists {
InitError::AlreadyExists(path.into())
} else {
init_io(path, source)
}
})
}
fn create_file(path: &Path, bytes: &[u8]) -> Result<(), InitError> {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|source| init_io(path, source))?;
file.write_all(bytes)
.map_err(|source| init_io(path, source))
}
fn init_io(path: &Path, source: io::Error) -> InitError {
InitError::Io {
path: path.into(),
source,
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::ordinary(InitKind::Worktree, false)]
#[case::bare(InitKind::Bare, true)]
fn creates_expected_layout(#[case] kind: InitKind, #[case] bare: bool) {
let root = tempfile::tempdir().unwrap();
let repo =
Repository::init(crate::ObjectFormat::Sha1, root.path().join("repo"), kind).unwrap();
assert_eq!(repo.worktree().is_none(), bare);
assert_eq!(repo.is_bare(), bare);
assert_eq!(repo.format_version(), 0);
assert_eq!(
fs::read(repo.git_dir().join("HEAD")).unwrap(),
b"ref: refs/heads/main\n"
);
assert!(!repo.git_dir().join("refs/heads/main").exists());
}
#[rstest]
#[case::absent(b"".as_slice(), Some("ref: refs/heads/master\n"))]
#[case::configured(b"[init]\n\tdefaultBranch = trunk\n".as_slice(), Some("ref: refs/heads/trunk\n"))]
#[case::invalid(b"[init]\n\tdefaultBranch = bad..name\n".as_slice(), None)]
#[case::empty(b"[init]\n\tdefaultBranch =\n".as_slice(), None)]
#[case::implicit(b"[init]\n\tdefaultBranch\n".as_slice(), None)]
fn default_branch_follows_configuration(#[case] config: &[u8], #[case] head: Option<&str>) {
let root = tempfile::tempdir().unwrap();
let config = crate::Config::parse(config).unwrap();
let options = InitOptions::new(InitKind::Bare).defaults_from(&config);
let head_bytes = options.ok().map(|options| {
let repo = Repository::init_with_options(root.path().join("repo"), &options).unwrap();
fs::read(repo.git_dir().join("HEAD")).unwrap()
});
assert_eq!(head_bytes, head.map(|head| head.as_bytes().to_vec()));
}
#[test]
fn preserves_unrelated_worktree_files() {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("keep"), b"original").unwrap();
Repository::init(crate::ObjectFormat::Sha1, root.path(), InitKind::Worktree).unwrap();
assert_eq!(fs::read(root.path().join("keep")).unwrap(), b"original");
}
#[test]
fn refuses_existing_empty_bare_destination() {
let root = tempfile::tempdir().unwrap();
assert!(matches!(
Repository::init(crate::ObjectFormat::Sha1, root.path(), InitKind::Bare),
Err(InitError::AlreadyExists(_))
));
assert_eq!(fs::read_dir(root.path()).unwrap().count(), 0);
}
#[rstest]
#[case::file(".git")]
#[case::head("HEAD")]
fn refuses_existing_marker_without_writes(#[case] marker: &str) {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join(marker), b"keep").unwrap();
assert!(
Repository::init(crate::ObjectFormat::Sha1, root.path(), InitKind::Worktree).is_err()
);
assert_eq!(fs::read(root.path().join(marker)).unwrap(), b"keep");
assert_eq!(fs::read_dir(root.path()).unwrap().count(), 1);
}
#[test]
fn missing_parent_is_not_created() {
let root = tempfile::tempdir().unwrap();
assert!(
Repository::init(
crate::ObjectFormat::Sha1,
root.path().join("missing/repo"),
InitKind::Bare
)
.is_err()
);
assert_eq!(fs::read_dir(root.path()).unwrap().count(), 0);
}
#[cfg(unix)]
#[test]
fn dangling_marker_is_not_replaced() {
let root = tempfile::tempdir().unwrap();
std::os::unix::fs::symlink("missing", root.path().join(".git")).unwrap();
assert!(
Repository::init(crate::ObjectFormat::Sha1, root.path(), InitKind::Worktree).is_err()
);
assert_eq!(
fs::read_link(root.path().join(".git")).unwrap(),
Path::new("missing")
);
assert_eq!(fs::read_dir(root.path()).unwrap().count(), 1);
}
#[test]
fn partial_population_failure_preserves_files_and_omits_head() {
let root = tempfile::tempdir().unwrap();
fs::write(root.path().join("config"), b"another writer").unwrap();
assert!(
populate(
root.path(),
InitKind::Bare,
crate::ObjectFormat::Sha1,
crate::refs::Backend::Files,
&initial_branch(),
)
.is_err()
);
assert_eq!(
fs::read(root.path().join("config")).unwrap(),
b"another writer"
);
assert!(root.path().join("objects").is_dir());
assert!(!root.path().join("HEAD").exists());
}
#[test]
fn concurrent_initializers_have_one_winner() {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("repo");
let barrier = std::sync::Barrier::new(2);
let (first, second) = std::thread::scope(|scope| {
let first = scope.spawn(|| {
barrier.wait();
Repository::init(crate::ObjectFormat::Sha1, &path, InitKind::Bare)
});
barrier.wait();
let second = Repository::init(crate::ObjectFormat::Sha1, &path, InitKind::Bare);
(first.join().unwrap(), second)
});
assert_ne!(first.is_ok(), second.is_ok());
let repo = Repository::open(&path).unwrap();
assert_eq!(
fs::read(repo.git_dir().join("HEAD")).unwrap(),
b"ref: refs/heads/main\n"
);
}
#[test]
fn exclusive_file_creation_preserves_existing_bytes() {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("config");
fs::write(&path, b"keep").unwrap();
assert!(create_file(&path, b"replace").is_err());
assert_eq!(fs::read(path).unwrap(), b"keep");
}
}