Merge pull request #769 from gitbutlerapp/can-commit-binary-files

Binary diff support
This commit is contained in:
Scott Chacon 2023-07-21 08:57:34 +02:00 committed by GitHub
commit ede0c571f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 242 additions and 56 deletions

View File

@ -661,7 +661,6 @@ impl Repository {
}
let no_commiter_mark = config.get_string("gitbutler.utmostDiscretion");
dbg!(&no_commiter_mark);
if no_commiter_mark.is_ok() && no_commiter_mark? == "1" {
committer = author.clone();
}

View File

@ -4,12 +4,14 @@ use anyhow::{Context, Result};
use super::Repository;
#[derive(Debug, PartialEq, Clone)]
pub struct Hunk {
pub old_start: usize,
pub old_lines: usize,
pub new_start: usize,
pub new_lines: usize,
pub diff: String,
pub binary: bool,
}
pub struct Options {
@ -37,15 +39,15 @@ pub fn workdir(
diff_opts
.recurse_untracked_dirs(true)
.include_untracked(true)
.show_binary(true)
.show_untracked_content(true);
diff_opts.context_lines(opts.context_lines);
let diff = repository
.git_repository
.diff_tree_to_workdir(Some(&tree), Some(&mut diff_opts))?;
let repo = &repository.git_repository;
let diff = repo.diff_tree_to_workdir(Some(&tree), Some(&mut diff_opts))?;
hunks_by_filepath(&diff)
hunks_by_filepath(repo, &diff)
}
pub fn trees(
@ -56,17 +58,19 @@ pub fn trees(
let mut opts = git2::DiffOptions::new();
opts.recurse_untracked_dirs(true)
.include_untracked(true)
.show_binary(true)
.show_untracked_content(true);
let diff =
repository
.git_repository
.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)?;
let repo = &repository.git_repository;
let diff = repo.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)?;
hunks_by_filepath(&diff)
hunks_by_filepath(repo, &diff)
}
fn hunks_by_filepath(diff: &git2::Diff) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> {
fn hunks_by_filepath(
repo: &git2::Repository,
diff: &git2::Diff,
) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> {
// find all the hunks
let mut hunks_by_filepath: HashMap<path::PathBuf, Vec<Hunk>> = HashMap::new();
let mut current_diff = String::new();
@ -77,6 +81,7 @@ fn hunks_by_filepath(diff: &git2::Diff) -> Result<HashMap<path::PathBuf, Vec<Hun
let mut current_new_lines: Option<usize> = None;
let mut current_old_start: Option<usize> = None;
let mut current_old_lines: Option<usize> = None;
let mut current_binary = false;
diff.print(git2::DiffFormat::Patch, |delta, hunk, line| {
let file_path = delta.new_file().path().unwrap_or_else(|| {
@ -101,7 +106,12 @@ fn hunks_by_filepath(diff: &git2::Diff) -> Result<HashMap<path::PathBuf, Vec<Hun
hunk.old_lines(),
)
} else {
return true;
if line.origin() == 'B' {
let hunk_id = format!("{:?}:{}", file_path.as_os_str(), delta.new_file().id());
(hunk_id.clone(), 0, 0, 0, 0)
} else {
return true;
}
};
let is_path_changed = if current_file_path.is_none() {
@ -124,6 +134,7 @@ fn hunks_by_filepath(diff: &git2::Diff) -> Result<HashMap<path::PathBuf, Vec<Hun
new_start: current_new_start.unwrap(),
new_lines: current_new_lines.unwrap(),
diff: current_diff.clone(),
binary: current_binary,
});
current_diff = String::new();
}
@ -133,7 +144,27 @@ fn hunks_by_filepath(diff: &git2::Diff) -> Result<HashMap<path::PathBuf, Vec<Hun
_ => {}
}
current_diff.push_str(std::str::from_utf8(line.content()).unwrap());
// is this a binary patch or a hunk?
match line.origin() {
'B' => {
// save the file_path to the odb
if !delta.new_file().id().is_zero() {
// the binary file wasnt deleted
let full_path = repo.workdir().unwrap().join(file_path);
let blob_oid = repo.blob_path(full_path.as_path()).unwrap();
if blob_oid != delta.new_file().id() {
return false;
}
}
current_diff.push_str(&format!("{}", delta.new_file().id()));
current_binary = true;
}
_ => {
current_diff.push_str(std::str::from_utf8(line.content()).unwrap());
current_binary = false;
}
}
current_file_path = Some(file_path.to_path_buf());
current_hunk_id = Some(hunk_id);
current_new_start = Some(new_start as usize);
@ -153,6 +184,7 @@ fn hunks_by_filepath(diff: &git2::Diff) -> Result<HashMap<path::PathBuf, Vec<Hun
new_start: current_new_start.unwrap(),
new_lines: current_new_lines.unwrap(),
diff: current_diff,
binary: current_binary,
});
}

