Support executable blobs

This commit is contained in:
Caleb Owens 2024-09-30 17:34:20 +02:00
parent 6d52977202
commit 511ca5212e
5 changed files with 159 additions and 15 deletions

2
Cargo.lock generated
View File

@ -2624,12 +2624,14 @@ dependencies = [
"gitbutler-branch",
"gitbutler-branch-actions",
"gitbutler-command-context",
"gitbutler-oxidize",
"gitbutler-project",
"gitbutler-reference",
"gitbutler-repo",
"gitbutler-storage",
"gitbutler-url",
"gitbutler-user",
"gix",
"gix-testtools",
"keyring",
"once_cell",

View File

@ -2,7 +2,6 @@
use std::os::unix::fs::PermissionsExt;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use std::path::PathBuf;
use std::{io::Write, path::Path, process::Stdio, str};
use crate::{Config, LogUntil};
@ -17,6 +16,7 @@ use gitbutler_oxidize::{
git2_signature_to_gix_signature, git2_to_gix_object_id, gix_to_git2_oid, gix_to_git2_signature,
};
use gitbutler_reference::{Refname, RemoteRefname};
use gix::fs::is_executable;
use gix::objs::WriteTo;
use tracing::instrument;
@ -196,11 +196,21 @@ impl RepositoryExt for git2::Repository {
} else if (status.is_index_deleted() && !status.is_wt_new()) || status.is_wt_deleted() {
tree_update_builder.remove(path);
} else {
let file_path = worktree_path.join(path);
let file = std::fs::read(file_path)?;
let blob = self.blob(&file)?;
let file_path = worktree_path.join(path).to_owned();
tree_update_builder.upsert(path, blob, git2::FileMode::Blob);
if file_path.is_symlink() {
} else {
let file = std::fs::read(&file_path)?;
let blob = self.blob(&file)?;
let file_type = if is_executable(&file_path.metadata()?) {
git2::FileMode::BlobExecutable
} else {
git2::FileMode::Blob
};
tree_update_builder.upsert(path, blob, file_type);
}
}
}

View File

