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)]
pub enum InitKind {
Worktree,
Bare,
}
#[derive(Debug, Error)]
pub enum InitError {
#[error("initialization destination already exists: {0}")]
AlreadyExists(PathBuf),
#[error("cannot initialize {path}: {source}")]
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) -> 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(
&git_dir,
kind,
crate::ObjectFormat::Sha1,
crate::refs::Backend::Files,
)?;
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_backend(
format: crate::ObjectFormat,
path: impl AsRef<Path>,
kind: InitKind,
backend: crate::refs::Backend,
) -> Result<Self, InitError> {
let path = path.as_ref();
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)?;
Ok(Self::open(path)?)
}
}
fn populate(
git_dir: &Path,
kind: InitKind,
format: crate::ObjectFormat,
backend: crate::refs::Backend,
) -> Result<(), InitError> {
if backend == crate::refs::Backend::Reftable {
return populate_reftable(git_dir, kind, format);
}
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"), &format_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_reftable(
git_dir: &Path,
kind: InitKind,
format: crate::ObjectFormat,
) -> Result<(), InitError> {
use crate::refs::Target;
use crate::refs::reftable::{Limits, RefRecord, Table};
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::new(b"HEAD").unwrap().into(),
update_index: 1,
target: Some(Target::Symbolic(initial_branch())),
peeled: None,
}],
logs: Vec::new(),
};
create_file(
&git_dir.join("reftable/initial.ref"),
&table.encode(Limits::default())?,
)?;
create_file(&git_dir.join("reftable/tables.list"), b"initial.ref\n")?;
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) -> Vec<u8> {
format_config(kind, crate::ObjectFormat::Sha1)
}
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());
}
#[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
)
.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");
}
}