mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2025-01-07 02:11:11 +03:00
Merge pull request #4993 from gitbutlerapp/fix-create-wd-tree
Use new git2 implementation
This commit is contained in:
commit
7422110a6d
@ -327,12 +327,14 @@ impl BranchManager<'_> {
|
|||||||
|
|
||||||
// We don't support having two branches applied that conflict with each other
|
// We don't support having two branches applied that conflict with each other
|
||||||
{
|
{
|
||||||
let mut index = repo.index()?;
|
let uncommited_changes_tree = repo.create_wd_tree()?;
|
||||||
index.add_all(["*"], git2::IndexAddOption::default(), None)?;
|
|
||||||
let index_tree = index.write_tree()?;
|
|
||||||
let index_tree = repo.find_tree(index_tree)?;
|
|
||||||
let branch_merged_with_other_applied_branches = repo
|
let branch_merged_with_other_applied_branches = repo
|
||||||
.merge_trees(&merge_base_tree, &branch_tree, &index_tree, None)
|
.merge_trees(
|
||||||
|
&merge_base_tree,
|
||||||
|
&branch_tree,
|
||||||
|
&uncommited_changes_tree,
|
||||||
|
None,
|
||||||
|
)
|
||||||
.context("failed to merge trees")?;
|
.context("failed to merge trees")?;
|
||||||
|
|
||||||
if branch_merged_with_other_applied_branches.has_conflicts() {
|
if branch_merged_with_other_applied_branches.has_conflicts() {
|
||||||
|
@ -60,3 +60,103 @@ pub(crate) fn checkout_branch_trees<'a>(
|
|||||||
Ok(final_tree)
|
Ok(final_tree)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These possibly could be considered more "integration" tests, but there is no
|
||||||
|
// need for `checkout_branch_trees` to be public, so it is tested here.
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use bstr::ByteSlice as _;
|
||||||
|
use gitbutler_branch::BranchCreateRequest;
|
||||||
|
use gitbutler_command_context::CommandContext;
|
||||||
|
use gitbutler_repo::RepositoryExt as _;
|
||||||
|
use gitbutler_testsupport::{paths, testing_repository::assert_tree_matches, TestProject};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn checkout_with_two_branches() {
|
||||||
|
let test_project = &TestProject::default();
|
||||||
|
|
||||||
|
let data_dir = paths::data_dir();
|
||||||
|
let projects = gitbutler_project::Controller::from_path(data_dir.path());
|
||||||
|
|
||||||
|
let project = projects
|
||||||
|
.add(test_project.path())
|
||||||
|
.expect("failed to add project");
|
||||||
|
|
||||||
|
crate::set_base_branch(&project, &"refs/remotes/origin/master".parse().unwrap()).unwrap();
|
||||||
|
|
||||||
|
let branch_1 =
|
||||||
|
crate::create_virtual_branch(&project, &BranchCreateRequest::default()).unwrap();
|
||||||
|
|
||||||
|
fs::write(test_project.path().join("foo.txt"), "content").unwrap();
|
||||||
|
|
||||||
|
crate::create_commit(&project, branch_1, "commit one", None, false).unwrap();
|
||||||
|
|
||||||
|
let branch_2 =
|
||||||
|
crate::create_virtual_branch(&project, &BranchCreateRequest::default()).unwrap();
|
||||||
|
|
||||||
|
fs::write(test_project.path().join("bar.txt"), "content").unwrap();
|
||||||
|
|
||||||
|
crate::create_commit(&project, branch_2, "commit two", None, false).unwrap();
|
||||||
|
|
||||||
|
let tree = test_project.local_repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
// Assert original state
|
||||||
|
assert_tree_matches(
|
||||||
|
&test_project.local_repository,
|
||||||
|
&tree,
|
||||||
|
&[("foo.txt", b"content"), ("bar.txt", b"content")],
|
||||||
|
);
|
||||||
|
assert_eq!(tree.len(), 2);
|
||||||
|
|
||||||
|
// Checkout an empty tree
|
||||||
|
{
|
||||||
|
let tree_oid = test_project
|
||||||
|
.local_repository
|
||||||
|
.treebuilder(None)
|
||||||
|
.unwrap()
|
||||||
|
.write()
|
||||||
|
.unwrap();
|
||||||
|
let tree = test_project.local_repository.find_tree(tree_oid).unwrap();
|
||||||
|
test_project
|
||||||
|
.local_repository
|
||||||
|
.checkout_tree_builder(&tree)
|
||||||
|
.force()
|
||||||
|
.remove_untracked()
|
||||||
|
.checkout()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert tree is indeed empty
|
||||||
|
{
|
||||||
|
let tree: git2::Tree = test_project.local_repository.create_wd_tree().unwrap();
|
||||||
|
dbg!(tree
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| t.name_bytes().to_str_lossy().to_string())
|
||||||
|
.collect::<Vec<_>>());
|
||||||
|
|
||||||
|
// Tree should be empty
|
||||||
|
assert_eq!(
|
||||||
|
tree.len(),
|
||||||
|
0,
|
||||||
|
"Should be empty after checking out an empty tree"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctx = CommandContext::open(&project).unwrap();
|
||||||
|
let mut guard = project.exclusive_worktree_access();
|
||||||
|
|
||||||
|
super::checkout_branch_trees(&ctx, guard.write_permission()).unwrap();
|
||||||
|
|
||||||
|
let tree = test_project.local_repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
// Should be back to original state
|
||||||
|
assert_tree_matches(
|
||||||
|
&test_project.local_repository,
|
||||||
|
&tree,
|
||||||
|
&[("foo.txt", b"content"), ("bar.txt", b"content")],
|
||||||
|
);
|
||||||
|
assert_eq!(tree.len(), 2, "Should match original state");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -253,10 +253,7 @@ pub(crate) fn save_and_return_to_workspace(
|
|||||||
let parents = commit.parents().collect::<Vec<_>>();
|
let parents = commit.parents().collect::<Vec<_>>();
|
||||||
|
|
||||||
// Recommit commit
|
// Recommit commit
|
||||||
let mut index = repository.index()?;
|
let tree = repository.create_wd_tree()?;
|
||||||
index.add_all(["*"], git2::IndexAddOption::DEFAULT, None)?;
|
|
||||||
let tree = index.write_tree()?;
|
|
||||||
let tree = repository.find_tree(tree)?;
|
|
||||||
|
|
||||||
let commit_headers = commit
|
let commit_headers = commit
|
||||||
.gitbutler_headers()
|
.gitbutler_headers()
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::{io::Write, path::Path, process::Stdio, str};
|
use std::{io::Write, path::Path, process::Stdio, str};
|
||||||
|
|
||||||
use crate::{Config, LogUntil};
|
use crate::{Config, LogUntil};
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use bstr::{BString, ByteSlice};
|
use bstr::BString;
|
||||||
use git2::{BlameOptions, Tree};
|
use git2::{BlameOptions, StatusOptions, Tree};
|
||||||
use gitbutler_branch::SignaturePurpose;
|
use gitbutler_branch::SignaturePurpose;
|
||||||
use gitbutler_commit::commit_headers::CommitHeadersV2;
|
use gitbutler_commit::commit_headers::CommitHeadersV2;
|
||||||
use gitbutler_config::git::{GbConfig, GitConfig};
|
use gitbutler_config::git::{GbConfig, GitConfig};
|
||||||
@ -17,13 +18,13 @@ use gitbutler_oxidize::{
|
|||||||
};
|
};
|
||||||
use gitbutler_reference::{Refname, RemoteRefname};
|
use gitbutler_reference::{Refname, RemoteRefname};
|
||||||
use gix::objs::WriteTo;
|
use gix::objs::WriteTo;
|
||||||
use gix::status::plumbing::index_as_worktree::{Change, EntryStatus};
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
/// Extension trait for `git2::Repository`.
|
/// Extension trait for `git2::Repository`.
|
||||||
///
|
///
|
||||||
/// For now, it collects useful methods from `gitbutler-core::git::Repository`
|
/// For now, it collects useful methods from `gitbutler-core::git::Repository`
|
||||||
pub trait RepositoryExt {
|
pub trait RepositoryExt {
|
||||||
|
fn worktree_path(&self) -> PathBuf;
|
||||||
fn signatures(&self) -> Result<(git2::Signature, git2::Signature)>;
|
fn signatures(&self) -> Result<(git2::Signature, git2::Signature)>;
|
||||||
fn l(&self, from: git2::Oid, to: LogUntil) -> Result<Vec<git2::Oid>>;
|
fn l(&self, from: git2::Oid, to: LogUntil) -> Result<Vec<git2::Oid>>;
|
||||||
fn list_commits(&self, from: git2::Oid, to: git2::Oid) -> Result<Vec<git2::Commit>>;
|
fn list_commits(&self, from: git2::Oid, to: git2::Oid) -> Result<Vec<git2::Commit>>;
|
||||||
@ -156,73 +157,67 @@ impl RepositoryExt for git2::Repository {
|
|||||||
/// the object database, and create a tree from it.
|
/// the object database, and create a tree from it.
|
||||||
///
|
///
|
||||||
/// Note that right now, it doesn't skip big files.
|
/// Note that right now, it doesn't skip big files.
|
||||||
|
///
|
||||||
|
/// It should also be noted that this will fail if run on an empty branch
|
||||||
|
/// or if the HEAD branch has no commits
|
||||||
#[instrument(level = tracing::Level::DEBUG, skip(self), err(Debug))]
|
#[instrument(level = tracing::Level::DEBUG, skip(self), err(Debug))]
|
||||||
fn create_wd_tree(&self) -> Result<Tree> {
|
fn create_wd_tree(&self) -> Result<Tree> {
|
||||||
let repo = gix::open(self.path())?;
|
let mut tree_update_builder = git2::build::TreeUpdateBuilder::new();
|
||||||
let workdir = repo.work_dir().context("Need non-bare repository")?;
|
|
||||||
let mut head_tree_editor = repo.edit_tree(repo.head_tree_id()?)?;
|
|
||||||
let status_changes = repo
|
|
||||||
.status(gix::progress::Discard)?
|
|
||||||
.index_worktree_rewrites(None)
|
|
||||||
.index_worktree_submodules(None)
|
|
||||||
.into_index_worktree_iter(None::<BString>)?;
|
|
||||||
for change in status_changes {
|
|
||||||
let change = change?;
|
|
||||||
match change {
|
|
||||||
// modified or tracked files are unconditionally added as blob.
|
|
||||||
gix::status::index_worktree::iter::Item::Modification {
|
|
||||||
rela_path,
|
|
||||||
status: EntryStatus::Change(Change::Type | Change::Modification { .. }),
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| gix::status::index_worktree::iter::Item::DirectoryContents {
|
|
||||||
entry:
|
|
||||||
gix::dir::Entry {
|
|
||||||
rela_path,
|
|
||||||
status: gix::dir::entry::Status::Untracked,
|
|
||||||
..
|
|
||||||
},
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let path = workdir.join(gix::path::from_bstr(&rela_path));
|
|
||||||
let Ok(md) = std::fs::symlink_metadata(&path) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let (id, kind) = if md.is_symlink() {
|
|
||||||
let target = std::fs::read_link(&path).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Failed to read link at '{}' for adding to the object database",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let id = repo.write_blob(gix::path::into_bstr(target).as_bytes())?;
|
|
||||||
(id, gix::object::tree::EntryKind::Link)
|
|
||||||
} else {
|
|
||||||
let mut file = std::fs::File::open(&path).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Could not open file at '{}' for adding it to the object database",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let kind = if gix::fs::is_executable(&md) {
|
|
||||||
gix::object::tree::EntryKind::BlobExecutable
|
|
||||||
} else {
|
|
||||||
gix::object::tree::EntryKind::Blob
|
|
||||||
};
|
|
||||||
(repo.write_blob_stream(&mut file)?, kind)
|
|
||||||
};
|
|
||||||
|
|
||||||
head_tree_editor.upsert(rela_path, kind, id)?;
|
let worktree_path = self.worktree_path();
|
||||||
}
|
|
||||||
gix::status::index_worktree::iter::Item::Rewrite { .. } => {
|
let statuses = self.statuses(Some(
|
||||||
unreachable!("disabled")
|
StatusOptions::new()
|
||||||
}
|
.renames_from_rewrites(false)
|
||||||
_ => {}
|
.renames_head_to_index(false)
|
||||||
|
.renames_index_to_workdir(false)
|
||||||
|
.include_untracked(true)
|
||||||
|
.recurse_untracked_dirs(true),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
// Truth table for upsert/remove:
|
||||||
|
// | HEAD Tree -> Index | Index -> Worktree | Action |
|
||||||
|
// | add | delete | no-action |
|
||||||
|
// | modify | delete | remove |
|
||||||
|
// | | delete | remove |
|
||||||
|
// | delete | | remove |
|
||||||
|
// | delete | add | upsert |
|
||||||
|
// | add | | upsert |
|
||||||
|
// | | add | upsert |
|
||||||
|
// | add | modify | upsert |
|
||||||
|
// | modify | modify | upsert |
|
||||||
|
|
||||||
|
for status_entry in &statuses {
|
||||||
|
let status = status_entry.status();
|
||||||
|
let path = status_entry.path().context("Failed to get path")?;
|
||||||
|
let path = Path::new(path);
|
||||||
|
|
||||||
|
if status.is_index_new() && status.is_wt_deleted() {
|
||||||
|
// This is a no-op
|
||||||
|
} 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)?;
|
||||||
|
|
||||||
|
tree_update_builder.upsert(path, blob, git2::FileMode::Blob);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let oid = git2::Oid::from_bytes(head_tree_editor.write()?.as_bytes())?;
|
let head_tree = self.head_commit()?.tree()?;
|
||||||
self.find_tree(oid).map(Into::into).map_err(Into::into)
|
let tree_oid = tree_update_builder.create_updated(self, &head_tree)?;
|
||||||
|
|
||||||
|
Ok(self.find_tree(tree_oid)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path of the working directory of the repository.
|
||||||
|
fn worktree_path(&self) -> PathBuf {
|
||||||
|
if self.is_bare() {
|
||||||
|
self.path().to_owned()
|
||||||
|
} else {
|
||||||
|
self.path().join("..")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_ref_from_head(&self) -> Result<git2::Reference<'_>> {
|
fn workspace_ref_from_head(&self) -> Result<git2::Reference<'_>> {
|
||||||
@ -634,3 +629,429 @@ impl GixRepositoryExt for gix::Repository {
|
|||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
mod create_wd_tree {
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::repository_ext::RepositoryExt as _;
|
||||||
|
use gitbutler_testsupport::testing_repository::{assert_tree_matches, TestingRepository};
|
||||||
|
|
||||||
|
/// These tests exercise the truth table that we use to update the HEAD
|
||||||
|
/// tree to match the worktree.
|
||||||
|
///
|
||||||
|
/// Truth table for upsert/remove:
|
||||||
|
/// | HEAD Tree -> Index | Index -> Worktree | Action |
|
||||||
|
/// | add | delete | no-action |
|
||||||
|
/// | modify | delete | remove |
|
||||||
|
/// | | delete | remove |
|
||||||
|
/// | delete | | remove |
|
||||||
|
/// | delete | add | upsert |
|
||||||
|
/// | add | | upsert |
|
||||||
|
/// | | add | upsert |
|
||||||
|
/// | add | modify | upsert |
|
||||||
|
/// | modify | modify | upsert |
|
||||||
|
#[cfg(test)]
|
||||||
|
mod head_upsert_truthtable {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// | add | delete | no-action |
|
||||||
|
#[test]
|
||||||
|
fn index_new_worktree_delete() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(None, &[]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut index = test_repository.repository.index().unwrap();
|
||||||
|
index.add_path(Path::new("file1.txt")).unwrap();
|
||||||
|
index.write().unwrap();
|
||||||
|
|
||||||
|
std::fs::remove_file(test_repository.tempdir.path().join("file1.txt")).unwrap();
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(tree.len(), 0, "Tree should end up empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// | modify | delete | remove |
|
||||||
|
#[test]
|
||||||
|
fn index_modify_worktree_delete() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut index = test_repository.repository.index().unwrap();
|
||||||
|
index.add_path(Path::new("file1.txt")).unwrap();
|
||||||
|
index.write().unwrap();
|
||||||
|
|
||||||
|
std::fs::remove_file(test_repository.tempdir.path().join("file1.txt")).unwrap();
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(tree.len(), 0, "Tree should end up empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// | | delete | remove |
|
||||||
|
#[test]
|
||||||
|
fn worktree_delete() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::remove_file(test_repository.tempdir.path().join("file1.txt")).unwrap();
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(tree.len(), 0, "Tree should end up empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// | delete | | remove |
|
||||||
|
#[test]
|
||||||
|
fn index_delete() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut index = test_repository.repository.index().unwrap();
|
||||||
|
index.remove_all(["*"], None).unwrap();
|
||||||
|
index.write().unwrap();
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
// We should ignore whatever happens to the index
|
||||||
|
assert_tree_matches(
|
||||||
|
&test_repository.repository,
|
||||||
|
&tree,
|
||||||
|
&[("file1.txt", b"content1")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// | delete | add | upsert |
|
||||||
|
#[test]
|
||||||
|
fn index_delete_worktree_add() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut index = test_repository.repository.index().unwrap();
|
||||||
|
index.remove_all(["*"], None).unwrap();
|
||||||
|
index.write().unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
// Tree should match whatever is written on disk
|
||||||
|
assert_tree_matches(
|
||||||
|
&test_repository.repository,
|
||||||
|
&tree,
|
||||||
|
&[("file1.txt", b"content2")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// | add | | upsert |
|
||||||
|
#[test]
|
||||||
|
fn index_add() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(None, &[]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut index = test_repository.repository.index().unwrap();
|
||||||
|
index.add_path(Path::new("file1.txt")).unwrap();
|
||||||
|
index.write().unwrap();
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
// Tree should match whatever is written on disk
|
||||||
|
assert_tree_matches(
|
||||||
|
&test_repository.repository,
|
||||||
|
&tree,
|
||||||
|
&[("file1.txt", b"content2")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// | | add | upsert |
|
||||||
|
#[test]
|
||||||
|
fn worktree_add() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(None, &[]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
// Tree should match whatever is written on disk
|
||||||
|
assert_tree_matches(
|
||||||
|
&test_repository.repository,
|
||||||
|
&tree,
|
||||||
|
&[("file1.txt", b"content2")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// | add | modify | upsert |
|
||||||
|
#[test]
|
||||||
|
fn index_add_worktree_modify() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(None, &[]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut index = test_repository.repository.index().unwrap();
|
||||||
|
index.add_path(Path::new("file1.txt")).unwrap();
|
||||||
|
index.write().unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
// Tree should match whatever is written on disk
|
||||||
|
assert_tree_matches(
|
||||||
|
&test_repository.repository,
|
||||||
|
&tree,
|
||||||
|
&[("file1.txt", b"content2")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// | modify | modify | upsert |
|
||||||
|
#[test]
|
||||||
|
fn index_modify_worktree_modify() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut index = test_repository.repository.index().unwrap();
|
||||||
|
index.add_path(Path::new("file1.txt")).unwrap();
|
||||||
|
index.write().unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content3")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
// Tree should match whatever is written on disk
|
||||||
|
assert_tree_matches(
|
||||||
|
&test_repository.repository,
|
||||||
|
&tree,
|
||||||
|
&[("file1.txt", b"content3")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lists_uncommited_changes() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
// Initial commit
|
||||||
|
// Create wd tree requires the HEAD branch to exist and for there
|
||||||
|
// to be at least one commit on that branch.
|
||||||
|
let commit = test_repository.commit_tree(None, &[]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1").unwrap();
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file2.txt"), "content2").unwrap();
|
||||||
|
|
||||||
|
let tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
assert_tree_matches(
|
||||||
|
&test_repository.repository,
|
||||||
|
&tree,
|
||||||
|
&[("file1.txt", b"content1"), ("file2.txt", b"content2")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_include_staged_but_deleted_files() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
// Initial commit
|
||||||
|
// Create wd tree requires the HEAD branch to exist and for there
|
||||||
|
// to be at least one commit on that branch.
|
||||||
|
let commit = test_repository.commit_tree(None, &[]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1").unwrap();
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file2.txt"), "content2").unwrap();
|
||||||
|
|
||||||
|
std::fs::write(test_repository.tempdir.path().join("file3.txt"), "content2").unwrap();
|
||||||
|
let mut index: git2::Index = test_repository.repository.index().unwrap();
|
||||||
|
index.add_path(Path::new("file3.txt")).unwrap();
|
||||||
|
index.write().unwrap();
|
||||||
|
std::fs::remove_file(test_repository.tempdir.path().join("file3.txt")).unwrap();
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
assert_tree_matches(
|
||||||
|
&test_repository.repository,
|
||||||
|
&tree,
|
||||||
|
&[("file1.txt", b"content1"), ("file2.txt", b"content2")],
|
||||||
|
);
|
||||||
|
assert!(tree.get_name("file3.txt").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_be_empty_after_checking_out_empty_tree() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(
|
||||||
|
None,
|
||||||
|
&[("file1.txt", "content1"), ("file2.txt", "content2")],
|
||||||
|
);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Checkout an empty tree
|
||||||
|
{
|
||||||
|
let tree_oid = test_repository
|
||||||
|
.repository
|
||||||
|
.treebuilder(None)
|
||||||
|
.unwrap()
|
||||||
|
.write()
|
||||||
|
.unwrap();
|
||||||
|
let tree = test_repository.repository.find_tree(tree_oid).unwrap();
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.checkout_tree_builder(&tree)
|
||||||
|
.force()
|
||||||
|
.remove_untracked()
|
||||||
|
.checkout()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(!test_repository.tempdir.path().join("file1.txt").exists());
|
||||||
|
assert!(!test_repository.tempdir.path().join("file2.txt").exists());
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
// Fails because `create_wd_tree` uses the head commit as the base,
|
||||||
|
// and then performs modifications to the tree
|
||||||
|
assert_eq!(tree.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_track_deleted_files() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(
|
||||||
|
None,
|
||||||
|
&[("file1.txt", "content1"), ("file2.txt", "content2")],
|
||||||
|
);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Make sure the index is empty, perhaps the user did this action
|
||||||
|
let mut index: git2::Index = test_repository.repository.index().unwrap();
|
||||||
|
index.remove_all(["*"], None).unwrap();
|
||||||
|
index.write().unwrap();
|
||||||
|
|
||||||
|
std::fs::remove_file(test_repository.tempdir.path().join("file1.txt")).unwrap();
|
||||||
|
|
||||||
|
assert!(!test_repository.tempdir.path().join("file1.txt").exists());
|
||||||
|
assert!(test_repository.tempdir.path().join("file2.txt").exists());
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
// Fails because `create_wd_tree` uses the head commit as the base,
|
||||||
|
// and then performs modifications to the tree
|
||||||
|
assert!(tree.get_name("file1.txt").is_none());
|
||||||
|
assert!(tree.get_name("file2.txt").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_not_change_index() {
|
||||||
|
let test_repository = TestingRepository::open();
|
||||||
|
|
||||||
|
let commit = test_repository.commit_tree(None, &[("file1.txt", "content1")]);
|
||||||
|
test_repository
|
||||||
|
.repository
|
||||||
|
.branch("master", &commit, true)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut index = test_repository.repository.index().unwrap();
|
||||||
|
index.remove_all(["*"], None).unwrap();
|
||||||
|
index.write().unwrap();
|
||||||
|
|
||||||
|
let index_tree = index.write_tree().unwrap();
|
||||||
|
let index_tree = test_repository.repository.find_tree(index_tree).unwrap();
|
||||||
|
assert_eq!(index_tree.len(), 0);
|
||||||
|
|
||||||
|
let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap();
|
||||||
|
|
||||||
|
let mut index = test_repository.repository.index().unwrap();
|
||||||
|
let index_tree = index.write_tree().unwrap();
|
||||||
|
let index_tree = test_repository.repository.find_tree(index_tree).unwrap();
|
||||||
|
assert_eq!(index_tree.len(), 0);
|
||||||
|
|
||||||
|
// Tree should match whatever is written on disk
|
||||||
|
assert_tree_matches(
|
||||||
|
&test_repository.repository,
|
||||||
|
&tree,
|
||||||
|
&[("file1.txt", b"content1")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user