use super::*;
use crate::attributes::*;
use crate::filter::*;
use crate::index::*;
use crate::index_io::*;
use crate::status::*;
use crate::types_admin::*;
pub fn deleted_index_entries(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
) -> Result<Vec<IndexEntry>> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
if !index_path.exists() {
return Ok(Vec::new());
}
let index = Index::parse(&fs::read(index_path)?, format)?;
let mut deleted = Vec::new();
for entry in index.entries {
if !worktree_path(worktree_root, entry.path.as_bytes())?.exists()
&& !index_entry_skip_worktree(&entry)
{
deleted.push(entry);
}
}
Ok(deleted)
}
pub fn modified_index_entries(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
) -> Result<Vec<IndexEntry>> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
if !index_path.exists() {
return Ok(Vec::new());
}
let mut index = Index::parse(&fs::read(&index_path)?, format)?;
if index.entries.iter().any(IndexEntry::is_sparse_dir) {
let db = FileObjectDatabase::from_git_dir(git_dir, format);
expand_sparse_index(&mut index, &db, format)?;
}
let stat_cache = IndexStatCache::from_index(&index, &index_path);
let mut modified = Vec::new();
for entry in index.entries {
let worktree_entry = worktree_entry_for_git_path(
worktree_root,
git_dir,
format,
entry.path.as_bytes(),
&entry.oid,
entry.mode,
Some(&stat_cache),
)?;
let Some(worktree_entry) = worktree_entry else {
if !index_entry_skip_worktree(&entry) {
modified.push(entry);
}
continue;
};
if worktree_entry.mode != entry.mode || worktree_entry.oid != entry.oid {
modified.push(entry);
}
}
Ok(modified)
}
pub fn checkout_branch(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
branch: &str,
committer: Vec<u8>,
) -> Result<CheckoutResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let branch_ref = branch_ref_name(branch)?;
let refs = FileRefStore::new(git_dir, format);
let target = match sley_refs::resolve_ref_peeled(&refs, &branch_ref)? {
Some(oid) => oid,
None => {
checkout_switch_head_symbolic(&refs, branch_ref, committer, branch, None, None)?;
return Ok(CheckoutResult {
branch: branch.into(),
oid: ObjectId::null(format),
files: 0,
});
}
};
let current_head = resolve_head_commit_oid(git_dir, format)?;
let files = if current_head == Some(target) {
0
} else {
checkout_commit_to_index_and_worktree(worktree_root, git_dir, format, &target)?
};
checkout_switch_head_symbolic(
&refs,
branch_ref,
committer,
branch,
Some(target),
Some(target),
)?;
Ok(CheckoutResult {
branch: branch.into(),
oid: target,
files,
})
}
pub fn checkout_detached(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
target: &ObjectId,
committer: Vec<u8>,
message: Vec<u8>,
) -> Result<CheckoutResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let files = checkout_commit_to_index_and_worktree(worktree_root, git_dir, format, target)?;
let refs = FileRefStore::new(git_dir, format);
let zero = ObjectId::null(format);
let mut tx = refs.transaction();
tx.update(RefUpdate {
name: "HEAD".into(),
expected: None,
new: RefTarget::Direct(*target),
reflog: Some(ReflogEntry {
old_oid: zero,
new_oid: *target,
committer,
message,
}),
});
tx.commit()?;
Ok(CheckoutResult {
branch: target.to_string(),
oid: *target,
files,
})
}
pub fn checkout_branch_filtered(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
branch: &str,
committer: Vec<u8>,
config: &GitConfig,
) -> Result<CheckoutResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let branch_ref = branch_ref_name(branch)?;
let refs = FileRefStore::new(git_dir, format);
let target = match sley_refs::resolve_ref_peeled(&refs, &branch_ref)? {
Some(oid) => oid,
None => {
checkout_switch_head_symbolic(&refs, branch_ref, committer, branch, None, None)?;
return Ok(CheckoutResult {
branch: branch.into(),
oid: ObjectId::null(format),
files: 0,
});
}
};
let current_head = resolve_head_commit_oid(git_dir, format)?;
let files = if current_head == Some(target) {
0
} else {
checkout_commit_to_index_and_worktree_filtered(
worktree_root,
git_dir,
format,
&target,
Some(config),
Some(vec![
("ref".to_string(), branch_ref.clone()),
("treeish".to_string(), target.to_hex()),
]),
)?
};
checkout_switch_head_symbolic(
&refs,
branch_ref,
committer,
branch,
Some(target),
Some(target),
)?;
Ok(CheckoutResult {
branch: branch.into(),
oid: target,
files,
})
}
pub fn checkout_detached_filtered(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
target: &ObjectId,
committer: Vec<u8>,
message: Vec<u8>,
config: &GitConfig,
) -> Result<CheckoutResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let files = checkout_commit_to_index_and_worktree_filtered(
worktree_root,
git_dir,
format,
target,
Some(config),
Some(vec![("treeish".to_string(), target.to_hex())]),
)?;
let refs = FileRefStore::new(git_dir, format);
let zero = ObjectId::null(format);
let mut tx = refs.transaction();
tx.update(RefUpdate {
name: "HEAD".into(),
expected: None,
new: RefTarget::Direct(*target),
reflog: Some(ReflogEntry {
old_oid: zero,
new_oid: *target,
committer,
message,
}),
});
tx.commit()?;
Ok(CheckoutResult {
branch: target.to_string(),
oid: *target,
files,
})
}
pub(crate) fn checkout_commit_to_index_and_worktree(
worktree_root: &Path,
git_dir: &Path,
format: ObjectFormat,
target: &ObjectId,
) -> Result<usize> {
checkout_commit_to_index_and_worktree_filtered(
worktree_root,
git_dir,
format,
target,
None,
None,
)
}
pub(crate) fn checkout_commit_to_index_and_worktree_filtered(
worktree_root: &Path,
git_dir: &Path,
format: ObjectFormat,
target: &ObjectId,
smudge_config: Option<&GitConfig>,
process_metadata: Option<Vec<(String, String)>>,
) -> Result<usize> {
if let Some((sparse, mode)) = active_sparse_checkout(git_dir)? {
return checkout_commit_to_index_and_worktree_sparse(
worktree_root,
git_dir,
format,
target,
Some((&sparse, mode)),
smudge_config,
process_metadata,
);
}
let _process_filter_metadata = set_process_filter_metadata(process_metadata);
let mut dirty = false;
if smudge_config.is_some() {
dirty = !modified_index_entries(worktree_root, git_dir, format)?.is_empty();
} else {
stream_short_status(worktree_root, git_dir, format, |entry| {
if !status_row_is_untracked_or_ignored(entry) {
dirty = true;
return Ok(StreamControl::Stop);
}
Ok(StreamControl::Continue)
})?;
}
if dirty {
return Err(GitError::Transaction(
"checkout requires a clean working tree".into(),
));
}
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let commit = read_commit(&db, format, target)?;
let mut target_entries = BTreeMap::new();
collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
refuse_if_current_working_directory_becomes_file(worktree_root, &target_entries)?;
let attributes = smudge_config
.map(|_| build_tree_attribute_matcher(worktree_root, &db, format, &commit.tree))
.transpose()?;
for path in read_index_entries(git_dir, format)?.keys() {
if !target_entries.contains_key(path) {
remove_worktree_file(worktree_root, path)?;
}
}
let mut index_entries = Vec::new();
for (path, entry) in &target_entries {
index_entries.push(materialize_tree_entry_with_optional_smudge(
&db,
format,
worktree_root,
path,
entry,
smudge_config,
attributes.as_ref(),
)?);
}
index_entries.sort_by(|left, right| left.path.cmp(&right.path));
let extensions = preserved_index_extensions(git_dir, format)?;
fs::write(
repository_index_path(git_dir),
Index {
version: 2,
entries: index_entries,
extensions,
checksum: None,
}
.write(format)?,
)?;
Ok(target_entries.len())
}
pub(crate) fn build_tree_attribute_matcher(
worktree_root: &Path,
db: &FileObjectDatabase,
format: ObjectFormat,
tree_oid: &ObjectId,
) -> Result<AttributeMatcher> {
let mut matcher = AttributeMatcher::default();
let git_dir = worktree_root.join(".git");
matcher.configure_case_sensitivity(&git_dir);
if !matcher.read_configured_attributes(worktree_root, &git_dir) {
matcher.read_default_global_attributes();
}
collect_attribute_patterns_from_tree(db, format, tree_oid, Vec::new(), &mut matcher)?;
read_attribute_patterns(
worktree_root.join(".git").join("info").join("attributes"),
&mut matcher,
&[],
b".git/info/attributes",
false,
);
Ok(matcher)
}
pub(crate) fn materialize_tree_entry_with_optional_smudge(
db: &FileObjectDatabase,
format: ObjectFormat,
worktree_root: &Path,
path: &[u8],
entry: &TrackedEntry,
smudge_config: Option<&GitConfig>,
attributes: Option<&AttributeMatcher>,
) -> Result<IndexEntry> {
if smudge_config.is_none()
|| sley_index::is_gitlink(entry.mode)
|| (entry.mode & 0o170000) == 0o120000
{
return materialize_tree_entry(db, worktree_root, path, entry);
}
let config = smudge_config.expect("checked above");
let matcher = attributes.expect("attributes are built when smudge_config is set");
let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
let checks = matcher.attributes_for_path(path, &filter_attribute_names(), false);
let body = apply_smudge_filter_with_attributes_cow_format(
config,
&checks,
path,
&object.body,
format,
)?;
let file_path = worktree_path(worktree_root, path)?;
prepare_blob_parent_dirs(worktree_root, &file_path)?;
remove_existing_worktree_path(&file_path)?;
fs::write(&file_path, &body)?;
set_worktree_file_mode(&file_path, entry.mode)?;
let metadata = fs::metadata(&file_path)?;
let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
index_entry.mode = entry.mode;
Ok(index_entry)
}
pub(crate) fn checkout_commit_to_index_and_worktree_sparse(
worktree_root: &Path,
git_dir: &Path,
format: ObjectFormat,
target: &ObjectId,
sparse: Option<(&SparseCheckout, SparseCheckoutMode)>,
smudge_config: Option<&GitConfig>,
process_metadata: Option<Vec<(String, String)>>,
) -> Result<usize> {
let _process_filter_metadata = set_process_filter_metadata(process_metadata);
let previously_skipped = skip_worktree_paths(git_dir, format)?;
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let commit = read_commit(&db, format, target)?;
let mut target_entries = BTreeMap::new();
collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
let mut dirty = false;
stream_short_status(worktree_root, git_dir, format, |entry| {
if previously_skipped.contains(entry.path) {
return Ok(StreamControl::Continue);
}
if entry.index_mode.is_some_and(sley_index::is_gitlink)
|| entry.worktree_mode.is_some_and(sley_index::is_gitlink)
{
return Ok(StreamControl::Continue);
}
if entry.index == b'?' && entry.worktree == b'?' {
let path = entry.path.strip_suffix(b"/").unwrap_or(entry.path);
if target_entries
.get(path)
.is_some_and(|target| sley_index::is_gitlink(target.mode))
{
return Ok(StreamControl::Continue);
}
}
dirty = true;
Ok(StreamControl::Stop)
})?;
if dirty {
return Err(GitError::Transaction(
"checkout requires a clean working tree".into(),
));
}
let matcher = sparse.map(|(spec, mode)| SparseMatcher::new(spec, mode));
let attributes = smudge_config
.map(|_| build_tree_attribute_matcher(worktree_root, &db, format, &commit.tree))
.transpose()?;
for path in read_index_entries(git_dir, format)?.keys() {
if target_entries.contains_key(path) {
continue;
}
if previously_skipped.contains(path) {
continue;
}
remove_worktree_file(worktree_root, path)?;
}
let mut index_entries = Vec::new();
for (path, entry) in &target_entries {
let in_cone = matcher.as_ref().map_or_else(
|| !previously_skipped.contains(path),
|matcher| matcher.includes_file(path),
);
let index_entry = if in_cone {
materialize_tree_entry_with_optional_smudge(
&db,
format,
worktree_root,
path,
entry,
smudge_config,
attributes.as_ref(),
)?
} else {
remove_worktree_file(worktree_root, path)?;
let mut index_entry = restored_head_index_entry(worktree_root, &db, path, entry)?;
set_skip_worktree(&mut index_entry);
index_entry
};
index_entries.push(index_entry);
}
index_entries.sort_by(|left, right| left.path.cmp(&right.path));
let mut index = Index {
version: 2,
entries: index_entries,
extensions: preserved_index_extensions(git_dir, format)?,
checksum: None,
};
normalize_index_version_for_extended_flags(&mut index);
write_repository_index_ref(git_dir, format, &index)?;
Ok(target_entries.len())
}
pub(crate) fn skip_worktree_paths(
git_dir: &Path,
format: ObjectFormat,
) -> Result<BTreeSet<Vec<u8>>> {
let index_path = repository_index_path(git_dir);
if !index_path.exists() {
return Ok(BTreeSet::new());
}
let index = Index::parse(&fs::read(index_path)?, format)?;
Ok(index
.entries
.into_iter()
.filter(index_entry_skip_worktree)
.map(|entry| entry.path.into_bytes())
.collect())
}
pub fn restore_worktree_paths(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
paths: &[PathBuf],
) -> Result<RestoreResult> {
restore_worktree_paths_inner(
worktree_root.as_ref(),
git_dir.as_ref(),
format,
paths,
None,
)
}
pub fn restore_worktree_paths_filtered(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
paths: &[PathBuf],
config: &GitConfig,
) -> Result<RestoreResult> {
restore_worktree_paths_inner(
worktree_root.as_ref(),
git_dir.as_ref(),
format,
paths,
Some(config),
)
}
pub(crate) fn restore_worktree_paths_inner(
worktree_root: &Path,
git_dir: &Path,
format: ObjectFormat,
paths: &[PathBuf],
smudge_config: Option<&GitConfig>,
) -> Result<RestoreResult> {
let index_path = repository_index_path(git_dir);
if !index_path.exists() {
return Err(GitError::Exit(1));
}
let mut index = Index::parse(&fs::read(&index_path)?, format)?;
let stat_cache = IndexStatCache::from_index(&index, &index_path);
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let mut restored = BTreeSet::new();
for path in paths {
let absolute = if path.is_absolute() {
path.clone()
} else {
worktree_root.join(path)
};
let absolute = normalize_absolute_path_lexically(&absolute);
let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
})?;
let git_path = git_path_bytes(relative)?;
let recursive = path == Path::new(".")
|| path.to_string_lossy().ends_with('/')
|| absolute.is_dir()
|| index_has_entry_under(&index.entries, &git_path);
let mut matched = false;
let matched_positions = index
.entries
.iter()
.enumerate()
.filter_map(|(position, entry)| {
(entry.path.as_bytes() == git_path.as_slice()
|| (recursive && index_entry_is_under_path(entry.path.as_bytes(), &git_path)))
.then_some(position)
})
.collect::<Vec<_>>();
for position in matched_positions {
let refreshed = restore_index_entry(
worktree_root,
git_dir,
format,
&db,
&index.entries[position],
smudge_config,
Some(&stat_cache),
)?;
restored.insert(index.entries[position].path.clone());
matched = true;
if let Some(refreshed) = refreshed {
index.entries[position] = refreshed;
}
}
if !matched {
eprintln!(
"error: pathspec '{}' did not match any file(s) known to git",
path.display()
);
return Err(GitError::Exit(1));
}
}
write_repository_index_ref(git_dir, format, &index)?;
Ok(RestoreResult {
restored: restored.len(),
})
}
pub fn checkout_index_paths(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
paths: &[PathBuf],
options: CheckoutIndexPathOptions<'_>,
) -> Result<RestoreResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
if !index_path.exists() {
return Err(GitError::Exit(1));
}
let mut index = Index::parse(&fs::read(&index_path)?, format)?;
if options.merge {
checkout_unmerge_resolve_undo_paths(worktree_root, &mut index, format, paths)?;
}
let stat_cache = IndexStatCache::from_index(&index, &index_path);
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let selected = checkout_selected_index_paths(worktree_root, &index, paths)?;
if options.stage.is_none() && !options.merge && !options.force {
for path in &selected {
if checkout_path_is_unmerged(&index, path) {
eprintln!(
"error: path '{}' is unmerged",
String::from_utf8_lossy(path)
);
return Err(GitError::Exit(1));
}
}
}
let mut refreshed = BTreeMap::new();
let mut restored = BTreeSet::new();
for path in selected {
let positions = index
.entries
.iter()
.enumerate()
.filter_map(|(position, entry)| (entry.path.as_bytes() == path).then_some(position))
.collect::<Vec<_>>();
let stage0 = positions
.iter()
.copied()
.find(|position| index.entries[*position].stage() == Stage::Normal);
let is_unmerged = positions
.iter()
.any(|position| index.entries[*position].stage() != Stage::Normal);
if is_unmerged {
if let Some(stage) = options.stage {
let wanted = match stage {
CheckoutStage::Ours => Stage::Ours,
CheckoutStage::Theirs => Stage::Theirs,
};
let Some(position) = positions
.iter()
.copied()
.find(|position| index.entries[*position].stage() == wanted)
else {
eprintln!(
"error: path '{}' does not have {} version",
String::from_utf8_lossy(&path),
match stage {
CheckoutStage::Ours => "our",
CheckoutStage::Theirs => "their",
}
);
return Err(GitError::Exit(1));
};
checkout_write_index_entry_to_worktree(
worktree_root,
git_dir,
format,
&db,
&index.entries[position],
options.smudge_config,
Some(&stat_cache),
)?;
restored.insert(path);
continue;
}
if options.merge {
checkout_merge_unmerged_path(
worktree_root,
&db,
&index,
&positions,
options.conflict_style,
)?;
restored.insert(path);
continue;
}
if options.force {
continue;
}
}
if let Some(position) = stage0 {
if let Some(updated) = checkout_write_index_entry_to_worktree(
worktree_root,
git_dir,
format,
&db,
&index.entries[position],
options.smudge_config,
Some(&stat_cache),
)? {
refreshed.insert(position, updated);
}
restored.insert(path);
}
}
for (position, entry) in refreshed {
index.entries[position] = entry;
}
if !index.entries.is_empty() {
write_repository_index_ref(git_dir, format, &index)?;
}
Ok(RestoreResult {
restored: restored.len(),
})
}
pub fn unresolve_index_paths(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
paths: &[PathBuf],
) -> Result<()> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
if !index_path.exists() {
return Ok(());
}
let mut index = Index::parse(&fs::read(&index_path)?, format)?;
checkout_unmerge_resolve_undo_paths(worktree_root, &mut index, format, paths)?;
write_repository_index_ref(git_dir, format, &index)
}
pub(crate) fn checkout_selected_index_paths(
worktree_root: &Path,
index: &Index,
paths: &[PathBuf],
) -> Result<BTreeSet<Vec<u8>>> {
let index_paths = index
.entries
.iter()
.map(|entry| entry.path.as_bytes().to_vec())
.collect::<BTreeSet<_>>();
let mut selected = BTreeSet::new();
for path in paths {
let absolute = if path.is_absolute() {
path.clone()
} else {
worktree_root.join(path)
};
let absolute = normalize_absolute_path_lexically(&absolute);
let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
})?;
let git_path = git_path_bytes(relative)?;
let recursive = path == Path::new(".")
|| path.to_string_lossy().ends_with('/')
|| absolute.is_dir()
|| index_paths
.iter()
.any(|entry| index_entry_is_under_path(entry, &git_path));
let matched = index_paths
.iter()
.filter(|entry| {
entry.as_slice() == git_path.as_slice()
|| (recursive && index_entry_is_under_path(entry, &git_path))
})
.cloned()
.collect::<Vec<_>>();
if matched.is_empty() {
eprintln!(
"error: pathspec '{}' did not match any file(s) known to git",
path.display()
);
return Err(GitError::Exit(1));
}
selected.extend(matched);
}
Ok(selected)
}
pub(crate) fn checkout_unmerge_resolve_undo_paths(
worktree_root: &Path,
index: &mut Index,
format: ObjectFormat,
paths: &[PathBuf],
) -> Result<()> {
let records = parse_resolve_undo_records(index.extension(b"REUC")?, format)?;
if records.is_empty() {
return Ok(());
}
let mut remaining = Vec::new();
let mut unmerged_any = false;
for record in records {
if checkout_pathspecs_match_git_path(worktree_root, paths, &record.path)? {
remove_index_entries_with_path(&mut index.entries, &record.path);
for (idx, stage) in record.stages.into_iter().enumerate() {
let Some((mode, oid)) = stage else {
continue;
};
index.entries.push(resolve_undo_index_entry(
record.path.clone(),
mode,
oid,
(idx + 1) as u16,
));
}
unmerged_any = true;
} else {
remaining.push(record);
}
}
if unmerged_any {
index.entries.sort_by(compare_index_key);
normalize_index_version_for_extended_flags(index);
set_resolve_undo_extension(index, &remaining)?;
}
Ok(())
}
pub(crate) fn checkout_pathspecs_match_git_path(
worktree_root: &Path,
paths: &[PathBuf],
candidate: &[u8],
) -> Result<bool> {
for path in paths {
let absolute = if path.is_absolute() {
path.clone()
} else {
worktree_root.join(path)
};
let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
})?;
let git_path = git_path_bytes(relative)?;
let recursive = path == Path::new(".")
|| path.to_string_lossy().ends_with('/')
|| absolute.is_dir()
|| index_entry_is_under_path(candidate, &git_path);
if candidate == git_path.as_slice()
|| (recursive && index_entry_is_under_path(candidate, &git_path))
{
return Ok(true);
}
}
Ok(false)
}
pub(crate) fn resolve_undo_index_entry(
path: Vec<u8>,
mode: u32,
oid: ObjectId,
stage: u16,
) -> IndexEntry {
let name_len = (path
.len()
.min(sley_index::INDEX_FLAG_NAME_LENGTH_MASK as usize)) as u16;
IndexEntry {
ctime_seconds: 0,
ctime_nanoseconds: 0,
mtime_seconds: 0,
mtime_nanoseconds: 0,
dev: 0,
ino: 0,
mode,
uid: 0,
gid: 0,
size: 0,
oid,
flags: name_len | (stage << 12),
flags_extended: 0,
path: path.into(),
}
}
pub(crate) fn checkout_path_is_unmerged(index: &Index, path: &[u8]) -> bool {
index
.entries
.iter()
.any(|entry| entry.path.as_bytes() == path && entry.stage() != Stage::Normal)
}
pub(crate) fn checkout_write_index_entry_to_worktree(
worktree_root: &Path,
git_dir: &Path,
format: ObjectFormat,
db: &FileObjectDatabase,
entry: &IndexEntry,
smudge_config: Option<&GitConfig>,
stat_cache: Option<&IndexStatCache>,
) -> Result<Option<IndexEntry>> {
restore_index_entry(
worktree_root,
git_dir,
format,
db,
entry,
smudge_config,
stat_cache,
)
}
pub(crate) fn checkout_merge_unmerged_path(
worktree_root: &Path,
db: &FileObjectDatabase,
index: &Index,
positions: &[usize],
style: CheckoutConflictStyle,
) -> Result<()> {
let mut base = None;
let mut ours = None;
let mut theirs = None;
for position in positions {
let entry = &index.entries[*position];
match entry.stage() {
Stage::Base => base = Some(entry),
Stage::Ours => ours = Some(entry),
Stage::Theirs => theirs = Some(entry),
Stage::Normal => {}
}
}
let Some(ours) = ours else {
return Ok(());
};
let Some(theirs) = theirs else {
return Ok(());
};
let base_body = match base {
Some(entry) => read_expected_object(db, &entry.oid, ObjectType::Blob)?
.body
.clone(),
None => Vec::new(),
};
let ours_body = read_expected_object(db, &ours.oid, ObjectType::Blob)?
.body
.clone();
let theirs_body = read_expected_object(db, &theirs.oid, ObjectType::Blob)?
.body
.clone();
let result = sley_diff_merge::merge_blobs(
&base_body,
&ours_body,
&theirs_body,
&sley_diff_merge::MergeBlobOptions {
ours_label: "ours",
theirs_label: "theirs",
base_label: "base",
style: match style {
CheckoutConflictStyle::Merge => sley_diff_merge::ConflictStyle::Merge,
CheckoutConflictStyle::Diff3 => sley_diff_merge::ConflictStyle::Diff3,
},
favor: sley_diff_merge::MergeFavor::None,
ws_ignore: sley_diff_merge::WsIgnore::EMPTY,
},
);
let file_path = worktree_path(worktree_root, ours.path.as_bytes())?;
prepare_blob_parent_dirs(worktree_root, &file_path)?;
remove_existing_worktree_path(&file_path)?;
fs::write(&file_path, result.content)?;
set_worktree_file_mode(&file_path, ours.mode)?;
Ok(())
}
pub fn restore_index_paths_from_head(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
paths: &[PathBuf],
) -> Result<RestoreResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
let index = if index_path.exists() {
Index::parse(&fs::read(&index_path)?, format)?
} else {
Index {
version: 2,
entries: Vec::new(),
extensions: Vec::new(),
checksum: None,
}
};
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let head_entries = head_tree_entries(git_dir, format, &db)?;
restore_index_paths_from_entries(
worktree_root,
git_dir,
format,
&db,
index,
&head_entries,
paths,
false,
)
}
pub fn restore_index_paths_from_tree(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
tree_oid: &ObjectId,
paths: &[PathBuf],
) -> Result<RestoreResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
let index = if index_path.exists() {
Index::parse(&fs::read(&index_path)?, format)?
} else {
Index {
version: 2,
entries: Vec::new(),
extensions: Vec::new(),
checksum: None,
}
};
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let source_entries = tree_entries(&db, format, tree_oid)?;
restore_index_paths_from_entries(
worktree_root,
git_dir,
format,
&db,
index,
&source_entries,
paths,
false,
)
}
pub fn restore_index_paths_from_tree_allow_unmatched(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
tree_oid: &ObjectId,
paths: &[PathBuf],
) -> Result<RestoreResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
let index = if index_path.exists() {
Index::parse(&fs::read(&index_path)?, format)?
} else {
Index {
version: 2,
entries: Vec::new(),
extensions: Vec::new(),
checksum: None,
}
};
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let source_entries = tree_entries(&db, format, tree_oid)?;
restore_index_paths_from_entries(
worktree_root,
git_dir,
format,
&db,
index,
&source_entries,
paths,
true,
)
}
pub(crate) fn restore_index_paths_from_entries(
worktree_root: &Path,
git_dir: &Path,
format: ObjectFormat,
db: &FileObjectDatabase,
mut index: Index,
source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
paths: &[PathBuf],
allow_unmatched: bool,
) -> Result<RestoreResult> {
let sparse = active_sparse_checkout(git_dir)?;
if index.is_sparse() {
expand_sparse_index(&mut index, db, format)?;
}
let index_version = index.version;
let extensions = index_extensions_without_cache_tree(&index.extensions);
let mut index_entries = index
.entries
.into_iter()
.map(|entry| (entry.path.as_bytes().to_vec(), entry))
.collect::<BTreeMap<_, _>>();
let prior_skip_worktree = index_entries
.iter()
.filter(|(_, entry)| entry.is_skip_worktree())
.map(|(path, _)| path.clone())
.collect::<BTreeSet<_>>();
let mut restored = BTreeSet::new();
for path in paths {
let absolute = if path.is_absolute() {
path.clone()
} else {
worktree_root.join(path)
};
let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
})?;
let git_path = git_path_bytes(relative)?;
let recursive = path == Path::new(".")
|| path.to_string_lossy().ends_with('/')
|| absolute.is_dir()
|| index_entries
.keys()
.any(|entry| index_entry_is_under_path(entry, &git_path))
|| source_entries
.keys()
.any(|entry| index_entry_is_under_path(entry, &git_path));
let mut matched_paths = BTreeSet::new();
for path in index_entries.keys().chain(source_entries.keys()) {
if path.as_slice() == git_path.as_slice()
|| (recursive && index_entry_is_under_path(path, &git_path))
{
matched_paths.insert(path.clone());
}
}
if matched_paths.is_empty() {
if allow_unmatched {
continue;
}
eprintln!(
"error: pathspec '{}' did not match any file(s) known to git",
path.display()
);
return Err(GitError::Exit(1));
}
for path in matched_paths {
if let Some(entry) = source_entries.get(&path) {
let unchanged = index_entries.get(&path).is_some_and(|existing| {
existing.oid == entry.oid
&& existing.mode == entry.mode
&& !existing.is_intent_to_add()
});
if !unchanged {
let mut restored = restored_head_index_entry(worktree_root, db, &path, entry)?;
if prior_skip_worktree.contains(&path) {
restored.set_skip_worktree(true);
}
index_entries.insert(path.clone(), restored);
}
} else {
index_entries.remove(&path);
}
restored.insert(path);
}
}
let mut entries = index_entries.into_values().collect::<Vec<_>>();
entries.sort_by(|left, right| left.path.cmp(&right.path));
let restored_paths = restored.iter().cloned().collect::<Vec<_>>();
let mut index = Index {
version: index_version,
entries,
extensions,
checksum: None,
};
invalidate_untracked_cache_for_git_paths(&mut index, format, &restored_paths)?;
if let Some((sparse, mode)) = sparse
&& sparse.sparse_index
{
let matcher = SparseMatcher::new(&sparse, mode);
collapse_to_sparse_index(&mut index, &matcher, db, format)?;
}
write_repository_index_ref(git_dir, format, &index)?;
Ok(RestoreResult {
restored: restored.len(),
})
}
pub fn restore_index_and_worktree_paths_from_head(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
paths: &[PathBuf],
overlay: bool,
) -> Result<RestoreResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
let index = if index_path.exists() {
Index::parse(&fs::read(&index_path)?, format)?
} else {
Index {
version: 2,
entries: Vec::new(),
extensions: Vec::new(),
checksum: None,
}
};
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let head_entries = head_tree_entries(git_dir, format, &db)?;
restore_index_and_worktree_paths_from_entries(
worktree_root,
git_dir,
format,
&db,
index,
&head_entries,
paths,
overlay,
)
}
pub fn restore_index_and_worktree_paths_from_tree(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
tree_oid: &ObjectId,
paths: &[PathBuf],
overlay: bool,
) -> Result<RestoreResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
let index = if index_path.exists() {
Index::parse(&fs::read(&index_path)?, format)?
} else {
Index {
version: 2,
entries: Vec::new(),
extensions: Vec::new(),
checksum: None,
}
};
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let source_entries = tree_entries(&db, format, tree_oid)?;
restore_index_and_worktree_paths_from_entries(
worktree_root,
git_dir,
format,
&db,
index,
&source_entries,
paths,
overlay,
)
}
pub(crate) fn restore_index_and_worktree_paths_from_entries(
worktree_root: &Path,
git_dir: &Path,
format: ObjectFormat,
db: &FileObjectDatabase,
index: Index,
source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
paths: &[PathBuf],
overlay: bool,
) -> Result<RestoreResult> {
let index_version = index.version;
let extensions = index_extensions_without_cache_tree(&index.extensions);
let mut index_entries = index
.entries
.into_iter()
.map(|entry| (entry.path.as_bytes().to_vec(), entry))
.collect::<BTreeMap<_, _>>();
let mut restored = BTreeSet::new();
for path in paths {
let absolute = if path.is_absolute() {
path.clone()
} else {
worktree_root.join(path)
};
let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
})?;
let git_path = git_path_bytes(relative)?;
let recursive = path == Path::new(".")
|| path.to_string_lossy().ends_with('/')
|| absolute.is_dir()
|| index_entries
.keys()
.any(|entry| index_entry_is_under_path(entry, &git_path))
|| source_entries
.keys()
.any(|entry| index_entry_is_under_path(entry, &git_path));
let mut matched_paths = BTreeSet::new();
for path in index_entries.keys().chain(source_entries.keys()) {
if path.as_slice() == git_path.as_slice()
|| (recursive && index_entry_is_under_path(path, &git_path))
{
matched_paths.insert(path.clone());
}
}
if matched_paths.is_empty() {
eprintln!(
"error: pathspec '{}' did not match any file(s) known to git",
path.display()
);
return Err(GitError::Exit(1));
}
for path in matched_paths {
if let Some(entry) = source_entries.get(&path) {
index_entries.insert(
path.clone(),
restore_head_entry_to_worktree_and_index(worktree_root, db, &path, entry)?,
);
} else if overlay {
continue;
} else {
index_entries.remove(&path);
remove_worktree_file(worktree_root, &path)?;
}
restored.insert(path);
}
}
let mut entries = index_entries.into_values().collect::<Vec<_>>();
entries.sort_by(|left, right| left.path.cmp(&right.path));
let restored_paths = restored.iter().cloned().collect::<Vec<_>>();
let mut index = Index {
version: index_version,
entries,
extensions,
checksum: None,
};
invalidate_untracked_cache_for_git_paths(&mut index, format, &restored_paths)?;
write_repository_index_ref(git_dir, format, &index)?;
Ok(RestoreResult {
restored: restored.len(),
})
}
pub fn reset_index_and_worktree_to_commit(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
commit_oid: &ObjectId,
) -> Result<RestoreResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let commit = read_commit(&db, format, commit_oid)?;
let mut target_entries = BTreeMap::new();
collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
refuse_if_current_working_directory_becomes_file(worktree_root, &target_entries)?;
let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
let attributes = build_tree_attribute_matcher(worktree_root, &db, format, &commit.tree)?;
for path in current_index_paths(git_dir, format, &db)? {
if !target_entries.contains_key(&path) {
remove_worktree_file(worktree_root, &path)?;
}
}
let mut index_entries = Vec::new();
for (path, entry) in &target_entries {
index_entries.push(materialize_tree_entry_filtered(
&db,
format,
worktree_root,
path,
entry,
&config,
&attributes,
)?);
}
index_entries.sort_by(|left, right| left.path.cmp(&right.path));
let extensions = preserved_index_extensions(git_dir, format)?;
fs::write(
repository_index_path(git_dir),
Index {
version: 2,
entries: index_entries,
extensions,
checksum: None,
}
.write(format)?,
)?;
Ok(RestoreResult {
restored: target_entries.len(),
})
}
pub(crate) fn current_index_paths(
git_dir: &Path,
format: ObjectFormat,
db: &FileObjectDatabase,
) -> Result<BTreeSet<Vec<u8>>> {
let (index, _stat_cache, _head_matches) = read_index_with_stat_cache(git_dir, format, db)?;
Ok(index
.entries
.into_iter()
.map(|entry| entry.path.into_bytes())
.collect())
}
pub(crate) fn materialize_tree_entry(
db: &FileObjectDatabase,
worktree_root: &Path,
path: &[u8],
entry: &TrackedEntry,
) -> Result<IndexEntry> {
if sley_index::is_gitlink(entry.mode) {
let dir_path = worktree_path(worktree_root, path)?;
materialize_gitlink_dir(worktree_root, &dir_path)?;
return Ok(IndexEntry {
ctime_seconds: 0,
ctime_nanoseconds: 0,
mtime_seconds: 0,
mtime_nanoseconds: 0,
dev: 0,
ino: 0,
mode: entry.mode,
uid: 0,
gid: 0,
size: 0,
oid: entry.oid,
flags: path.len().min(0x0fff) as u16,
flags_extended: 0,
path: BString::from(path),
});
}
let file_path = write_worktree_blob_entry(db, worktree_root, path, entry)?;
let metadata = fs::symlink_metadata(&file_path)?;
let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
index_entry.mode = entry.mode;
Ok(index_entry)
}
pub(crate) fn materialize_gitlink_dir(worktree_root: &Path, dir_path: &Path) -> Result<()> {
prepare_blob_parent_dirs(worktree_root, dir_path)?;
if fs::symlink_metadata(dir_path).is_ok_and(|metadata| !metadata.is_dir()) {
remove_existing_worktree_path(dir_path)?;
}
fs::create_dir_all(dir_path)?;
Ok(())
}
pub(crate) fn materialize_tree_entry_filtered(
db: &FileObjectDatabase,
format: ObjectFormat,
worktree_root: &Path,
path: &[u8],
entry: &TrackedEntry,
config: &GitConfig,
attributes: &AttributeMatcher,
) -> Result<IndexEntry> {
if sley_index::is_gitlink(entry.mode) || (entry.mode & 0o170000) == 0o120000 {
return materialize_tree_entry(db, worktree_root, path, entry);
}
let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
let checks = attributes.attributes_for_path(path, &filter_attribute_names(), false);
let body = apply_smudge_filter_with_attributes_cow_format(
config,
&checks,
path,
&object.body,
format,
)?;
let file_path = worktree_path(worktree_root, path)?;
prepare_blob_parent_dirs(worktree_root, &file_path)?;
remove_existing_worktree_path(&file_path)?;
fs::write(&file_path, &body)?;
set_worktree_file_mode(&file_path, entry.mode)?;
let metadata = fs::symlink_metadata(&file_path)?;
let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
index_entry.mode = entry.mode;
Ok(index_entry)
}
pub(crate) fn write_worktree_blob_entry(
db: &FileObjectDatabase,
worktree_root: &Path,
path: &[u8],
entry: &TrackedEntry,
) -> Result<PathBuf> {
let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
let file_path = worktree_path(worktree_root, path)?;
prepare_blob_parent_dirs(worktree_root, &file_path)?;
remove_existing_worktree_path(&file_path)?;
write_blob_body_or_symlink(&file_path, entry.mode, &object.body, &object.body)?;
Ok(file_path)
}
pub fn write_blob_body_or_symlink(
file_path: &Path,
mode: u32,
body: &[u8],
link_target: &[u8],
) -> Result<()> {
if (mode & 0o170000) == 0o120000 {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
let target =
std::path::PathBuf::from(std::ffi::OsString::from_vec(link_target.to_vec()));
std::os::unix::fs::symlink(&target, file_path)?;
}
#[cfg(not(unix))]
{
let _ = link_target;
fs::write(file_path, body)?;
}
} else {
fs::write(file_path, body)?;
set_worktree_file_mode(file_path, mode)?;
}
Ok(())
}
pub(crate) fn prepare_blob_parent_dirs(worktree_root: &Path, file_path: &Path) -> Result<()> {
let parent = match file_path.parent() {
Some(parent) => parent,
None => return Ok(()),
};
if parent.is_dir() {
return Ok(());
}
let mut components: Vec<&Path> = Vec::new();
let mut cursor = Some(parent);
while let Some(dir) = cursor {
if dir == worktree_root {
break;
}
components.push(dir);
cursor = dir.parent();
if cursor.is_none() {
break;
}
}
for dir in components.iter().rev() {
match fs::symlink_metadata(dir) {
Ok(metadata) if metadata.is_dir() => {}
Ok(_) => {
fs::remove_file(dir)?;
fs::create_dir(dir)?;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
fs::create_dir(dir)?;
}
Err(err) => return Err(err.into()),
}
}
Ok(())
}
pub(crate) fn remove_existing_worktree_path(file_path: &Path) -> Result<()> {
let metadata = match fs::symlink_metadata(file_path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(err.into()),
};
if metadata.is_dir() {
if path_is_original_cwd(file_path) {
return refuse_remove_current_working_directory(file_path);
}
match fs::remove_dir_all(file_path) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
} else {
fs::remove_file(file_path)?;
}
Ok(())
}
#[cfg(unix)]
pub(crate) fn set_worktree_file_mode(file_path: &Path, entry_mode: u32) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let perms = match entry_mode {
0o100755 => 0o755,
0o100644 => 0o644,
_ => return Ok(()),
};
fs::set_permissions(file_path, fs::Permissions::from_mode(perms))?;
Ok(())
}
#[cfg(not(unix))]
pub(crate) fn set_worktree_file_mode(_file_path: &Path, _entry_mode: u32) -> Result<()> {
Ok(())
}
pub fn checkout_tree_to_index_and_worktree(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
tree_oid: &ObjectId,
) -> Result<RestoreResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let mut target_entries = BTreeMap::new();
collect_tree_entries(&db, format, tree_oid, &mut target_entries)?;
for path in read_index_entries(git_dir, format)?.keys() {
if !target_entries.contains_key(path) {
remove_worktree_file(worktree_root, path)?;
}
}
let mut index_entries = Vec::new();
for (path, entry) in &target_entries {
index_entries.push(materialize_tree_entry(&db, worktree_root, path, entry)?);
}
index_entries.sort_by(|left, right| left.path.cmp(&right.path));
let extensions = preserved_index_extensions(git_dir, format)?;
fs::write(
repository_index_path(git_dir),
Index {
version: 2,
entries: index_entries,
extensions,
checksum: None,
}
.write(format)?,
)?;
Ok(RestoreResult {
restored: target_entries.len(),
})
}
pub fn reset_index_to_commit(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
commit_oid: &ObjectId,
) -> Result<RestoreResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let commit = read_commit(&db, format, commit_oid)?;
let mut target_entries = BTreeMap::new();
collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
let index_path = repository_index_path(git_dir);
let prior_skip_worktree: BTreeSet<Vec<u8>> = match fs::read(&index_path) {
Ok(bytes) => Index::parse(&bytes, format)?
.entries
.iter()
.filter(|entry| entry.is_skip_worktree())
.map(|entry| entry.path.as_bytes().to_vec())
.collect(),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => BTreeSet::new(),
Err(err) => return Err(err.into()),
};
let mut index_entries = Vec::new();
for (path, entry) in &target_entries {
let mut restored = restored_head_index_entry(worktree_root, &db, path, entry)?;
if prior_skip_worktree.contains(path) {
restored.set_skip_worktree(true);
}
index_entries.push(restored);
}
index_entries.sort_by(|left, right| left.path.cmp(&right.path));
let mut index = Index {
version: 2,
entries: index_entries,
extensions: preserved_index_extensions(git_dir, format)?,
checksum: None,
};
index.upgrade_version_for_flags();
write_repository_index_ref(git_dir, format, &index)?;
Ok(RestoreResult {
restored: target_entries.len(),
})
}
pub fn index_from_tree(
db: &FileObjectDatabase,
format: ObjectFormat,
tree_oid: &ObjectId,
) -> Result<Index> {
let mut entries: Vec<IndexEntry> = Vec::new();
if *tree_oid != ObjectId::empty_tree(format) {
let mut tree_entries = BTreeMap::new();
collect_tree_entries(db, format, tree_oid, &mut tree_entries)?;
entries.reserve(tree_entries.len());
for (path, entry) in tree_entries {
let name_len = (path.len().min(0x0fff)) as u16;
entries.push(IndexEntry {
ctime_seconds: 0,
ctime_nanoseconds: 0,
mtime_seconds: 0,
mtime_nanoseconds: 0,
dev: 0,
ino: 0,
mode: entry.mode,
uid: 0,
gid: 0,
size: 0,
oid: entry.oid,
flags: name_len,
flags_extended: 0,
path: path.into(),
});
}
}
entries.sort_by(|left, right| left.path.cmp(&right.path));
Ok(Index {
version: 2,
entries,
extensions: Vec::new(),
checksum: None,
})
}
pub fn path_in_sparse_checkout(
path: &[u8],
sparse: &SparseCheckout,
mode: SparseCheckoutMode,
) -> bool {
SparseMatcher::new(sparse, mode).includes_file(path)
}
pub(crate) fn active_sparse_checkout(
git_dir: &Path,
) -> Result<Option<(SparseCheckout, SparseCheckoutMode)>> {
let worktree_config = GitConfig::read(git_dir.join("config.worktree")).unwrap_or_default();
let repo_config = GitConfig::read(git_dir.join("config")).unwrap_or_default();
let sparse_enabled = worktree_config
.get_bool("core", None, "sparseCheckout")
.or_else(|| repo_config.get_bool("core", None, "sparseCheckout"))
.unwrap_or(false);
if !sparse_enabled {
return Ok(None);
}
let sparse_file = git_dir.join("info").join("sparse-checkout");
if !sparse_file.exists() {
return Ok(None);
}
let cone = worktree_config
.get_bool("core", None, "sparseCheckoutCone")
.or_else(|| repo_config.get_bool("core", None, "sparseCheckoutCone"))
.unwrap_or(false);
let sparse_index = cone
&& worktree_config
.get_bool("index", None, "sparse")
.or_else(|| repo_config.get_bool("index", None, "sparse"))
.unwrap_or(false);
let bytes = fs::read(sparse_file)?;
let mut patterns = bytes
.split(|byte| *byte == b'\n')
.map(<[u8]>::to_vec)
.collect::<Vec<_>>();
if patterns.last().map(Vec::is_empty) == Some(true) {
patterns.pop();
}
let mode = if cone {
SparseCheckoutMode::Cone
} else {
SparseCheckoutMode::Full
};
Ok(Some((
SparseCheckout {
patterns,
sparse_index,
},
mode,
)))
}
pub fn apply_sparse_checkout(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
sparse: &SparseCheckout,
) -> Result<ApplySparseResult> {
apply_sparse_checkout_with_mode(
worktree_root,
git_dir,
format,
sparse,
SparseCheckoutMode::Auto,
)
}
pub fn apply_sparse_checkout_with_mode(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
sparse: &SparseCheckout,
mode: SparseCheckoutMode,
) -> Result<ApplySparseResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
let mut index = if index_path.exists() {
Index::parse(&fs::read(&index_path)?, format)?
} else {
return Ok(ApplySparseResult {
materialized: Vec::new(),
skipped: Vec::new(),
not_up_to_date: Vec::new(),
});
};
let matcher = SparseMatcher::new(sparse, mode);
let db = FileObjectDatabase::from_git_dir(git_dir, format);
if index.entries.iter().any(IndexEntry::is_sparse_dir) {
expand_sparse_index(&mut index, &db, format)?;
}
let mut materialized = Vec::new();
let mut skipped = Vec::new();
let mut not_up_to_date = Vec::new();
for entry in &mut index.entries {
if index_entry_stage(entry) != 0 {
continue;
}
if matcher.includes_file(entry.path.as_bytes()) {
clear_skip_worktree(entry);
let file_path = worktree_path(worktree_root, entry.path.as_bytes())?;
if !file_path.exists() {
materialize_index_entry_file(&db, worktree_root, &file_path, entry)?;
let metadata = fs::symlink_metadata(&file_path)?;
*entry = index_entry_with_refreshed_stat(entry, &metadata);
}
materialized.push(entry.path.as_bytes().to_vec());
} else {
let file_path = worktree_path(worktree_root, entry.path.as_bytes())?;
match fs::symlink_metadata(&file_path) {
Ok(metadata) if !worktree_entry_is_uptodate(entry, &metadata) => {
clear_skip_worktree(entry);
not_up_to_date.push(entry.path.as_bytes().to_vec());
}
_ => {
set_skip_worktree(entry);
remove_worktree_file(worktree_root, entry.path.as_bytes())?;
skipped.push(entry.path.as_bytes().to_vec());
}
}
}
}
not_up_to_date.sort();
normalize_index_version_for_extended_flags(&mut index);
if sparse.sparse_index {
collapse_to_sparse_index(&mut index, &matcher, &db, format)?;
} else {
index.clear_sparse_extension()?;
}
write_repository_index_ref(git_dir, format, &index)?;
Ok(ApplySparseResult {
materialized,
skipped,
not_up_to_date,
})
}
pub fn expand_sparse_index(
index: &mut Index,
db: &FileObjectDatabase,
format: ObjectFormat,
) -> Result<bool> {
if !index.entries.iter().any(IndexEntry::is_sparse_dir) {
let had_marker = index.is_sparse();
index.clear_sparse_extension()?;
if had_marker {
sley_core::trace2::region("index", "ensure_full_index");
}
return Ok(had_marker);
}
let mut expanded: Vec<IndexEntry> = Vec::with_capacity(index.entries.len());
for entry in std::mem::take(&mut index.entries) {
if !entry.is_sparse_dir() {
expanded.push(entry);
continue;
}
let dir = entry.path.as_bytes();
let dir_prefix = dir; for (rel, (mode, oid)) in sley_diff_merge::flatten_tree(db, format, &entry.oid)? {
let mut full_path = dir_prefix.to_vec();
full_path.extend_from_slice(&rel);
let mut blob = blank_sparse_blob_entry(format, &full_path, mode, oid);
blob.set_skip_worktree(true);
expanded.push(blob);
}
}
expanded.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
index.entries = expanded;
index.clear_sparse_extension()?;
normalize_index_version_for_extended_flags(index);
sley_core::trace2::region("index", "ensure_full_index");
Ok(true)
}
pub(crate) fn index_sparse_dir_contains_path(index: &Index, git_path: &[u8]) -> bool {
index.entries.iter().any(|entry| {
entry.is_sparse_dir()
&& git_path.starts_with(entry.path.as_bytes())
&& git_path.len() > entry.path.len()
})
}
pub(crate) fn blank_sparse_blob_entry(
format: ObjectFormat,
path: &[u8],
mode: u32,
oid: ObjectId,
) -> IndexEntry {
let _ = format;
let mut entry = IndexEntry {
ctime_seconds: 0,
ctime_nanoseconds: 0,
mtime_seconds: 0,
mtime_nanoseconds: 0,
dev: 0,
ino: 0,
mode,
uid: 0,
gid: 0,
size: 0,
oid,
flags: 0,
flags_extended: 0,
path: path.into(),
};
entry.refresh_name_length();
entry
}
pub(crate) fn collapse_to_sparse_index(
index: &mut Index,
matcher: &SparseMatcher,
db: &FileObjectDatabase,
format: ObjectFormat,
) -> Result<()> {
if index.entries.iter().any(IndexEntry::is_sparse_dir) {
expand_sparse_index(index, db, format)?;
}
if index.entries.iter().any(|e| index_entry_stage(e) != 0) {
index.clear_sparse_extension()?;
return Ok(());
}
index
.entries
.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
use std::collections::BTreeMap;
let mut dir_has_in_cone: BTreeMap<Vec<u8>, bool> = BTreeMap::new();
for entry in &index.entries {
let path = entry.path.as_bytes();
let in_cone = matcher.includes_file(path);
let mut start = 0usize;
while let Some(rel) = path
.get(start..)
.and_then(|s| s.iter().position(|b| *b == b'/'))
{
let end = start + rel;
let dir = path[..end].to_vec();
let flag = dir_has_in_cone.entry(dir).or_insert(false);
*flag = *flag || in_cone;
start = end + 1;
}
}
let collapsible: Vec<Vec<u8>> = {
let all: Vec<Vec<u8>> = dir_has_in_cone
.iter()
.filter(|(_, has)| !**has)
.map(|(dir, _)| dir.clone())
.collect();
all.iter()
.filter(|dir| {
!all.iter().any(|other| {
other != *dir
&& dir
.strip_prefix(other.as_slice())
.is_some_and(|rest| rest.first() == Some(&b'/'))
})
})
.cloned()
.collect()
};
if collapsible.is_empty() {
index.clear_sparse_extension()?;
return Ok(());
}
let mut checker = db.presence_checker();
let mut new_entries: Vec<IndexEntry> = Vec::with_capacity(index.entries.len());
let mut consumed: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
for dir in &collapsible {
let mut subtree: Vec<&IndexEntry> = index
.entries
.iter()
.filter(|e| {
e.path
.as_bytes()
.strip_prefix(dir.as_slice())
.is_some_and(|rest| rest.first() == Some(&b'/'))
})
.collect();
if subtree.is_empty() {
continue;
}
subtree.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
let mut prefix = dir.clone();
prefix.push(b'/');
let tree_entries: Vec<WriteTreeEntry<'_>> = subtree
.iter()
.map(|e| WriteTreeEntry {
path: e.path.as_bytes(),
mode: e.mode,
oid: e.oid.clone(),
})
.collect();
let tree_oid =
write_tree_entries_stream(&tree_entries, &prefix, None, db, &mut checker, false)?;
for e in &subtree {
consumed.insert(e.path.as_bytes().to_vec());
}
let mut sparse_path = dir.clone();
sparse_path.push(b'/');
let mut sparse_entry =
blank_sparse_blob_entry(format, &sparse_path, SPARSE_DIR_MODE, tree_oid);
sparse_entry.set_skip_worktree(true);
new_entries.push(sparse_entry);
}
for entry in &index.entries {
if consumed.contains(entry.path.as_bytes()) {
continue;
}
new_entries.push(entry.clone());
}
new_entries.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
index.entries = new_entries;
index.set_sparse_extension();
normalize_index_version_for_extended_flags(index);
sley_core::trace2::region("index", "convert_to_sparse");
Ok(())
}
pub(crate) fn worktree_entry_is_uptodate(entry: &IndexEntry, metadata: &fs::Metadata) -> bool {
if u64::from(entry.size) != metadata.len() {
return false;
}
let Some((mtime_seconds, mtime_nanoseconds)) = file_mtime_parts(metadata) else {
return false;
};
u64::from(entry.mtime_seconds) == mtime_seconds
&& u64::from(entry.mtime_nanoseconds) == mtime_nanoseconds
}
pub(crate) fn worktree_entry_ref_is_uptodate(
entry: &IndexEntryRef<'_>,
metadata: &fs::Metadata,
) -> bool {
if u64::from(entry.size) != metadata.len() {
return false;
}
let Some((mtime_seconds, mtime_nanoseconds)) = file_mtime_parts(metadata) else {
return false;
};
u64::from(entry.mtime_seconds) == mtime_seconds
&& u64::from(entry.mtime_nanoseconds) == mtime_nanoseconds
}
pub(crate) fn file_mtime_parts(metadata: &fs::Metadata) -> Option<(u64, u64)> {
let modified = metadata.modified().ok()?;
let duration = modified.duration_since(UNIX_EPOCH).ok()?;
Some((duration.as_secs(), u64::from(duration.subsec_nanos())))
}
pub fn write_metadata_file_atomic(
path: impl AsRef<Path>,
bytes: &[u8],
options: AtomicMetadataWriteOptions,
) -> Result<AtomicMetadataWriteResult> {
let path = path.as_ref();
let parent = path.parent().ok_or_else(|| {
GitError::InvalidPath(format!("metadata path has no parent: {}", path.display()))
})?;
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)?;
}
let lock_path = metadata_lock_path(path)?;
let mut lock = match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path)
{
Ok(lock) => lock,
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
return Err(GitError::Transaction(format!(
"metadata lock already exists: {}",
lock_path.display()
)));
}
Err(err) => return Err(err.into()),
};
if let Err(err) = lock.write_all(bytes) {
let _ = fs::remove_file(&lock_path);
return Err(err.into());
}
if options.fsync_file
&& let Err(err) = lock.sync_all()
{
let _ = fs::remove_file(&lock_path);
return Err(err.into());
}
drop(lock);
if let Err(err) = fs::rename(&lock_path, path) {
let _ = fs::remove_file(&lock_path);
return Err(err.into());
}
if options.fsync_dir
&& let Ok(dir) = fs::File::open(parent)
{
dir.sync_all()?;
}
let metadata = fs::metadata(path)?;
Ok(AtomicMetadataWriteResult {
path: path.to_path_buf(),
len: metadata.len(),
mtime: file_mtime_parts(&metadata),
})
}
pub(crate) fn metadata_lock_path(path: &Path) -> Result<PathBuf> {
let file_name = path.file_name().ok_or_else(|| {
GitError::InvalidPath(format!("metadata path has no filename: {}", path.display()))
})?;
let mut lock_name = file_name.to_os_string();
lock_name.push(".lock");
Ok(path.with_file_name(lock_name))
}
pub fn checkout_detached_sparse(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
target: &ObjectId,
committer: Vec<u8>,
message: Vec<u8>,
sparse: &SparseCheckout,
) -> Result<CheckoutResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let files = checkout_commit_to_index_and_worktree_sparse(
worktree_root,
git_dir,
format,
target,
Some((sparse, SparseCheckoutMode::Auto)),
None,
None,
)?;
let refs = FileRefStore::new(git_dir, format);
let zero = ObjectId::null(format);
let mut tx = refs.transaction();
tx.update(RefUpdate {
name: "HEAD".into(),
expected: None,
new: RefTarget::Direct(*target),
reflog: Some(ReflogEntry {
old_oid: zero,
new_oid: *target,
committer,
message,
}),
});
tx.commit()?;
Ok(CheckoutResult {
branch: target.to_string(),
oid: *target,
files,
})
}
pub(crate) fn materialize_index_entry_file(
db: &FileObjectDatabase,
worktree_root: &Path,
file_path: &Path,
entry: &IndexEntry,
) -> Result<()> {
if sley_index::is_gitlink(entry.mode) {
materialize_gitlink_dir(worktree_root, file_path)?;
return Ok(());
}
let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
prepare_blob_parent_dirs(worktree_root, file_path)?;
remove_existing_worktree_path(file_path)?;
write_blob_body_or_symlink(file_path, entry.mode, &object.body, &object.body)?;
Ok(())
}
pub(crate) fn set_skip_worktree(entry: &mut IndexEntry) {
entry.flags |= INDEX_FLAG_EXTENDED;
entry.flags_extended |= INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
}
pub(crate) fn clear_skip_worktree(entry: &mut IndexEntry) {
entry.flags_extended &= !INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
if entry.flags_extended == 0 {
entry.flags &= !INDEX_FLAG_EXTENDED;
}
}
pub fn restore_worktree_paths_from_head(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
paths: &[PathBuf],
) -> Result<RestoreResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
let index = if index_path.exists() {
Index::parse(&fs::read(&index_path)?, format)?
} else {
Index {
version: 2,
entries: Vec::new(),
extensions: Vec::new(),
checksum: None,
}
};
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let head_entries = head_tree_entries(git_dir, format, &db)?;
restore_worktree_paths_from_entries(worktree_root, &db, index, &head_entries, paths)
}
pub fn restore_worktree_paths_from_tree(
worktree_root: impl AsRef<Path>,
git_dir: impl AsRef<Path>,
format: ObjectFormat,
tree_oid: &ObjectId,
paths: &[PathBuf],
) -> Result<RestoreResult> {
let worktree_root = worktree_root.as_ref();
let git_dir = git_dir.as_ref();
let index_path = repository_index_path(git_dir);
let index = if index_path.exists() {
Index::parse(&fs::read(&index_path)?, format)?
} else {
Index {
version: 2,
entries: Vec::new(),
extensions: Vec::new(),
checksum: None,
}
};
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let source_entries = tree_entries(&db, format, tree_oid)?;
restore_worktree_paths_from_entries(worktree_root, &db, index, &source_entries, paths)
}
pub(crate) fn restore_worktree_paths_from_entries(
worktree_root: &Path,
db: &FileObjectDatabase,
index: Index,
source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
paths: &[PathBuf],
) -> Result<RestoreResult> {
let index_entries = index
.entries
.into_iter()
.map(|entry| entry.path.into_bytes())
.collect::<BTreeSet<_>>();
let mut restored = BTreeSet::new();
for path in paths {
let absolute = if path.is_absolute() {
path.clone()
} else {
worktree_root.join(path)
};
let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
})?;
let git_path = git_path_bytes(relative)?;
let recursive = path == Path::new(".")
|| path.to_string_lossy().ends_with('/')
|| absolute.is_dir()
|| index_entries
.iter()
.any(|entry| index_entry_is_under_path(entry, &git_path))
|| source_entries
.keys()
.any(|entry| index_entry_is_under_path(entry, &git_path));
let mut matched_paths = BTreeSet::new();
for path in index_entries.iter().chain(source_entries.keys()) {
if path.as_slice() == git_path.as_slice()
|| (recursive && index_entry_is_under_path(path, &git_path))
{
matched_paths.insert(path.clone());
}
}
if matched_paths.is_empty() {
eprintln!(
"error: pathspec '{}' did not match any file(s) known to git",
path.display()
);
return Err(GitError::Exit(1));
}
for path in matched_paths {
if let Some(entry) = source_entries.get(&path) {
restore_head_entry_to_worktree(worktree_root, db, &path, entry)?;
} else {
remove_worktree_file(worktree_root, &path)?;
}
restored.insert(path);
}
}
Ok(RestoreResult {
restored: restored.len(),
})
}