View File

@ -84,6 +84,7 @@ pub struct VirtualBranchFile {
pub hunks: Vec<VirtualBranchHunk>,
pub modified_at: u128,
pub conflicted: bool,
pub binary: bool,
}
// this struct is a mapping to the view `Hunk` type in Typescript
@ -104,6 +105,7 @@ pub struct VirtualBranchHunk {
pub hash: String,
pub start: usize,
pub end: usize,
pub binary: bool,
}
// this struct is a mapping to the view `RemoteBranch` type in Typescript
@ -766,6 +768,7 @@ pub fn list_virtual_branches(
id: file_path.display().to_string(),
path: file_path.clone(),
hunks: hunks.clone(),
binary: hunks.iter().any(|h| h.binary),
modified_at: hunks.iter().map(|h| h.modified_at).max().unwrap_or(0),
conflicted: conflicts::is_conflicting(
project_repository,
@ -1233,6 +1236,7 @@ fn hunks_by_filepath(
diff: hunk.diff.clone(),
start: hunk.new_start,
end: hunk.new_start + hunk.new_lines,
binary: hunk.binary,
hash: diff_hash(&hunk.diff),
})
.collect::<Vec<_>>();
@ -1446,6 +1450,7 @@ pub fn get_status_by_branch(
id: file_path.display().to_string(),
path: file_path.clone(),
hunks: hunks.clone(),
binary: hunks.iter().any(|h| h.binary),
modified_at: hunks.iter().map(|h| h.modified_at).max().unwrap_or(0),
conflicted: conflicts::is_conflicting(
project_repository,
@ -1925,33 +1930,40 @@ fn write_tree(
let blob_oid = git_repository.blob(bytes)?;
builder.upsert(rel_path, blob_oid, filemode);
} else if let Ok(tree_entry) = base_tree.get_path(rel_path) {
// blob from tree_entry
let blob = tree_entry
.to_object(git_repository)
.unwrap()
.peel_to_blob()
.context("failed to get blob")?;
if file.binary {
let new_blob_oid = &file.hunks[0].diff;
// convert string to Oid
let new_blob_oid = git2::Oid::from_str(&new_blob_oid)?;
builder.upsert(rel_path, new_blob_oid, filemode);
} else {
// blob from tree_entry
let blob = tree_entry
.to_object(git_repository)
.unwrap()
.peel_to_blob()
.context("failed to get blob")?;
// get the contents
let blob_contents = blob.content();
// get the contents
let blob_contents = blob.content();
let mut patch = "--- original\n+++ modified\n".to_string();
let mut patch = "--- original\n+++ modified\n".to_string();
let mut hunks = file.hunks.to_vec();
hunks.sort_by_key(|hunk| hunk.start);
for hunk in hunks {
patch.push_str(&hunk.diff);
let mut hunks = file.hunks.to_vec();
hunks.sort_by_key(|hunk| hunk.start);
for hunk in hunks {
patch.push_str(&hunk.diff);
}
// apply patch to blob_contents
let patch_bytes = patch.as_bytes();
let patch = Patch::from_bytes(patch_bytes)?;
let new_content = apply_bytes(blob_contents, &patch)?;
// create a blob
let new_blob_oid = git_repository.blob(&new_content)?;
// upsert into the builder
builder.upsert(rel_path, new_blob_oid, filemode);
}
// apply patch to blob_contents
let patch_bytes = patch.as_bytes();
let patch = Patch::from_bytes(patch_bytes)?;
let new_content = apply_bytes(blob_contents, &patch)?;
// create a blob
let new_blob_oid = git_repository.blob(&new_content)?;
// upsert into the builder
builder.upsert(rel_path, new_blob_oid, filemode);
} else {
// create a git blob from a file on disk
let blob_oid = git_repository.blob_path(&full_path)?;
@ -1976,9 +1988,11 @@ fn _print_tree(repo: &git2::Repository, tree: &git2::Tree) -> Result<()> {
let object = entry.to_object(repo).context("failed to get object")?;
let blob = object.as_blob().context("failed to get blob")?;
// convert content to string
let content =
std::str::from_utf8(blob.content()).context("failed to convert content to string")?;
println!(" blob: {:?}", content);
if let Ok(content) = std::str::from_utf8(blob.content()) {
println!(" blob: {:?}", content);
} else {
println!(" blob: BINARY");
}
}
Ok(())
}

View File

@ -27,6 +27,7 @@ fn commit_all(repository: &git2::Repository) -> Result<git2::Oid> {
fn test_repository() -> Result<git2::Repository> {
let path = tempdir()?.path().to_str().unwrap().to_string();
//dbg!(&path);
let repository = git2::Repository::init(path)?;
repository.remote_add_fetch("origin/master", "master")?;
let mut index = repository.index()?;
@ -119,6 +120,124 @@ fn test_commit_on_branch_then_change_file_then_get_status() -> Result<()> {
Ok(())
}
#[test]
fn test_track_binary_files() -> Result<()> {
let repository = test_repository()?;
let project = projects::Project::try_from(&repository)?;
let gb_repo_path = tempdir()?.path().to_str().unwrap().to_string();
let storage = storage::Storage::from_path(tempdir()?.path());
let user_store = users::Storage::new(storage.clone());
let project_store = projects::Storage::new(storage);
project_store.add_project(&project)?;
let gb_repo = gb_repository::Repository::open(
gb_repo_path,
project.id.clone(),
project_store,
user_store,
)?;
let project_repository = project_repository::Repository::open(&project)?;
let file_path = std::path::Path::new("test.txt");
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\n",
)?;
let file_path2 = std::path::Path::new("test2.txt");
std::fs::write(
std::path::Path::new(&project.path).join(file_path2),
"line5\nline6\nline7\nline8\n",
)?;
// add a binary file
let image_data: [u8; 12] = [
255, 0, 0, // Red pixel
0, 0, 255, // Blue pixel
255, 255, 0, // Yellow pixel
0, 255, 0, // Green pixel
];
let mut file = fs::File::create(std::path::Path::new(&project.path).join("image.bin"))?;
file.write_all(&image_data)?;
commit_all(&repository)?;
target::Writer::new(&gb_repo).write_default(&target::Target {
remote_name: "origin".to_string(),
branch_name: "master".to_string(),
remote_url: "origin".to_string(),
sha: repository.head().unwrap().target().unwrap(),
behind: 0,
})?;
update_gitbutler_integration(&gb_repo, &project_repository)?;
let branch1_id = create_virtual_branch(&gb_repo, &BranchCreateRequest::default())
.expect("failed to create virtual branch")
.id;
// test file change
std::fs::write(
std::path::Path::new(&project.path).join(file_path2),
"line5\nline6\nline7\nline8\nline9\n",
)?;
// add a binary file
let image_data: [u8; 12] = [
255, 0, 0, // Red pixel
0, 255, 0, // Green pixel
0, 0, 255, // Blue pixel
255, 255, 0, // Yellow pixel
];
let mut file = fs::File::create(std::path::Path::new(&project.path).join("image.bin"))?;
file.write_all(&image_data)?;
let branches = list_virtual_branches(&gb_repo, &project_repository, true)?;
let branch = &branches[0];
assert_eq!(branch.files.len(), 2);
let img_file = &branch
.files
.iter()
.find(|b| b.path.as_os_str() == "image.bin")
.unwrap();
assert_eq!(img_file.binary, true);
assert_eq!(
img_file.hunks[0].diff,
"944996dd82015a616247c72b251e41661e528ae1"
);
// commit
commit(&gb_repo, &project_repository, &branch1_id, "test commit")?;
// status (no files)
let branches = list_virtual_branches(&gb_repo, &project_repository, true)?;
let commit_id = &branches[0].commits[0].id;
let commit_obj = repository.find_commit(git2::Oid::from_str(commit_id).unwrap())?;
let tree = commit_obj.tree()?;
let files = tree_to_entry_list(&repository, &tree)?;
assert_eq!(files[0].0, "image.bin");
assert_eq!(files[0].3, "944996dd82015a616247c72b251e41661e528ae1");
let image_data: [u8; 12] = [
0, 255, 0, // Green pixel
255, 0, 0, // Red pixel
255, 255, 0, // Yellow pixel
0, 0, 255, // Blue pixel
];
let mut file = fs::File::create(std::path::Path::new(&project.path).join("image.bin"))?;
file.write_all(&image_data)?;
// commit
commit(&gb_repo, &project_repository, &branch1_id, "test commit")?;
let branches = list_virtual_branches(&gb_repo, &project_repository, true)?;
let commit_id = &branches[0].commits[0].id;
// get tree from commit_id
let commit_obj = repository.find_commit(git2::Oid::from_str(commit_id).unwrap())?;
let tree = commit_obj.tree()?;
let files = tree_to_entry_list(&repository, &tree)?;
assert_eq!(files[0].0, "image.bin");
assert_eq!(files[0].3, "ea6901a04d1eed6ebf6822f4360bda9f008fa317");
Ok(())
}
#[test]
fn test_create_branch_with_ownership() -> Result<()> {
let repository = test_repository()?;
@ -2533,7 +2652,7 @@ fn tree_to_file_list(repository: &git2::Repository, tree: &git2::Tree) -> Result
fn tree_to_entry_list(
repository: &git2::Repository,
tree: &git2::Tree,
) -> Result<Vec<(String, String, String)>> {
) -> Result<Vec<(String, String, String, String)>> {
let mut file_list = Vec::new();
for entry in tree.iter() {
let path = entry.name().unwrap();
@ -2545,10 +2664,24 @@ fn tree_to_entry_list(
.context(format!("failed to get object for tree entry {}", path))?;
let blob = object.as_blob().context("failed to get blob")?;
// convert content to string
let content =
std::str::from_utf8(blob.content()).context("failed to convert content to string")?;
let octal_mode = format!("{:o}", entry.filemode());
file_list.push((path.to_string(), octal_mode, content.to_string()));
if let Ok(content) =
std::str::from_utf8(blob.content()).context("failed to convert content to string")
{
file_list.push((
path.to_string(),
octal_mode,
content.to_string(),
blob.id().to_string(),
));
} else {
file_list.push((
path.to_string(),
octal_mode,
"BINARY".to_string(),
blob.id().to_string(),
));
}
}
Ok(file_list)
}

View File

@ -21,6 +21,7 @@ export class File {
modifiedAt!: Date;
conflicted!: boolean;
content!: string;
binary!: boolean;
}
export class Branch {

View File

@ -84,7 +84,7 @@
class="flex w-full flex-col justify-center gap-2 border-b border-t border-light-400 bg-light-50 py-1 text-light-900 dark:border-dark-400 dark:bg-dark-800 dark:text-light-300"
>
<div
class="flex pl-2 cursor-default"
class="flex cursor-default pl-2"
role="button"
tabindex="0"
on:dblclick={() => {
@ -116,10 +116,12 @@
tabindex="0"
class="cursor-pointer px-3 py-2 text-light-600 dark:text-dark-200"
>
{#if expanded}
<IconTriangleUp />
{:else}
<IconTriangleDown />
{#if !file.binary}
{#if expanded}
<IconTriangleUp />
{:else}
<IconTriangleDown />
{/if}
{/if}
</div>
</div>

View File

@ -15,7 +15,7 @@
<div
id="new-branch-dz"
role="group"
class="h-full pt-6 flex w-[22.5rem] shrink-0 justify-center text-center text-light-800 dark:text-dark-100"
class="flex h-full w-[22.5rem] shrink-0 justify-center pt-6 text-center text-light-800 dark:text-dark-100"
use:dzHighlight={{ type: 'text/hunk', hover: 'drop-zone-hover', active: 'drop-zone-active' }}
on:drop|stopPropagation={(e) => {
if (!e.dataTransfer) {
@ -33,7 +33,7 @@
</Button>
</div>
</div>
<div class="drop-zone-marker h-36 hidden border border-green-450 p-8">
<div class="drop-zone-marker hidden h-36 border border-green-450 p-8">
<div class="flex h-full flex-col items-center self-center p-2">
<p>Drop here to create new virtual branch</p>
</div>

View File

@ -43,7 +43,7 @@
{line.afterLineNumber || ''}
</div>
<div
class="flex-grow overflow-hidden pl-1 whitespace-pre"
class="flex-grow overflow-hidden whitespace-pre pl-1"
class:diff-line-deletion={sectionType === SectionType.RemovedLines}
class:diff-line-addition={sectionType === SectionType.AddedLines}
>

View File

@ -405,7 +405,8 @@ test('parses file with one hunk and balanced add-remove', () => {
expanded: true,
modifiedAt: new Date(2021, 1, 1),
conflicted: false,
content: fileContent
content: fileContent,
binary: false
};
const sections = parseFileSections(file);
expect(sections.length).toBe(3);
@ -462,7 +463,8 @@ test('parses file with one hunk with more added than removed', () => {
expanded: true,
modifiedAt: new Date(2021, 1, 1),
conflicted: false,
content: fileContent
content: fileContent,
binary: false
};
const sections = parseFileSections(file);
expect(sections.length).toBe(3);
@ -521,7 +523,8 @@ test('parses file with two hunks ordered by position in file', () => {
expanded: true,
modifiedAt: new Date(2021, 1, 1),
conflicted: false,
content: fileContent
content: fileContent,
binary: false
};
const sections = parseFileSections(file);
expect(sections.length).toBe(3);
@ -592,7 +595,8 @@ test('parses whole file deleted', () => {
expanded: true,
modifiedAt: new Date(2021, 1, 1),
conflicted: false,
content: fileContent
content: fileContent,
binary: false
};
const sections = parseFileSections(file);
expect(sections.length).toBe(1);
@ -617,7 +621,8 @@ test('parses new file created', () => {
expanded: true,
modifiedAt: new Date(2021, 1, 1),
conflicted: false,
content: fileContent
content: fileContent,
binary: false
};
const sections = parseFileSections(file);
expect(sections.length).toBe(1);