@ -1,7 +1,12 @@
use std::path::Path;
use std::{
fs::{File, Permissions},
path::Path,
};
use gitbutler_repo::RepositoryExt as _;
use gitbutler_testsupport::testing_repository::{assert_tree_matches, TestingRepository};
use gitbutler_testsupport::testing_repository::{
assert_tree_matches, assert_tree_matches_with_mode, EntryAttribute, TestingRepository,
};
/// These tests exercise the truth table that we use to update the HEAD
/// tree to match the worktree.
@ -19,7 +24,6 @@ use gitbutler_testsupport::testing_repository::{assert_tree_matches, TestingRepo
/// | modify | modify | upsert |
#[cfg(test)]
mod head_upsert_truthtable {
use super::*;
// | add | delete | no-action |
@ -410,3 +414,77 @@ fn should_not_change_index() {
&[("file1.txt", b"content1")],
);
}
#[test]
fn tree_behavior() {
let test_repository = TestingRepository::open();
let commit = test_repository.commit_tree(
None,
&[
("dir1/file1.txt", "content1"),
("dir2/file2.txt", "content2"),
],
);
test_repository
.repository
.branch("master", &commit, true)
.unwrap();
// Update a file in a directory
std::fs::write(
test_repository.tempdir.path().join("dir1/file1.txt"),
"new1",
)
.unwrap();
// Make a new directory and file
std::fs::create_dir(test_repository.tempdir.path().join("dir3")).unwrap();
std::fs::write(
test_repository.tempdir.path().join("dir3/file1.txt"),
"new2",
)
.unwrap();
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
assert_tree_matches(
&test_repository.repository,
&tree,
&[
("dir1/file1.txt", b"new1"),
("dir2/file2.txt", b"content2"),
("dir3/file1.txt", b"new2"),
],
);
}
#[cfg(unix)]
#[test]
fn executable_blobs() {
use std::{io::Write, os::unix::fs::PermissionsExt as _};
let test_repository = TestingRepository::open();
let commit = test_repository.commit_tree(None, &[]);
test_repository
.repository
.branch("master", &commit, true)
.unwrap();
let mut file = File::create(test_repository.tempdir.path().join("file1.txt")).unwrap();
file.set_permissions(Permissions::from_mode(0o755)).unwrap();
file.write_all(b"content1").unwrap();
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
assert_tree_matches_with_mode(
&test_repository.repository,
tree.id(),
&[(
"file1.txt",
b"content1",
&[EntryAttribute::Blob, EntryAttribute::Executable],
)],
);
}

View File

@ -17,6 +17,7 @@ tempfile = "3.10.1"
keyring.workspace = true
serde_json = "1.0"
gix-testtools = "0.15.0"
gix.workspace = true
parking_lot.workspace = true
gitbutler-branch-actions = { path = "../gitbutler-branch-actions" }
gitbutler-repo = { path = "../gitbutler-repo" }
@ -27,3 +28,4 @@ gitbutler-branch.workspace = true
gitbutler-reference.workspace = true
gitbutler-storage.workspace = true
gitbutler-url.workspace = true
gitbutler-oxidize.workspace = true

View File

@ -1,5 +1,6 @@
use std::{fs, path::Path};
use std::fs;
use gitbutler_oxidize::git2_to_gix_object_id;
use gix_testtools::bstr::ByteSlice as _;
use tempfile::{tempdir, TempDir};
@ -50,7 +51,14 @@ impl TestingRepository {
}
// Write any files
for (file_name, contents) in files {
fs::write(self.tempdir.path().join(file_name), contents).unwrap();
let path = self.tempdir.path().join(file_name);
let parent = path.parent().unwrap();
if !parent.exists() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, contents).unwrap();
}
// Update the index
@ -94,16 +102,60 @@ pub fn assert_tree_matches<'a>(
tree: &git2::Tree<'a>,
files: &[(&str, &[u8])],
) {
for (path, content) in files {
let blob = tree.get_path(Path::new(path)).unwrap().id();
let blob: git2::Blob<'a> = repository.find_blob(blob).unwrap();
assert_tree_matches_with_mode(
repository,
tree.id(),
&files
.iter()
.map(|(path, content)| (*path, *content, &[] as &[EntryAttribute]))
.collect::<Vec<_>>(),
);
}
#[derive(Debug, Copy, Clone)]
pub enum EntryAttribute {
Tree,
Commit,
Link,
Blob,
Executable,
}
pub fn assert_tree_matches_with_mode(
repository: &git2::Repository,
tree: git2::Oid,
files: &[(&str, &[u8], &[EntryAttribute])],
) {
let gix_repository = gix::open(repository.path()).unwrap();
let tree = gix_repository
.find_tree(git2_to_gix_object_id(tree))
.unwrap();
for (path, content, entry_attributes) in files {
let tree_entry = tree.lookup_entry_by_path(path).unwrap().unwrap();
let object = gix_repository.find_object(tree_entry.id()).unwrap();
assert_eq!(
blob.content(),
object.data,
*content,
"{}: expect {} == {}",
path,
blob.content().to_str_lossy(),
object.data.to_str_lossy(),
content.to_str_lossy()
);
for entry_attribute in *entry_attributes {
match entry_attribute {
EntryAttribute::Tree => assert!(tree_entry.mode().is_tree(), "{} is a tree", path),
EntryAttribute::Commit => {
assert!(tree_entry.mode().is_commit(), "{} is a commit", path)
}
EntryAttribute::Link => assert!(tree_entry.mode().is_link(), "{} is a link", path),
EntryAttribute::Blob => assert!(tree_entry.mode().is_blob(), "{} is a blob", path),
EntryAttribute::Executable => {
assert!(tree_entry.mode().is_executable(), "{} is executable", path)
}
}
}
}
}