Merge pull request #4349 from Byron/refactor

refactor and PoC for worktree mutability handling
This commit is contained in:
Kiril Videlov 2024-07-15 23:02:01 +02:00 committed by GitHub
commit 8ccd8d511e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
95 changed files with 2160 additions and 1924 deletions

7
Cargo.lock generated
View File

@ -2005,6 +2005,7 @@ dependencies = [
"gitbutler-command-context",
"gitbutler-commit",
"gitbutler-error",
"gitbutler-fs",
"gitbutler-git",
"gitbutler-id",
"gitbutler-oplog",
@ -2175,6 +2176,7 @@ name = "gitbutler-project"
version = "0.0.0"
dependencies = [
"anyhow",
"fslock",
"git2",
"gitbutler-error",
"gitbutler-id",
@ -2182,6 +2184,7 @@ dependencies = [
"gitbutler-storage",
"gitbutler-testsupport",
"gix",
"parking_lot 0.12.3",
"resolve-path",
"serde",
"serde_json",
@ -7195,9 +7198,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.9.1"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [
"getrandom 0.2.15",
"rand 0.8.5",

View File

@ -44,6 +44,8 @@ thiserror = "1.0.61"
tokio = { version = "1.38.0", default-features = false }
keyring = "2.3.3"
anyhow = "1.0.86"
fslock = "0.2.1"
parking_lot = "0.12.3"
gitbutler-id = { path = "crates/gitbutler-id" }
gitbutler-git = { path = "crates/gitbutler-git" }

View File

@ -21,6 +21,7 @@ gitbutler-id.workspace = true
gitbutler-time.workspace = true
gitbutler-commit.workspace = true
gitbutler-url.workspace = true
gitbutler-fs.workspace = true
serde = { workspace = true, features = ["std"] }
bstr = "1.9.1"
diffy = "0.3.0"

View File

@ -1,37 +1,33 @@
use anyhow::Result;
use gitbutler_branch::{
branch::{BranchCreateRequest, BranchId, BranchUpdateRequest},
diff,
ownership::BranchOwnershipClaims,
};
use gitbutler_command_context::ProjectRepository;
use gitbutler_oplog::{
entry::{OperationKind, SnapshotDetails},
oplog::Oplog,
snapshot::Snapshot,
};
use gitbutler_project::{FetchResult, Project};
use gitbutler_reference::ReferenceName;
use gitbutler_reference::{Refname, RemoteRefname};
use gitbutler_repo::{credentials::Helper, RepoActions, RepositoryExt};
use crate::branch_manager::branch_removal::BranchRemoval;
use crate::{
base::{
get_base_branch_data, set_base_branch, set_target_push_remote, update_base_branch,
BaseBranch,
},
branch_manager::{branch_creation::BranchCreation, BranchManagerAccess},
branch_manager::BranchManagerExt,
remote::{get_branch_data, list_remote_branches, RemoteBranch, RemoteBranchData},
VirtualBranchesExt,
};
use anyhow::Result;
use gitbutler_branch::{
diff, BranchOwnershipClaims, {BranchCreateRequest, BranchId, BranchUpdateRequest},
};
use gitbutler_command_context::ProjectRepository;
use gitbutler_oplog::{
entry::{OperationKind, SnapshotDetails},
OplogExt, SnapshotExt,
};
use gitbutler_project::{FetchResult, Project};
use gitbutler_reference::ReferenceName;
use gitbutler_reference::{Refname, RemoteRefname};
use gitbutler_repo::{credentials::Helper, RepoActionsExt, RepositoryExt};
use tracing::instrument;
use super::r#virtual as branch;
use crate::files::RemoteBranchFile;
#[derive(Clone, Default)]
pub struct VirtualBranchActions {}
#[derive(Clone, Copy, Default)]
pub struct VirtualBranchActions;
impl VirtualBranchActions {
pub async fn create_commit(
@ -43,13 +39,17 @@ impl VirtualBranchActions {
run_hooks: bool,
) -> Result<git2::Oid> {
let project_repository = open_with_verify(project)?;
let snapshot_tree = project_repository.project().prepare_snapshot();
let mut guard = project.exclusive_worktree_access();
let snapshot_tree = project_repository
.project()
.prepare_snapshot(guard.read_permission());
let result = branch::commit(
&project_repository,
branch_id,
message,
ownership,
run_hooks,
guard.write_permission(),
)
.map_err(Into::into);
let _ = snapshot_tree.and_then(|snapshot_tree| {
@ -58,6 +58,7 @@ impl VirtualBranchActions {
result.as_ref().err(),
message.to_owned(),
None,
guard.write_permission(),
)
});
result
@ -76,8 +77,11 @@ impl VirtualBranchActions {
&self,
project: &Project,
) -> Result<(Vec<branch::VirtualBranch>, Vec<diff::FileDiff>)> {
let project_repository = open_with_verify(project)?;
branch::list_virtual_branches(&project_repository).map_err(Into::into)
branch::list_virtual_branches(
&open_with_verify(project)?,
project.exclusive_worktree_access().write_permission(),
)
.map_err(Into::into)
}
pub async fn create_virtual_branch(
@ -86,12 +90,16 @@ impl VirtualBranchActions {
create: &BranchCreateRequest,
) -> Result<BranchId> {
let project_repository = open_with_verify(project)?;
let mut guard = project.exclusive_worktree_access();
let branch_manager = project_repository.branch_manager();
let branch_id = branch_manager.create_virtual_branch(create)?.id;
let branch_id = branch_manager
.create_virtual_branch(create, guard.write_permission())?
.id;
Ok(branch_id)
}
pub async fn get_base_branch_data(&self, project: &Project) -> Result<BaseBranch> {
#[instrument(skip(project), err(Debug))]
pub async fn get_base_branch_data(project: &Project) -> Result<BaseBranch> {
let project_repository = ProjectRepository::open(project)?;
get_base_branch_data(&project_repository)
}
@ -112,9 +120,11 @@ impl VirtualBranchActions {
target_branch: &RemoteRefname,
) -> Result<BaseBranch> {
let project_repository = ProjectRepository::open(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::SetBaseBranch));
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::SetBaseBranch),
guard.write_permission(),
);
set_base_branch(&project_repository, target_branch)
}
@ -129,18 +139,22 @@ impl VirtualBranchActions {
branch_id: BranchId,
) -> Result<()> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::MergeUpstream));
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::MergeUpstream),
guard.write_permission(),
);
branch::integrate_upstream_commits(&project_repository, branch_id).map_err(Into::into)
}
pub async fn update_base_branch(&self, project: &Project) -> Result<Vec<ReferenceName>> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::UpdateWorkspaceBase));
update_base_branch(&project_repository).map_err(Into::into)
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::UpdateWorkspaceBase),
guard.write_permission(),
);
update_base_branch(&project_repository, guard.write_permission()).map_err(Into::into)
}
pub async fn update_virtual_branch(
@ -149,7 +163,10 @@ impl VirtualBranchActions {
branch_update: BranchUpdateRequest,
) -> Result<()> {
let project_repository = open_with_verify(project)?;
let snapshot_tree = project_repository.project().prepare_snapshot();
let mut guard = project.exclusive_worktree_access();
let snapshot_tree = project_repository
.project()
.prepare_snapshot(guard.read_permission());
let old_branch = project_repository
.project()
.virtual_branches()
@ -161,6 +178,7 @@ impl VirtualBranchActions {
&old_branch,
&branch_update,
result.as_ref().err(),
guard.write_permission(),
)
});
result?;
@ -173,7 +191,8 @@ impl VirtualBranchActions {
) -> Result<()> {
let project_repository = open_with_verify(project)?;
let branch_manager = project_repository.branch_manager();
branch_manager.delete_branch(branch_id)
let mut guard = project.exclusive_worktree_access();
branch_manager.delete_branch(branch_id, guard.write_permission())
}
pub async fn unapply_ownership(
@ -182,17 +201,22 @@ impl VirtualBranchActions {
ownership: &BranchOwnershipClaims,
) -> Result<()> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::DiscardHunk));
branch::unapply_ownership(&project_repository, ownership).map_err(Into::into)
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::DiscardHunk),
guard.write_permission(),
);
branch::unapply_ownership(&project_repository, ownership, guard.write_permission())
.map_err(Into::into)
}
pub async fn reset_files(&self, project: &Project, files: &Vec<String>) -> Result<()> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::DiscardFile));
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::DiscardFile),
guard.write_permission(),
);
branch::reset_files(&project_repository, files).map_err(Into::into)
}
@ -204,10 +228,18 @@ impl VirtualBranchActions {
ownership: &BranchOwnershipClaims,
) -> Result<git2::Oid> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::AmendCommit));
branch::amend(&project_repository, branch_id, commit_oid, ownership)
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::AmendCommit),
guard.write_permission(),
);
branch::amend(
&project_repository,
branch_id,
commit_oid,
ownership,
guard.write_permission(),
)
}
pub async fn move_commit_file(
@ -219,9 +251,11 @@ impl VirtualBranchActions {
ownership: &BranchOwnershipClaims,
) -> Result<git2::Oid> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::MoveCommitFile));
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::MoveCommitFile),
guard.write_permission(),
);
branch::move_commit_file(
&project_repository,
branch_id,
@ -239,7 +273,10 @@ impl VirtualBranchActions {
commit_oid: git2::Oid,
) -> Result<()> {
let project_repository = open_with_verify(project)?;
let snapshot_tree = project_repository.project().prepare_snapshot();
let mut guard = project.exclusive_worktree_access();
let snapshot_tree = project_repository
.project()
.prepare_snapshot(guard.read_permission());
let result: Result<()> =
branch::undo_commit(&project_repository, branch_id, commit_oid).map_err(Into::into);
let _ = snapshot_tree.and_then(|snapshot_tree| {
@ -247,6 +284,7 @@ impl VirtualBranchActions {
snapshot_tree,
result.as_ref(),
commit_oid,
guard.write_permission(),
)
});
result
@ -260,9 +298,11 @@ impl VirtualBranchActions {
offset: i32,
) -> Result<()> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::InsertBlankCommit));
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::InsertBlankCommit),
guard.write_permission(),
);
branch::insert_blank_commit(&project_repository, branch_id, commit_oid, offset)
.map_err(Into::into)
}
@ -275,9 +315,11 @@ impl VirtualBranchActions {
offset: i32,
) -> Result<()> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::ReorderCommit));
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::ReorderCommit),
guard.write_permission(),
);
branch::reorder_commit(&project_repository, branch_id, commit_oid, offset)
.map_err(Into::into)
}
@ -289,9 +331,11 @@ impl VirtualBranchActions {
target_commit_oid: git2::Oid,
) -> Result<()> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::UndoCommit));
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::UndoCommit),
guard.write_permission(),
);
branch::reset_branch(&project_repository, branch_id, target_commit_oid).map_err(Into::into)
}
@ -299,17 +343,26 @@ impl VirtualBranchActions {
&self,
project: &Project,
branch_id: BranchId,
name_conflict_resolution: branch::NameConflitResolution,
name_conflict_resolution: branch::NameConflictResolution,
) -> Result<ReferenceName> {
let project_repository = open_with_verify(project)?;
let snapshot_tree = project_repository.project().prepare_snapshot();
let mut guard = project.exclusive_worktree_access();
let snapshot_tree = project_repository
.project()
.prepare_snapshot(guard.read_permission());
let branch_manager = project_repository.branch_manager();
let result = branch_manager.convert_to_real_branch(branch_id, name_conflict_resolution);
let result = branch_manager.convert_to_real_branch(
branch_id,
name_conflict_resolution,
guard.write_permission(),
);
let _ = snapshot_tree.and_then(|snapshot_tree| {
project_repository
.project()
.snapshot_branch_unapplied(snapshot_tree, result.as_ref())
project_repository.project().snapshot_branch_unapplied(
snapshot_tree,
result.as_ref(),
guard.write_permission(),
)
});
result
@ -327,7 +380,7 @@ impl VirtualBranchActions {
branch::push(&project_repository, branch_id, with_force, &helper, askpass)
}
pub async fn list_remote_branches(&self, project: Project) -> Result<Vec<RemoteBranch>> {
pub async fn list_remote_branches(project: Project) -> Result<Vec<RemoteBranch>> {
let project_repository = ProjectRepository::open(&project)?;
list_remote_branches(&project_repository)
}
@ -348,9 +401,11 @@ impl VirtualBranchActions {
commit_oid: git2::Oid,
) -> Result<()> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::SquashCommit));
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::SquashCommit),
guard.write_permission(),
);
branch::squash(&project_repository, branch_id, commit_oid).map_err(Into::into)
}
@ -362,9 +417,11 @@ impl VirtualBranchActions {
message: &str,
) -> Result<()> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::UpdateCommitMessage));
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::UpdateCommitMessage),
guard.write_permission(),
);
branch::update_commit_message(&project_repository, branch_id, commit_oid, message)
.map_err(Into::into)
}
@ -411,10 +468,18 @@ impl VirtualBranchActions {
commit_oid: git2::Oid,
) -> Result<()> {
let project_repository = open_with_verify(project)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::MoveCommit));
branch::move_commit(&project_repository, target_branch_id, commit_oid).map_err(Into::into)
let mut guard = project.exclusive_worktree_access();
let _ = project_repository.project().create_snapshot(
SnapshotDetails::new(OperationKind::MoveCommit),
guard.write_permission(),
);
branch::move_commit(
&project_repository,
target_branch_id,
commit_oid,
guard.write_permission(),
)
.map_err(Into::into)
}
pub async fn create_virtual_branch_from_branch(
@ -424,14 +489,16 @@ impl VirtualBranchActions {
) -> Result<BranchId> {
let project_repository = open_with_verify(project)?;
let branch_manager = project_repository.branch_manager();
let mut guard = project.exclusive_worktree_access();
branch_manager
.create_virtual_branch_from_branch(branch)
.create_virtual_branch_from_branch(branch, guard.write_permission())
.map_err(Into::into)
}
}
fn open_with_verify(project: &Project) -> Result<ProjectRepository> {
let project_repository = ProjectRepository::open(project)?;
crate::integration::verify_branch(&project_repository)?;
let mut guard = project.exclusive_worktree_access();
crate::integration::verify_branch(&project_repository, guard.write_permission())?;
Ok(project_repository)
}

View File

@ -2,27 +2,27 @@ use std::{path::Path, time};
use anyhow::{anyhow, Context, Result};
use git2::Index;
use gitbutler_branch::branch::{self, BranchId};
use gitbutler_branch::diff;
use gitbutler_branch::ownership::BranchOwnershipClaims;
use gitbutler_branch::target::Target;
use gitbutler_branch::BranchOwnershipClaims;
use gitbutler_branch::Target;
use gitbutler_branch::VirtualBranchesHandle;
use gitbutler_branch::{self, BranchId};
use gitbutler_branch::{diff, Branch};
use gitbutler_command_context::ProjectRepository;
use gitbutler_project::FetchResult;
use gitbutler_reference::ReferenceName;
use gitbutler_reference::{Refname, RemoteRefname};
use gitbutler_repo::{LogUntil, RepoActions, RepositoryExt};
use gitbutler_repo::{LogUntil, RepoActionsExt, RepositoryExt};
use serde::Serialize;
use super::r#virtual as vb;
use crate::branch_manager::branch_removal::BranchRemoval;
use crate::branch_manager::BranchManagerAccess;
use crate::conflicts::RepoConflicts;
use crate::branch_manager::BranchManagerExt;
use crate::conflicts::RepoConflictsExt;
use crate::integration::{get_workspace_head, update_gitbutler_integration};
use crate::remote::{commit_to_remote_commit, RemoteCommit};
use crate::{VirtualBranchHunk, VirtualBranchesExt};
use gitbutler_branch::GITBUTLER_INTEGRATION_REFERENCE;
use gitbutler_error::error::Marker;
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_repo::rebase::cherry_rebase;
#[derive(Debug, Serialize, PartialEq, Clone)]
@ -43,7 +43,7 @@ pub struct BaseBranch {
pub last_fetched_ms: Option<u128>,
}
pub fn get_base_branch_data(project_repository: &ProjectRepository) -> Result<BaseBranch> {
pub(crate) fn get_base_branch_data(project_repository: &ProjectRepository) -> Result<BaseBranch> {
let target = default_target(&project_repository.project().gb_dir())?;
let base = target_to_base_branch(project_repository, &target)?;
Ok(base)
@ -115,7 +115,7 @@ fn go_back_to_integration(
Ok(base)
}
pub fn set_base_branch(
pub(crate) fn set_base_branch(
project_repository: &ProjectRepository,
target_branch_ref: &RemoteRefname,
) -> Result<BaseBranch> {
@ -235,7 +235,7 @@ pub fn set_base_branch(
(None, None)
};
let branch = branch::Branch {
let branch = Branch {
id: BranchId::generate(),
name: head_name.to_string().replace("refs/heads/", ""),
notes: String::new(),
@ -271,7 +271,7 @@ pub fn set_base_branch(
Ok(base)
}
pub fn set_target_push_remote(
pub(crate) fn set_target_push_remote(
project_repository: &ProjectRepository,
push_remote_name: &str,
) -> Result<()> {
@ -328,8 +328,9 @@ fn _print_tree(repo: &git2::Repository, tree: &git2::Tree) -> Result<()> {
// determine if what the target branch is now pointing to is mergeable with our current working directory
// merge the target branch into our current working directory
// update the target sha
pub fn update_base_branch(
pub(crate) fn update_base_branch(
project_repository: &ProjectRepository,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<Vec<ReferenceName>> {
project_repository.assure_resolved()?;
@ -366,172 +367,171 @@ pub fn update_base_branch(
// try to update every branch
let updated_vbranches =
vb::get_status_by_branch(project_repository, Some(&integration_commit))?
vb::get_status_by_branch(project_repository, Some(&integration_commit), perm)?
.0
.into_iter()
.map(|(branch, _)| branch)
.map(
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
let branch_tree = repo.find_tree(branch.tree)?;
.map(|mut branch: Branch| -> Result<Option<Branch>> {
let branch_tree = repo.find_tree(branch.tree)?;
let branch_head_commit = repo.find_commit(branch.head).context(format!(
"failed to find commit {} for branch {}",
branch.head, branch.id
))?;
let branch_head_tree = branch_head_commit.tree().context(format!(
"failed to find tree for commit {} for branch {}",
branch.head, branch.id
let branch_head_commit = repo.find_commit(branch.head).context(format!(
"failed to find commit {} for branch {}",
branch.head, branch.id
))?;
let branch_head_tree = branch_head_commit.tree().context(format!(
"failed to find tree for commit {} for branch {}",
branch.head, branch.id
))?;
let result_integrated_detected = |mut branch: Branch| -> Result<Option<Branch>> {
// branch head tree is the same as the new target tree.
// meaning we can safely use the new target commit as the branch head.
branch.head = new_target_commit.id();
// it also means that the branch is fully integrated into the target.
// disconnect it from the upstream
branch.upstream = None;
branch.upstream_head = None;
let non_commited_files =
diff::trees(project_repository.repo(), &branch_head_tree, &branch_tree)?;
if non_commited_files.is_empty() {
// if there are no commited files, then the branch is fully merged
// and we can delete it.
vb_state.mark_as_not_in_workspace(branch.id)?;
project_repository.delete_branch_reference(&branch)?;
Ok(None)
} else {
vb_state.set_branch(branch.clone())?;
Ok(Some(branch))
}
};
if branch_head_tree.id() == new_target_tree.id() {
return result_integrated_detected(branch);
}
// try to merge branch head with new target
let mut branch_tree_merge_index = repo
.merge_trees(&old_target_tree, &branch_tree, &new_target_tree, None)
.context(format!("failed to merge trees for branch {}", branch.id))?;
if branch_tree_merge_index.has_conflicts() {
// branch tree conflicts with new target, unapply branch for now. we'll handle it later, when user applies it back.
let branch_manager = project_repository.branch_manager();
let unapplied_real_branch = branch_manager.convert_to_real_branch(
branch.id,
Default::default(),
perm,
)?;
unapplied_branch_names.push(unapplied_real_branch);
return Ok(None);
}
let branch_merge_index_tree_oid =
branch_tree_merge_index.write_tree_to(project_repository.repo())?;
if branch_merge_index_tree_oid == new_target_tree.id() {
return result_integrated_detected(branch);
}
if branch.head == target.sha {
// there are no commits on the branch, so we can just update the head to the new target and calculate the new tree
branch.head = new_target_commit.id();
branch.tree = branch_merge_index_tree_oid;
vb_state.set_branch(branch.clone())?;
return Ok(Some(branch));
}
let mut branch_head_merge_index = repo
.merge_trees(&old_target_tree, &branch_head_tree, &new_target_tree, None)
.context(format!(
"failed to merge head tree for branch {}",
branch.id
))?;
let result_integrated_detected =
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
// branch head tree is the same as the new target tree.
// meaning we can safely use the new target commit as the branch head.
if branch_head_merge_index.has_conflicts() {
// branch commits conflict with new target, make sure the branch is
// unapplied. conflicts witll be dealt with when applying it back.
let branch_manager = project_repository.branch_manager();
let unapplied_real_branch = branch_manager.convert_to_real_branch(
branch.id,
Default::default(),
perm,
)?;
unapplied_branch_names.push(unapplied_real_branch);
branch.head = new_target_commit.id();
return Ok(None);
}
// it also means that the branch is fully integrated into the target.
// disconnect it from the upstream
branch.upstream = None;
branch.upstream_head = None;
// branch commits do not conflict with new target, so lets merge them
let branch_head_merge_tree_oid = branch_head_merge_index
.write_tree_to(project_repository.repo())
.context(format!(
"failed to write head merge index for {}",
branch.id
))?;
let non_commited_files = diff::trees(
project_repository.repo(),
&branch_head_tree,
&branch_tree,
)?;
if non_commited_files.is_empty() {
// if there are no commited files, then the branch is fully merged
// and we can delete it.
vb_state.mark_as_not_in_workspace(branch.id)?;
project_repository.delete_branch_reference(&branch)?;
Ok(None)
} else {
vb_state.set_branch(branch.clone())?;
Ok(Some(branch))
}
};
let ok_with_force_push = branch.allow_rebasing;
if branch_head_tree.id() == new_target_tree.id() {
return result_integrated_detected(branch);
}
let result_merge = |mut branch: Branch| -> Result<Option<Branch>> {
// branch was pushed to upstream, and user doesn't like force pushing.
// create a merge commit to avoid the need of force pushing then.
let branch_head_merge_tree = repo
.find_tree(branch_head_merge_tree_oid)
.context("failed to find tree")?;
// try to merge branch head with new target
let mut branch_tree_merge_index = repo
.merge_trees(&old_target_tree, &branch_tree, &new_target_tree, None)
.context(format!("failed to merge trees for branch {}", branch.id))?;
let new_target_head = project_repository
.commit(
format!(
"Merged {}/{} into {}",
target.branch.remote(),
target.branch.branch(),
branch.name,
)
.as_str(),
&branch_head_merge_tree,
&[&branch_head_commit, &new_target_commit],
None,
)
.context("failed to commit merge")?;
if branch_tree_merge_index.has_conflicts() {
// branch tree conflicts with new target, unapply branch for now. we'll handle it later, when user applies it back.
let branch_manager = project_repository.branch_manager();
let unapplied_real_branch =
branch_manager.convert_to_real_branch(branch.id, Default::default())?;
branch.head = new_target_head;
branch.tree = branch_merge_index_tree_oid;
vb_state.set_branch(branch.clone())?;
Ok(Some(branch))
};
unapplied_branch_names.push(unapplied_real_branch);
if branch.upstream.is_some() && !ok_with_force_push {
return result_merge(branch);
}
return Ok(None);
}
// branch was not pushed to upstream yet. attempt a rebase,
let rebased_head_oid = cherry_rebase(
project_repository,
new_target_commit.id(),
new_target_commit.id(),
branch.head,
);
let branch_merge_index_tree_oid =
branch_tree_merge_index.write_tree_to(project_repository.repo())?;
// rebase failed, just do the merge
if rebased_head_oid.is_err() {
return result_merge(branch);
}
if branch_merge_index_tree_oid == new_target_tree.id() {
return result_integrated_detected(branch);
}
if let Some(rebased_head_oid) = rebased_head_oid? {
// rebase worked out, rewrite the branch head
branch.head = rebased_head_oid;
branch.tree = branch_merge_index_tree_oid;
vb_state.set_branch(branch.clone())?;
return Ok(Some(branch));
}
if branch.head == target.sha {
// there are no commits on the branch, so we can just update the head to the new target and calculate the new tree
branch.head = new_target_commit.id();
branch.tree = branch_merge_index_tree_oid;
vb_state.set_branch(branch.clone())?;
return Ok(Some(branch));
}
let mut branch_head_merge_index = repo
.merge_trees(&old_target_tree, &branch_head_tree, &new_target_tree, None)
.context(format!(
"failed to merge head tree for branch {}",
branch.id
))?;
if branch_head_merge_index.has_conflicts() {
// branch commits conflict with new target, make sure the branch is
// unapplied. conflicts witll be dealt with when applying it back.
let branch_manager = project_repository.branch_manager();
let unapplied_real_branch =
branch_manager.convert_to_real_branch(branch.id, Default::default())?;
unapplied_branch_names.push(unapplied_real_branch);
return Ok(None);
}
// branch commits do not conflict with new target, so lets merge them
let branch_head_merge_tree_oid = branch_head_merge_index
.write_tree_to(project_repository.repo())
.context(format!(
"failed to write head merge index for {}",
branch.id
))?;
let ok_with_force_push = branch.allow_rebasing;
let result_merge =
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
// branch was pushed to upstream, and user doesn't like force pushing.
// create a merge commit to avoid the need of force pushing then.
let branch_head_merge_tree = repo
.find_tree(branch_head_merge_tree_oid)
.context("failed to find tree")?;
let new_target_head = project_repository
.commit(
format!(
"Merged {}/{} into {}",
target.branch.remote(),
target.branch.branch(),
branch.name,
)
.as_str(),
&branch_head_merge_tree,
&[&branch_head_commit, &new_target_commit],
None,
)
.context("failed to commit merge")?;
branch.head = new_target_head;
branch.tree = branch_merge_index_tree_oid;
vb_state.set_branch(branch.clone())?;
Ok(Some(branch))
};
if branch.upstream.is_some() && !ok_with_force_push {
return result_merge(branch);
}
// branch was not pushed to upstream yet. attempt a rebase,
let rebased_head_oid = cherry_rebase(
project_repository,
new_target_commit.id(),
new_target_commit.id(),
branch.head,
);
// rebase failed, just do the merge
if rebased_head_oid.is_err() {
return result_merge(branch);
}
if let Some(rebased_head_oid) = rebased_head_oid? {
// rebase worked out, rewrite the branch head
branch.head = rebased_head_oid;
branch.tree = branch_merge_index_tree_oid;
vb_state.set_branch(branch.clone())?;
return Ok(Some(branch));
}
result_merge(branch)
},
)
result_merge(branch)
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
@ -569,7 +569,7 @@ pub fn update_base_branch(
Ok(unapplied_branch_names)
}
pub fn target_to_base_branch(
pub(crate) fn target_to_base_branch(
project_repository: &ProjectRepository,
target: &Target,
) -> Result<BaseBranch> {

View File

@ -1,35 +1,29 @@
use super::{branch_removal::BranchRemoval, BranchManager};
use super::BranchManager;
use crate::{
conflicts::{self, RepoConflicts},
conflicts::{self, RepoConflictsExt},
ensure_selected_for_changes,
integration::update_gitbutler_integration,
set_ownership, undo_commit, VirtualBranchHunk, VirtualBranchesExt,
};
use anyhow::{anyhow, bail, Context, Result};
use gitbutler_branch::{
branch::{self, BranchCreateRequest, BranchId},
dedup::dedup,
diff,
ownership::BranchOwnershipClaims,
dedup, diff, Branch, BranchOwnershipClaims, {self, BranchCreateRequest, BranchId},
};
use gitbutler_commit::commit_headers::HasCommitHeaders;
use gitbutler_error::error::Marker;
use gitbutler_oplog::snapshot::Snapshot;
use gitbutler_oplog::SnapshotExt;
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_reference::Refname;
use gitbutler_repo::{rebase::cherry_rebase, RepoActions, RepositoryExt};
use gitbutler_repo::{rebase::cherry_rebase, RepoActionsExt, RepositoryExt};
use gitbutler_time::time::now_since_unix_epoch_ms;
pub trait BranchCreation {
/// Create an empty virtual branch
fn create_virtual_branch(&self, create: &BranchCreateRequest) -> Result<branch::Branch>;
/// Create a virtual branch from a real branch (whether remote or local)
fn create_virtual_branch_from_branch(&self, upstream: &Refname) -> Result<BranchId>;
}
impl BranchCreation for BranchManager<'_> {
fn create_virtual_branch(&self, create: &BranchCreateRequest) -> Result<branch::Branch> {
impl BranchManager<'_> {
pub fn create_virtual_branch(
&self,
create: &BranchCreateRequest,
perm: &mut WorktreeWritePermission,
) -> Result<Branch> {
let vb_state = self.project_repository.project().virtual_branches();
let default_target = vb_state.get_default_target()?;
let commit = self
@ -60,7 +54,7 @@ impl BranchCreation for BranchManager<'_> {
_ = self
.project_repository
.project()
.snapshot_branch_creation(name.clone());
.snapshot_branch_creation(name.clone(), perm);
all_virtual_branches.sort_by_key(|branch| branch.order);
@ -98,7 +92,7 @@ impl BranchCreation for BranchManager<'_> {
let now = gitbutler_time::time::now_ms();
let mut branch = branch::Branch {
let mut branch = Branch {
id: BranchId::generate(),
name: name.clone(),
notes: String::new(),
@ -128,7 +122,11 @@ impl BranchCreation for BranchManager<'_> {
Ok(branch)
}
fn create_virtual_branch_from_branch(&self, upstream: &Refname) -> Result<BranchId> {
pub fn create_virtual_branch_from_branch(
&self,
upstream: &Refname,
perm: &mut WorktreeWritePermission,
) -> Result<BranchId> {
// only set upstream if it's not the default target
let upstream_branch = match upstream {
Refname::Other(_) | Refname::Virtual(_) => {
@ -147,7 +145,7 @@ impl BranchCreation for BranchManager<'_> {
let _ = self
.project_repository
.project()
.snapshot_branch_creation(branch_name.clone());
.snapshot_branch_creation(branch_name.clone(), perm);
let vb_state = self.project_repository.project().virtual_branches();
@ -177,7 +175,7 @@ impl BranchCreation for BranchManager<'_> {
.list_branches_in_workspace()
.context("failed to read virtual branches")?
.into_iter()
.collect::<Vec<branch::Branch>>();
.collect::<Vec<Branch>>();
let order = vb_state.next_order_index()?;
@ -235,7 +233,7 @@ impl BranchCreation for BranchManager<'_> {
branch
} else {
branch::Branch {
Branch {
id: BranchId::generate(),
name: branch_name.clone(),
notes: String::new(),
@ -259,7 +257,7 @@ impl BranchCreation for BranchManager<'_> {
vb_state.set_branch(branch.clone())?;
self.project_repository.add_branch_reference(&branch)?;
match self.apply_branch(branch.id) {
match self.apply_branch(branch.id, perm) {
Ok(_) => Ok(branch.id),
Err(err)
if err
@ -276,7 +274,11 @@ impl BranchCreation for BranchManager<'_> {
/// Holding private methods associated to branch creation
impl BranchManager<'_> {
fn apply_branch(&self, branch_id: BranchId) -> Result<String> {
fn apply_branch(
&self,
branch_id: BranchId,
perm: &mut WorktreeWritePermission,
) -> Result<String> {
self.project_repository.assure_resolved()?;
self.project_repository.assure_unconflicted()?;
let repo = self.project_repository.repo();
@ -323,7 +325,7 @@ impl BranchManager<'_> {
.iter()
.filter(|branch| branch.id != branch_id)
{
self.convert_to_real_branch(branch.id, Default::default())?;
self.convert_to_real_branch(branch.id, Default::default(), perm)?;
}
// apply the branch

View File

@ -2,39 +2,27 @@ use crate::{
conflicts::{self},
ensure_selected_for_changes, get_applied_status,
integration::get_integration_commiter,
write_tree, NameConflitResolution, VirtualBranchesExt,
write_tree, NameConflictResolution, VirtualBranchesExt,
};
use anyhow::{anyhow, Context, Result};
use git2::build::TreeUpdateBuilder;
use gitbutler_branch::{
branch::{self, BranchId},
branch_ext::BranchExt,
};
use gitbutler_branch::{Branch, BranchExt, BranchId};
use gitbutler_commit::commit_headers::CommitHeadersV2;
use gitbutler_oplog::snapshot::Snapshot;
use gitbutler_oplog::SnapshotExt;
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_reference::ReferenceName;
use gitbutler_reference::{normalize_branch_name, Refname};
use gitbutler_repo::{RepoActions, RepositoryExt};
use gitbutler_repo::{RepoActionsExt, RepositoryExt};
use super::BranchManager;
pub trait BranchRemoval {
/// Perminently deletes a virtual branch
fn delete_branch(&self, branch_id: BranchId) -> Result<()>;
/// Converts a virtual branch into a real branch
fn convert_to_real_branch(
&self,
branch_id: BranchId,
name_conflict_resolution: NameConflitResolution,
) -> Result<ReferenceName>;
}
impl BranchRemoval for BranchManager<'_> {
impl BranchManager<'_> {
// to unapply a branch, we need to write the current tree out, then remove those file changes from the wd
fn convert_to_real_branch(
pub fn convert_to_real_branch(
&self,
branch_id: BranchId,
name_conflict_resolution: NameConflitResolution,
name_conflict_resolution: NameConflictResolution,
perm: &mut WorktreeWritePermission,
) -> Result<ReferenceName> {
let vb_state = self.project_repository.project().virtual_branches();
@ -43,7 +31,7 @@ impl BranchRemoval for BranchManager<'_> {
// Convert the vbranch to a real branch
let real_branch = self.build_real_branch(&mut target_branch, name_conflict_resolution)?;
self.delete_branch(branch_id)?;
self.delete_branch(branch_id, perm)?;
// If we were conflicting, it means that it was the only branch applied. Since we've now unapplied it we can clear all conflicts
if conflicts::is_conflicting(self.project_repository, None)? {
@ -60,7 +48,11 @@ impl BranchRemoval for BranchManager<'_> {
real_branch.reference_name()
}
fn delete_branch(&self, branch_id: BranchId) -> Result<()> {
pub(crate) fn delete_branch(
&self,
branch_id: BranchId,
perm: &mut WorktreeWritePermission,
) -> Result<()> {
let vb_state = self.project_repository.project().virtual_branches();
let Some(branch) = vb_state.try_branch(branch_id)? else {
return Ok(());
@ -74,7 +66,7 @@ impl BranchRemoval for BranchManager<'_> {
_ = self
.project_repository
.project()
.snapshot_branch_deletion(branch.name.clone());
.snapshot_branch_deletion(branch.name.clone(), perm);
let repo = self.project_repository.repo();
@ -90,6 +82,7 @@ impl BranchRemoval for BranchManager<'_> {
self.project_repository,
&integration_commit.id(),
virtual_branches,
perm,
)
.context("failed to get status by branch")?;
@ -135,8 +128,8 @@ impl BranchRemoval for BranchManager<'_> {
impl BranchManager<'_> {
fn build_real_branch(
&self,
vbranch: &mut branch::Branch,
name_conflict_resolution: NameConflitResolution,
vbranch: &mut Branch,
name_conflict_resolution: NameConflictResolution,
) -> Result<git2::Branch<'_>> {
let repo = self.project_repository.repo();
let target_commit = repo.find_commit(vbranch.head)?;
@ -149,7 +142,7 @@ impl BranchManager<'_> {
.is_ok()
{
match name_conflict_resolution {
NameConflitResolution::Suffix => {
NameConflictResolution::Suffix => {
let mut suffix = 1;
loop {
let new_branch_name = format!("{}-{}", branch_name, suffix);
@ -162,7 +155,7 @@ impl BranchManager<'_> {
suffix += 1;
}
}
NameConflitResolution::Rename(new_name) => {
NameConflictResolution::Rename(new_name) => {
if repo
.find_branch(new_name.as_str(), git2::BranchType::Local)
.is_ok()
@ -172,7 +165,7 @@ impl BranchManager<'_> {
new_name
}
}
NameConflitResolution::Overwrite => branch_name,
NameConflictResolution::Overwrite => branch_name,
}
} else {
branch_name
@ -190,7 +183,7 @@ impl BranchManager<'_> {
fn build_metadata_commit(
&self,
vbranch: &mut branch::Branch,
vbranch: &mut Branch,
branch: &git2::Branch<'_>,
) -> Result<git2::Oid> {
let repo = self.project_repository.repo();

View File

@ -1,17 +1,17 @@
use gitbutler_command_context::ProjectRepository;
pub mod branch_creation;
pub mod branch_removal;
mod branch_creation;
mod branch_removal;
pub struct BranchManager<'l> {
project_repository: &'l ProjectRepository,
}
pub trait BranchManagerAccess {
pub trait BranchManagerExt {
fn branch_manager(&self) -> BranchManager;
}
impl BranchManagerAccess for ProjectRepository {
impl BranchManagerExt for ProjectRepository {
fn branch_manager(&self) -> BranchManager {
BranchManager {
project_repository: self,

View File

@ -1,10 +1,9 @@
// stuff to manage merge conflict state
// this is the dumbest possible way to do this, but it is a placeholder
// conflicts are stored one path per line in .git/conflicts
// merge parent is stored in .git/base_merge_parent
// conflicts are removed as they are resolved, the conflicts file is removed when there are no more conflicts
// the merge parent file is removed when the merge is complete
/// stuff to manage merge conflict state.
/// This is the dumbest possible way to do this, but it is a placeholder.
/// Conflicts are stored one path per line in .git/conflicts.
/// Merge parent is stored in .git/base_merge_parent.
/// Conflicts are removed as they are resolved, the conflicts file is removed when there are no more conflicts
/// or when the merge is complete.
use std::{
io::{BufRead, Write},
path::{Path, PathBuf},
@ -16,7 +15,7 @@ use itertools::Itertools;
use gitbutler_error::error::Marker;
pub fn mark<P: AsRef<Path>, A: AsRef<[P]>>(
pub(crate) fn mark<P: AsRef<Path>, A: AsRef<[P]>>(
repository: &ProjectRepository,
paths: A,
parent: Option<git2::Oid>,
@ -25,25 +24,26 @@ pub fn mark<P: AsRef<Path>, A: AsRef<[P]>>(
if paths.is_empty() {
return Ok(());
}
let conflicts_path = repository.repo().path().join("conflicts");
// write all the file paths to a file on disk
let mut file = std::fs::File::create(conflicts_path)?;
let mut buf = Vec::<u8>::with_capacity(512);
for path in paths {
file.write_all(path.as_ref().as_os_str().as_encoded_bytes())?;
file.write_all(b"\n")?;
buf.write_all(path.as_ref().as_os_str().as_encoded_bytes())?;
buf.write_all(b"\n")?;
}
gitbutler_fs::write(repository.repo().path().join("conflicts"), buf)?;
if let Some(parent) = parent {
let merge_path = repository.repo().path().join("base_merge_parent");
// write all the file paths to a file on disk
let mut file = std::fs::File::create(merge_path)?;
file.write_all(parent.to_string().as_bytes())?;
gitbutler_fs::write(
repository.repo().path().join("base_merge_parent"),
parent.to_string().as_bytes(),
)?;
}
Ok(())
}
pub fn merge_parent(repository: &ProjectRepository) -> Result<Option<git2::Oid>> {
pub(crate) fn merge_parent(repository: &ProjectRepository) -> Result<Option<git2::Oid>> {
let merge_path = repository.repo().path().join("base_merge_parent");
if !merge_path.exists() {
return Ok(None);
@ -74,17 +74,16 @@ pub fn resolve<P: AsRef<Path>>(repository: &ProjectRepository, path: P) -> Resul
}
}
// remove file
std::fs::remove_file(conflicts_path)?;
// re-write file if needed
if !remaining.is_empty() {
// re-write file if needed, otherwise remove file entirely
if remaining.is_empty() {
std::fs::remove_file(conflicts_path)?;
} else {
mark(repository, &remaining, None)?;
}
Ok(())
}
pub fn conflicting_files(repository: &ProjectRepository) -> Result<Vec<String>> {
pub(crate) fn conflicting_files(repository: &ProjectRepository) -> Result<Vec<String>> {
let conflicts_path = repository.repo().path().join("conflicts");
if !conflicts_path.exists() {
return Ok(vec![]);
@ -97,7 +96,7 @@ pub fn conflicting_files(repository: &ProjectRepository) -> Result<Vec<String>>
/// Check if `path` is conflicting in `repository`, or if `None`, check if there is any conflict.
// TODO(ST): Should this not rather check the conflicting state in the index?
pub fn is_conflicting(repository: &ProjectRepository, path: Option<&Path>) -> Result<bool> {
pub(crate) fn is_conflicting(repository: &ProjectRepository, path: Option<&Path>) -> Result<bool> {
let conflicts_path = repository.repo().path().join("conflicts");
if !conflicts_path.exists() {
return Ok(false);
@ -124,11 +123,11 @@ pub fn is_conflicting(repository: &ProjectRepository, path: Option<&Path>) -> Re
// is this project still in a resolving conflict state?
// - could be that there are no more conflicts, but the state is not committed
pub fn is_resolving(repository: &ProjectRepository) -> bool {
pub(crate) fn is_resolving(repository: &ProjectRepository) -> bool {
repository.repo().path().join("base_merge_parent").exists()
}
pub fn clear(repository: &ProjectRepository) -> Result<()> {
pub(crate) fn clear(repository: &ProjectRepository) -> Result<()> {
let merge_path = repository.repo().path().join("base_merge_parent");
std::fs::remove_file(merge_path)?;
@ -139,13 +138,13 @@ pub fn clear(repository: &ProjectRepository) -> Result<()> {
Ok(())
}
pub trait RepoConflicts {
pub(crate) trait RepoConflictsExt {
fn assure_unconflicted(&self) -> Result<()>;
fn assure_resolved(&self) -> Result<()>;
fn is_resolving(&self) -> bool;
}
impl RepoConflicts for ProjectRepository {
impl RepoConflictsExt for ProjectRepository {
fn is_resolving(&self) -> bool {
is_resolving(self)
}

View File

@ -12,7 +12,7 @@ pub struct RemoteBranchFile {
pub binary: bool,
}
pub fn list_remote_commit_files(
pub(crate) fn list_remote_commit_files(
repository: &git2::Repository,
commit_id: git2::Oid,
) -> Result<Vec<RemoteBranchFile>> {

View File

@ -3,8 +3,8 @@ use std::{path::PathBuf, vec};
use anyhow::{anyhow, bail, Context, Result};
use bstr::ByteSlice;
use gitbutler_branch::branch::{self, BranchCreateRequest};
use gitbutler_branch::VirtualBranchesHandle;
use gitbutler_branch::{self, BranchCreateRequest};
use gitbutler_branch::{Branch, VirtualBranchesHandle};
use gitbutler_branch::{
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL, GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
GITBUTLER_INTEGRATION_REFERENCE,
@ -12,15 +12,15 @@ use gitbutler_branch::{
use gitbutler_command_context::ProjectRepository;
use gitbutler_commit::commit_ext::CommitExt;
use gitbutler_error::error::Marker;
use gitbutler_repo::{LogUntil, RepoActions, RepositoryExt};
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_repo::{LogUntil, RepoActionsExt, RepositoryExt};
use crate::branch_manager::branch_creation::BranchCreation;
use crate::branch_manager::BranchManagerAccess;
use crate::branch_manager::BranchManagerExt;
use crate::{conflicts, VirtualBranchesExt};
const WORKSPACE_HEAD: &str = "Workspace Head";
pub fn get_integration_commiter<'a>() -> Result<git2::Signature<'a>> {
pub(crate) fn get_integration_commiter<'a>() -> Result<git2::Signature<'a>> {
Ok(git2::Signature::now(
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL,
@ -31,7 +31,7 @@ pub fn get_integration_commiter<'a>() -> Result<git2::Signature<'a>> {
//
// This is the base against which we diff the working directory to understand
// what files have been modified.
pub fn get_workspace_head(
pub(crate) fn get_workspace_head(
vb_state: &VirtualBranchesHandle,
project_repo: &ProjectRepository,
) -> Result<git2::Oid> {
@ -41,7 +41,7 @@ pub fn get_workspace_head(
let repo: &git2::Repository = project_repo.repo();
let vb_state = project_repo.project().virtual_branches();
let virtual_branches: Vec<branch::Branch> = vb_state.list_branches_in_workspace()?;
let virtual_branches: Vec<Branch> = vb_state.list_branches_in_workspace()?;
let target_commit = repo.find_commit(target.sha)?;
let mut workspace_tree = target_commit.tree()?;
@ -93,7 +93,7 @@ pub fn get_workspace_head(
}
// TODO: Why does commit only accept a slice of commits? Feels like we
// could make use of AsRef with the right traits.
// could make use of AsRef with the right traits.
let head_refs: Vec<&git2::Commit<'_>> = heads.iter().collect();
let workspace_head_id = repo.commit(
@ -167,7 +167,7 @@ pub fn update_gitbutler_integration(
let vb_state = project_repository.project().virtual_branches();
// get all virtual branches, we need to try to update them all
let virtual_branches: Vec<branch::Branch> = vb_state
let virtual_branches: Vec<Branch> = vb_state
.list_branches_in_workspace()
.context("failed to list virtual branches")?;
@ -282,138 +282,130 @@ pub fn update_gitbutler_integration(
Ok(final_commit)
}
pub fn verify_branch(project_repository: &ProjectRepository) -> Result<()> {
project_repository
.verify_current_branch_name()
.and_then(|me| me.verify_head_is_set())
.and_then(|me| me.verify_head_is_clean())
pub fn verify_branch(ctx: &ProjectRepository, perm: &mut WorktreeWritePermission) -> Result<()> {
verify_current_branch_name(ctx)
.and_then(verify_head_is_set)
.and_then(|()| verify_head_is_clean(ctx, perm))
.context(Marker::VerificationFailure)?;
Ok(())
}
pub trait Verify {
fn verify_head_is_set(&self) -> Result<&Self>;
fn verify_current_branch_name(&self) -> Result<&Self>;
fn verify_head_is_clean(&self) -> Result<&Self>;
fn verify_head_is_set(ctx: &ProjectRepository) -> Result<()> {
match ctx.repo().head().context("failed to get head")?.name() {
Some(refname) if *refname == GITBUTLER_INTEGRATION_REFERENCE.to_string() => Ok(()),
Some(head_name) => Err(invalid_head_err(head_name)),
None => Err(anyhow!(
"project in detached head state. Please checkout {} to continue",
GITBUTLER_INTEGRATION_REFERENCE.branch()
)),
}
}
impl Verify for ProjectRepository {
fn verify_head_is_set(&self) -> Result<&Self> {
match self.repo().head().context("failed to get head")?.name() {
Some(refname) if *refname == GITBUTLER_INTEGRATION_REFERENCE.to_string() => Ok(self),
Some(head_name) => Err(invalid_head_err(head_name)),
None => Err(anyhow!(
"project in detached head state. Please checkout {} to continue",
GITBUTLER_INTEGRATION_REFERENCE.branch()
)),
}
}
// Returns an error if repo head is not pointing to the integration branch.
fn verify_current_branch_name(&self) -> Result<&Self> {
match self.repo().head()?.name() {
Some(head) => {
let head_name = head.to_string();
if head_name != GITBUTLER_INTEGRATION_REFERENCE.to_string() {
return Err(invalid_head_err(&head_name));
}
Ok(self)
// Returns an error if repo head is not pointing to the integration branch.
fn verify_current_branch_name(ctx: &ProjectRepository) -> Result<&ProjectRepository> {
match ctx.repo().head()?.name() {
Some(head) => {
let head_name = head.to_string();
if head_name != GITBUTLER_INTEGRATION_REFERENCE.to_string() {
return Err(invalid_head_err(&head_name));
}
None => Err(anyhow!("Repo HEAD is unavailable")),
Ok(ctx)
}
None => Err(anyhow!("Repo HEAD is unavailable")),
}
}
// TODO(ST): Probably there should not be an implicit vbranch creation here.
fn verify_head_is_clean(ctx: &ProjectRepository, perm: &mut WorktreeWritePermission) -> Result<()> {
let head_commit = ctx
.repo()
.head()
.context("failed to get head")?
.peel_to_commit()
.context("failed to peel to commit")?;
let vb_handle = VirtualBranchesHandle::new(ctx.project().gb_dir());
let default_target = vb_handle
.get_default_target()
.context("failed to get default target")?;
let mut extra_commits = ctx
.log(head_commit.id(), LogUntil::Commit(default_target.sha))
.context("failed to get log")?;
let integration_commit = extra_commits.pop();
if integration_commit.is_none() {
// no integration commit found
bail!("gibButler's integration commit not found on head");
}
fn verify_head_is_clean(&self) -> Result<&Self> {
let head_commit = self
.repo()
.head()
.context("failed to get head")?
.peel_to_commit()
.context("failed to peel to commit")?;
if extra_commits.is_empty() {
// no extra commits found, so we're good
return Ok(());
}
let vb_handle = VirtualBranchesHandle::new(self.project().gb_dir());
let default_target = vb_handle
.get_default_target()
.context("failed to get default target")?;
ctx.repo()
.reset(
integration_commit.as_ref().unwrap().as_object(),
git2::ResetType::Soft,
None,
)
.context("failed to reset to integration commit")?;
let mut extra_commits = self
.log(head_commit.id(), LogUntil::Commit(default_target.sha))
.context("failed to get log")?;
let integration_commit = extra_commits.pop();
if integration_commit.is_none() {
// no integration commit found
bail!("gibButler's integration commit not found on head");
}
if extra_commits.is_empty() {
// no extra commits found, so we're good
return Ok(self);
}
self.repo()
.reset(
integration_commit.as_ref().unwrap().as_object(),
git2::ResetType::Soft,
None,
)
.context("failed to reset to integration commit")?;
let branch_manager = self.branch_manager();
let mut new_branch = branch_manager
.create_virtual_branch(&BranchCreateRequest {
let branch_manager = ctx.branch_manager();
let mut new_branch = branch_manager
.create_virtual_branch(
&BranchCreateRequest {
name: extra_commits
.last()
.map(|commit| commit.message_bstr().to_string()),
..Default::default()
})
.context("failed to create virtual branch")?;
},
perm,
)
.context("failed to create virtual branch")?;
// rebasing the extra commits onto the new branch
let vb_state = self.project().virtual_branches();
extra_commits.reverse();
let mut head = new_branch.head;
for commit in extra_commits {
let new_branch_head = self
.repo()
.find_commit(head)
.context("failed to find new branch head")?;
// rebasing the extra commits onto the new branch
let vb_state = ctx.project().virtual_branches();
extra_commits.reverse();
let mut head = new_branch.head;
for commit in extra_commits {
let new_branch_head = ctx
.repo()
.find_commit(head)
.context("failed to find new branch head")?;
let rebased_commit_oid = self
.repo()
.commit_with_signature(
None,
&commit.author(),
&commit.committer(),
&commit.message_bstr().to_str_lossy(),
&commit.tree().unwrap(),
&[&new_branch_head],
None,
)
.context(format!(
"failed to rebase commit {} onto new branch",
commit.id()
))?;
let rebased_commit_oid = ctx
.repo()
.commit_with_signature(
None,
&commit.author(),
&commit.committer(),
&commit.message_bstr().to_str_lossy(),
&commit.tree().unwrap(),
&[&new_branch_head],
None,
)
.context(format!(
"failed to rebase commit {} onto new branch",
commit.id()
))?;
let rebased_commit = self
.repo()
.find_commit(rebased_commit_oid)
.context(format!(
"failed to find rebased commit {}",
rebased_commit_oid
))?;
let rebased_commit = ctx.repo().find_commit(rebased_commit_oid).context(format!(
"failed to find rebased commit {}",
rebased_commit_oid
))?;
new_branch.head = rebased_commit.id();
new_branch.tree = rebased_commit.tree_id();
vb_state
.set_branch(new_branch.clone())
.context("failed to write branch")?;
new_branch.head = rebased_commit.id();
new_branch.tree = rebased_commit.tree_id();
vb_state
.set_branch(new_branch.clone())
.context("failed to write branch")?;
head = rebased_commit.id();
}
Ok(self)
head = rebased_commit.id();
}
Ok(())
}
fn invalid_head_err(head_name: &str) -> anyhow::Error {

View File

@ -1,19 +1,24 @@
//! GitButler internal library containing functionaliry related to branches, i.e. the virtual branches implementation
pub mod actions;
//! GitButler internal library containing functionality related to branches, i.e. the virtual branches implementation
mod actions;
pub use actions::VirtualBranchActions;
pub mod r#virtual;
mod r#virtual;
pub use r#virtual::*;
pub mod branch_manager;
mod branch_manager;
pub use branch_manager::{BranchManager, BranchManagerExt};
pub mod base;
mod base;
pub use base::BaseBranch;
pub mod integration;
mod integration;
pub use integration::{update_gitbutler_integration, verify_branch};
pub mod files;
mod files;
pub use files::RemoteBranchFile;
pub mod remote;
mod remote;
pub use remote::{list_remote_branches, RemoteBranch, RemoteBranchData, RemoteCommit};
pub mod conflicts;

View File

@ -2,15 +2,13 @@ use std::path::Path;
use anyhow::{Context, Result};
use bstr::BString;
use gitbutler_branch::VirtualBranchesHandle;
use gitbutler_branch::{Target, VirtualBranchesHandle};
use gitbutler_command_context::ProjectRepository;
use gitbutler_commit::commit_ext::CommitExt;
use gitbutler_reference::{Refname, RemoteRefname};
use gitbutler_repo::{LogUntil, RepoActions, RepositoryExt};
use gitbutler_repo::{LogUntil, RepoActionsExt, RepositoryExt};
use serde::Serialize;
use gitbutler_branch::target;
use crate::author::Author;
// this struct is a mapping to the view `RemoteBranch` type in Typescript
@ -71,7 +69,7 @@ pub fn list_remote_branches(project_repository: &ProjectRepository) -> Result<Ve
.context("failed to list remote branches")?
.flatten()
{
let branch = branch_to_remote_branch(&branch)?;
let branch = branch_to_remote_branch(&branch);
if let Some(branch) = branch {
let branch_is_trunk = branch.name.branch() == Some(default_target.branch.branch())
@ -88,7 +86,7 @@ pub fn list_remote_branches(project_repository: &ProjectRepository) -> Result<Ve
Ok(remote_branches)
}
pub fn get_branch_data(
pub(crate) fn get_branch_data(
project_repository: &ProjectRepository,
refname: &Refname,
) -> Result<RemoteBranchData> {
@ -103,7 +101,7 @@ pub fn get_branch_data(
.context("failed to get branch data")
}
pub fn branch_to_remote_branch(branch: &git2::Branch) -> Result<Option<RemoteBranch>> {
pub(crate) fn branch_to_remote_branch(branch: &git2::Branch) -> Option<RemoteBranch> {
let commit = match branch.get().peel_to_commit() {
Ok(c) => c,
Err(err) => {
@ -112,41 +110,31 @@ pub fn branch_to_remote_branch(branch: &git2::Branch) -> Result<Option<RemoteBra
"ignoring branch {:?} as peeling failed",
branch.name()
);
return Ok(None);
return None;
}
};
let name = Refname::try_from(branch).context("could not get branch name");
match name {
Ok(name) => branch
.get()
.target()
.map(|sha| {
Ok(RemoteBranch {
sha,
upstream: if let Refname::Local(local_name) = &name {
local_name.remote().cloned()
} else {
None
},
name,
last_commit_timestamp_ms: commit
.time()
.seconds()
.try_into()
.map(|t: u128| t * 1000)
.ok(),
last_commit_author: commit
.author()
.name()
.map(std::string::ToString::to_string),
})
})
.transpose(),
Err(_) => Ok(None),
}
let name = Refname::try_from(branch)
.context("could not get branch name")
.ok()?;
branch.get().target().map(|sha| RemoteBranch {
sha,
upstream: if let Refname::Local(local_name) = &name {
local_name.remote().cloned()
} else {
None
},
name,
last_commit_timestamp_ms: commit
.time()
.seconds()
.try_into()
.map(|t: u128| t * 1000)
.ok(),
last_commit_author: commit.author().name().map(std::string::ToString::to_string),
})
}
pub fn branch_to_remote_branch_data(
pub(crate) fn branch_to_remote_branch_data(
project_repository: &ProjectRepository,
branch: &git2::Branch,
base: git2::Oid,
@ -186,8 +174,8 @@ pub fn branch_to_remote_branch_data(
.transpose()
}
pub fn commit_to_remote_commit(commit: &git2::Commit) -> RemoteCommit {
let parent_ids: Vec<git2::Oid> = commit.parents().map(|c| c.id()).collect::<Vec<_>>();
pub(crate) fn commit_to_remote_commit(commit: &git2::Commit) -> RemoteCommit {
let parent_ids = commit.parents().map(|c| c.id()).collect();
RemoteCommit {
id: commit.id().to_string(),
description: commit.message_bstr().to_owned(),
@ -198,6 +186,6 @@ pub fn commit_to_remote_commit(commit: &git2::Commit) -> RemoteCommit {
}
}
fn default_target(base_path: &Path) -> Result<target::Target> {
fn default_target(base_path: &Path) -> Result<Target> {
VirtualBranchesHandle::new(base_path).get_default_target()
}

View File

@ -1,16 +1,15 @@
use gitbutler_branch::branch::{self, Branch, BranchCreateRequest, BranchId};
use gitbutler_branch::dedup::{dedup, dedup_fmt};
use gitbutler_branch::diff::{self, diff_files_into_hunks, trees, FileDiff, GitHunk};
use gitbutler_branch::file_ownership::OwnershipClaim;
use gitbutler_branch::hunk::{Hunk, HunkHash};
use gitbutler_branch::ownership::{reconcile_claims, BranchOwnershipClaims};
use gitbutler_branch::VirtualBranchesHandle;
use gitbutler_branch::{dedup, BranchUpdateRequest, VirtualBranchesHandle};
use gitbutler_branch::{dedup_fmt, Branch, BranchCreateRequest, BranchId};
use gitbutler_branch::{reconcile_claims, BranchOwnershipClaims};
use gitbutler_branch::{Hunk, HunkHash};
use gitbutler_branch::{OwnershipClaim, Target};
use gitbutler_command_context::ProjectRepository;
use gitbutler_commit::commit_ext::CommitExt;
use gitbutler_commit::commit_headers::HasCommitHeaders;
use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname};
use gitbutler_repo::credentials::Helper;
use gitbutler_repo::{LogUntil, RepoActions, RepositoryExt};
use gitbutler_repo::{LogUntil, RepoActionsExt, RepositoryExt};
use std::borrow::Borrow;
#[cfg(target_family = "unix")]
use std::os::unix::prelude::PermissionsExt;
@ -29,20 +28,18 @@ use hex::ToHex;
use serde::{Deserialize, Serialize};
use crate::author::Author;
use crate::branch_manager::branch_creation::BranchCreation;
use crate::branch_manager::branch_removal::BranchRemoval;
use crate::branch_manager::BranchManagerAccess;
use crate::conflicts::{self, RepoConflicts};
use crate::branch_manager::BranchManagerExt;
use crate::conflicts::{self, RepoConflictsExt};
use crate::integration::get_workspace_head;
use crate::remote::{branch_to_remote_branch, RemoteBranch};
use crate::VirtualBranchesExt;
use gitbutler_branch::target;
use gitbutler_error::error::Code;
use gitbutler_error::error::Marker;
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_repo::rebase::{cherry_rebase, cherry_rebase_group};
use gitbutler_time::time::now_since_unix_epoch_ms;
type AppliedStatuses = Vec<(branch::Branch, BranchStatus)>;
type AppliedStatuses = Vec<(Branch, BranchStatus)>;
// this struct is a mapping to the view `Branch` type in Typescript
// found in src-tauri/src/routes/repo/[project_id]/types.ts
@ -165,7 +162,7 @@ pub struct VirtualBranchHunk {
/// Lifecycle
impl VirtualBranchHunk {
pub fn gen_id(new_start: u32, new_lines: u32) -> String {
pub(crate) fn gen_id(new_start: u32, new_lines: u32) -> String {
format!("{}-{}", new_start, new_start + new_lines)
}
fn from_git_hunk(
@ -194,7 +191,7 @@ impl VirtualBranchHunk {
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type", content = "value")]
pub enum NameConflitResolution {
pub enum NameConflictResolution {
#[default]
Suffix,
Rename(String),
@ -204,6 +201,7 @@ pub enum NameConflitResolution {
pub fn unapply_ownership(
project_repository: &ProjectRepository,
ownership: &BranchOwnershipClaims,
perm: &mut WorktreeWritePermission,
) -> Result<()> {
project_repository.assure_resolved()?;
@ -215,9 +213,13 @@ pub fn unapply_ownership(
let integration_commit_id = get_workspace_head(&vb_state, project_repository)?;
let (applied_statuses, _) =
get_applied_status(project_repository, &integration_commit_id, virtual_branches)
.context("failed to get status by branch")?;
let (applied_statuses, _) = get_applied_status(
project_repository,
&integration_commit_id,
virtual_branches,
perm,
)
.context("failed to get status by branch")?;
let hunks_to_unapply = applied_statuses
.iter()
@ -294,7 +296,10 @@ pub fn unapply_ownership(
}
// reset a file in the project to the index state
pub fn reset_files(project_repository: &ProjectRepository, files: &Vec<String>) -> Result<()> {
pub(crate) fn reset_files(
project_repository: &ProjectRepository,
files: &Vec<String>,
) -> Result<()> {
project_repository.assure_resolved()?;
// for each tree, we need to checkout the entry from the index at that path
@ -347,6 +352,7 @@ fn find_base_tree<'a>(
fn resolve_old_applied_state(
project_repository: &ProjectRepository,
vb_state: &VirtualBranchesHandle,
perm: &mut WorktreeWritePermission,
) -> Result<()> {
let branches = vb_state.list_all_branches()?;
@ -354,7 +360,7 @@ fn resolve_old_applied_state(
for mut branch in branches {
if branch.is_old_unapplied() {
branch_manager.convert_to_real_branch(branch.id, Default::default())?;
branch_manager.convert_to_real_branch(branch.id, Default::default(), perm)?;
} else {
branch.applied = branch.in_workspace;
vb_state.set_branch(branch)?;
@ -365,27 +371,26 @@ fn resolve_old_applied_state(
}
pub fn list_virtual_branches(
project_repository: &ProjectRepository,
ctx: &ProjectRepository,
// TODO(ST): this should really only shared access, but there is some internals
// that conditionally write things.
perm: &mut WorktreeWritePermission,
) -> Result<(Vec<VirtualBranch>, Vec<diff::FileDiff>)> {
let mut branches: Vec<VirtualBranch> = Vec::new();
let vb_state = project_repository.project().virtual_branches();
let vb_state = ctx.project().virtual_branches();
resolve_old_applied_state(project_repository, &vb_state)?;
resolve_old_applied_state(ctx, &vb_state, perm)?;
let default_target = vb_state
.get_default_target()
.context("failed to get default target")?;
let integration_commit_id =
crate::integration::get_workspace_head(&vb_state, project_repository)?;
let integration_commit = project_repository
.repo()
.find_commit(integration_commit_id)
.unwrap();
let integration_commit_id = get_workspace_head(&vb_state, ctx)?;
let integration_commit = ctx.repo().find_commit(integration_commit_id).unwrap();
let (statuses, skipped_files) =
get_status_by_branch(project_repository, Some(&integration_commit.id()))?;
get_status_by_branch(ctx, Some(&integration_commit.id()), perm)?;
let max_selected_for_changes = statuses
.iter()
.filter_map(|(branch, _)| branch.selected_for_changes)
@ -393,8 +398,8 @@ pub fn list_virtual_branches(
.unwrap_or(-1);
for (branch, files) in statuses {
let repo = project_repository.repo();
update_conflict_markers(project_repository, &files)?;
let repo = ctx.repo();
update_conflict_markers(ctx, &files)?;
let upstream_branch = match branch.clone().upstream {
Some(upstream) => repo.find_branch_by_refname(&Refname::from(upstream))?,
@ -420,7 +425,7 @@ pub fn list_virtual_branches(
upstream.id(),
default_target.sha
))?;
for oid in project_repository.l(upstream.id(), LogUntil::Commit(merge_base))? {
for oid in ctx.l(upstream.id(), LogUntil::Commit(merge_base))? {
pushed_commits.insert(oid, true);
}
}
@ -429,7 +434,7 @@ pub fn list_virtual_branches(
let mut is_remote = false;
// find all commits on head that are not on target.sha
let commits = project_repository.log(branch.head, LogUntil::Commit(default_target.sha))?;
let commits = ctx.log(branch.head, LogUntil::Commit(default_target.sha))?;
let vbranch_commits = commits
.iter()
.map(|commit| {
@ -443,16 +448,10 @@ pub fn list_virtual_branches(
is_integrated = if is_integrated {
is_integrated
} else {
is_commit_integrated(project_repository, &default_target, commit)?
is_commit_integrated(ctx, &default_target, commit)?
};
commit_to_vbranch_commit(
project_repository,
&branch,
commit,
is_integrated,
is_remote,
)
commit_to_vbranch_commit(ctx, &branch, commit, is_integrated, is_remote)
})
.collect::<Result<Vec<_>>>()?;
@ -461,12 +460,10 @@ pub fn list_virtual_branches(
.context("failed to find merge base")?;
let base_current = true;
let upstream = upstream_branch
.map(|upstream_branch| branch_to_remote_branch(&upstream_branch))
.transpose()?
.flatten();
let upstream =
upstream_branch.and_then(|upstream_branch| branch_to_remote_branch(&upstream_branch));
let mut files = diffs_into_virtual_files(project_repository, files);
let mut files = diffs_into_virtual_files(ctx, files);
let path_claim_positions: HashMap<&PathBuf, usize> = branch
.ownership
@ -483,7 +480,7 @@ pub fn list_virtual_branches(
.cmp(path_claim_positions.get(&b.path).unwrap_or(&usize::MAX))
});
let requires_force = is_requires_force(project_repository, &branch)?;
let requires_force = is_requires_force(ctx, &branch)?;
let fork_point = commits
.last()
@ -503,7 +500,7 @@ pub fn list_virtual_branches(
upstream_name: branch
.upstream
.and_then(|r| Refname::from(r).branch().map(Into::into)),
conflicted: conflicts::is_resolving(project_repository),
conflicted: conflicts::is_resolving(ctx),
base_current,
ownership: branch.ownership,
updated_at: branch.updated_timestamp_ms,
@ -542,10 +539,7 @@ fn joined(start_a: u32, end_a: u32, start_b: u32, end_b: u32) -> bool {
|| ((start_b >= start_a && start_b <= end_a) || (end_b >= start_a && end_b <= end_a))
}
fn is_requires_force(
project_repository: &ProjectRepository,
branch: &branch::Branch,
) -> Result<bool> {
fn is_requires_force(project_repository: &ProjectRepository, branch: &Branch) -> Result<bool> {
let upstream = if let Some(upstream) = &branch.upstream {
upstream
} else {
@ -593,7 +587,7 @@ fn list_virtual_commit_files(
fn commit_to_vbranch_commit(
repository: &ProjectRepository,
branch: &branch::Branch,
branch: &Branch,
commit: &git2::Commit,
is_integrated: bool,
is_remote: bool,
@ -784,7 +778,7 @@ pub fn integrate_upstream_commits(
Ok(())
}
pub fn integrate_with_rebase(
pub(crate) fn integrate_with_rebase(
project_repository: &ProjectRepository,
branch: &mut Branch,
unknown_commits: &mut Vec<git2::Oid>,
@ -796,7 +790,7 @@ pub fn integrate_with_rebase(
)
}
pub fn integrate_with_merge(
pub(crate) fn integrate_with_merge(
project_repository: &ProjectRepository,
branch: &mut Branch,
upstream_commit: &git2::Commit,
@ -852,8 +846,8 @@ pub fn integrate_with_merge(
pub fn update_branch(
project_repository: &ProjectRepository,
branch_update: &branch::BranchUpdateRequest,
) -> Result<branch::Branch> {
branch_update: &BranchUpdateRequest,
) -> Result<Branch> {
let vb_state = project_repository.project().virtual_branches();
let mut branch = vb_state.get_branch_in_workspace(branch_update.id)?;
@ -929,7 +923,7 @@ pub fn update_branch(
Ok(branch)
}
pub fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> {
pub(crate) fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> {
let mut virtual_branches = vb_state
.list_branches_in_workspace()
.context("failed to list branches")?;
@ -954,10 +948,10 @@ pub fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<(
Ok(())
}
pub fn set_ownership(
pub(crate) fn set_ownership(
vb_state: &VirtualBranchesHandle,
target_branch: &mut branch::Branch,
ownership: &gitbutler_branch::ownership::BranchOwnershipClaims,
target_branch: &mut Branch,
ownership: &BranchOwnershipClaims,
) -> Result<()> {
if target_branch.ownership.eq(ownership) {
// nothing to update
@ -1029,7 +1023,7 @@ pub(super) fn virtual_hunks_by_git_hunks<'a>(
})
}
pub fn virtual_hunks_by_file_diffs<'a>(
pub(crate) fn virtual_hunks_by_file_diffs<'a>(
project_path: &'a Path,
diff: impl IntoIterator<Item = (PathBuf, FileDiff)> + 'a,
) -> impl Iterator<Item = (PathBuf, Vec<VirtualBranchHunk>)> + 'a {
@ -1048,6 +1042,7 @@ pub type VirtualBranchHunksByPathMap = HashMap<PathBuf, Vec<VirtualBranchHunk>>;
pub fn get_status_by_branch(
project_repository: &ProjectRepository,
integration_commit: Option<&git2::Oid>,
perm: &mut WorktreeWritePermission,
) -> Result<(AppliedStatuses, Vec<diff::FileDiff>)> {
let vb_state = project_repository.project().virtual_branches();
@ -1062,6 +1057,7 @@ pub fn get_status_by_branch(
// TODO: Keep this optional or update lots of tests?
integration_commit.unwrap_or(&default_target.sha),
virtual_branches,
perm,
)?;
Ok((applied_status, skipped_files))
@ -1070,7 +1066,7 @@ pub fn get_status_by_branch(
fn new_compute_locks(
repository: &git2::Repository,
unstaged_hunks_by_path: &HashMap<PathBuf, Vec<diff::GitHunk>>,
virtual_branches: &[branch::Branch],
virtual_branches: &[Branch],
) -> Result<HashMap<HunkHash, Vec<diff::HunkLock>>> {
// If we cant find the integration commit and subsequently the target commit, we can't find any locks
let target_tree = repository.target_commit()?.tree()?;
@ -1095,8 +1091,7 @@ fn new_compute_locks(
})
.collect::<Vec<_>>();
let mut integration_hunks_by_path =
HashMap::<PathBuf, Vec<(diff::GitHunk, &branch::Branch)>>::new();
let mut integration_hunks_by_path = HashMap::<PathBuf, Vec<(diff::GitHunk, &Branch)>>::new();
for (branch, hunks_by_filepath) in branch_path_diffs {
for (path, hunks) in hunks_by_filepath {
@ -1145,7 +1140,8 @@ fn new_compute_locks(
pub(crate) fn get_applied_status(
project_repository: &ProjectRepository,
integration_commit: &git2::Oid,
mut virtual_branches: Vec<branch::Branch>,
mut virtual_branches: Vec<Branch>,
perm: &mut WorktreeWritePermission,
) -> Result<(AppliedStatuses, Vec<diff::FileDiff>)> {
let base_file_diffs = diff::workdir(project_repository.repo(), &integration_commit.to_owned())
.context("failed to diff workdir")?;
@ -1165,7 +1161,7 @@ pub(crate) fn get_applied_status(
if virtual_branches.is_empty() && !base_diffs.is_empty() {
virtual_branches = vec![branch_manager
.create_virtual_branch(&BranchCreateRequest::default())
.create_virtual_branch(&BranchCreateRequest::default(), perm)
.context("failed to create default branch")?];
}
@ -1344,7 +1340,7 @@ fn virtual_hunks_into_virtual_files(
}
// reset virtual branch to a specific commit
pub fn reset_branch(
pub(crate) fn reset_branch(
project_repository: &ProjectRepository,
branch_id: BranchId,
target_commit_id: git2::Oid,
@ -1426,7 +1422,7 @@ fn diffs_into_virtual_files(
// this function takes a list of file ownership,
// constructs a tree from those changes on top of the target
// and writes it as a new tree for storage
pub fn write_tree(
pub(crate) fn write_tree(
project_repository: &ProjectRepository,
target: &git2::Oid,
files: impl IntoIterator<Item = (impl Borrow<PathBuf>, impl Borrow<Vec<diff::GitHunk>>)>,
@ -1434,7 +1430,7 @@ pub fn write_tree(
write_tree_onto_commit(project_repository, *target, files)
}
pub fn write_tree_onto_commit(
pub(crate) fn write_tree_onto_commit(
project_repository: &ProjectRepository,
commit_oid: git2::Oid,
files: impl IntoIterator<Item = (impl Borrow<PathBuf>, impl Borrow<Vec<diff::GitHunk>>)>,
@ -1448,7 +1444,7 @@ pub fn write_tree_onto_commit(
write_tree_onto_tree(project_repository, &base_tree, files)
}
pub fn write_tree_onto_tree(
pub(crate) fn write_tree_onto_tree(
project_repository: &ProjectRepository,
base_tree: &git2::Tree,
files: impl IntoIterator<Item = (impl Borrow<PathBuf>, impl Borrow<Vec<diff::GitHunk>>)>,
@ -1600,8 +1596,9 @@ pub fn commit(
project_repository: &ProjectRepository,
branch_id: BranchId,
message: &str,
ownership: Option<&gitbutler_branch::ownership::BranchOwnershipClaims>,
ownership: Option<&BranchOwnershipClaims>,
run_hooks: bool,
perm: &mut WorktreeWritePermission,
) -> Result<git2::Oid> {
let mut message_buffer = message.to_owned();
let vb_state = project_repository.project().virtual_branches();
@ -1631,8 +1628,9 @@ pub fn commit(
let integration_commit_id = get_workspace_head(&vb_state, project_repository)?;
// get the files to commit
let (statuses, _) = get_status_by_branch(project_repository, Some(&integration_commit_id))
.context("failed to get status by branch")?;
let (statuses, _) =
get_status_by_branch(project_repository, Some(&integration_commit_id), perm)
.context("failed to get status by branch")?;
let (ref mut branch, files) = statuses
.into_iter()
@ -1717,7 +1715,7 @@ pub fn commit(
Ok(commit_oid)
}
pub fn push(
pub(crate) fn push(
project_repository: &ProjectRepository,
branch_id: BranchId,
with_force: bool,
@ -1786,7 +1784,7 @@ pub fn push(
fn is_commit_integrated(
project_repository: &ProjectRepository,
target: &target::Target,
target: &Target,
commit: &git2::Commit,
) -> Result<bool> {
let remote_branch = project_repository
@ -1891,7 +1889,7 @@ pub fn is_remote_branch_mergeable(
// and the rebase should be simple. if the "to" commit is above the "from" commit,
// the changes need to be removed from the "from" commit, everything rebased,
// then added to the "to" commit and everything above that rebased again.
pub fn move_commit_file(
pub(crate) fn move_commit_file(
project_repository: &ProjectRepository,
branch_id: BranchId,
from_commit_id: git2::Oid,
@ -2128,11 +2126,12 @@ pub fn move_commit_file(
// takes a list of file ownership and a commit oid and rewrites that commit to
// add the file changes. The branch is then rebased onto the new commit
// and the respective branch head is updated
pub fn amend(
pub(crate) fn amend(
project_repository: &ProjectRepository,
branch_id: BranchId,
commit_oid: git2::Oid,
target_ownership: &BranchOwnershipClaims,
perm: &mut WorktreeWritePermission,
) -> Result<git2::Oid> {
project_repository.assure_resolved()?;
let vb_state = project_repository.project().virtual_branches();
@ -2147,11 +2146,14 @@ pub fn amend(
let default_target = vb_state.get_default_target()?;
let integration_commit_id =
crate::integration::get_workspace_head(&vb_state, project_repository)?;
let integration_commit_id = get_workspace_head(&vb_state, project_repository)?;
let (mut applied_statuses, _) =
get_applied_status(project_repository, &integration_commit_id, virtual_branches)?;
let (mut applied_statuses, _) = get_applied_status(
project_repository,
&integration_commit_id,
virtual_branches,
perm,
)?;
let (ref mut target_branch, target_status) = applied_statuses
.iter_mut()
@ -2262,7 +2264,7 @@ pub fn amend(
// if the offset is positive, move the commit down one
// if the offset is negative, move the commit up one
// rewrites the branch head to the new head commit
pub fn reorder_commit(
pub(crate) fn reorder_commit(
project_repository: &ProjectRepository,
branch_id: BranchId,
commit_oid: git2::Oid,
@ -2347,7 +2349,7 @@ pub fn reorder_commit(
// create and insert a blank commit (no tree change) either above or below a commit
// if offset is positive, insert below, if negative, insert above
// return the oid of the new head commit of the branch with the inserted blank commit
pub fn insert_blank_commit(
pub(crate) fn insert_blank_commit(
project_repository: &ProjectRepository,
branch_id: BranchId,
commit_oid: git2::Oid,
@ -2401,7 +2403,7 @@ pub fn insert_blank_commit(
// remove a commit in a branch by rebasing all commits _except_ for it onto it's parent
// if successful, it will update the branch head to the new head commit
pub fn undo_commit(
pub(crate) fn undo_commit(
project_repository: &ProjectRepository,
branch_id: BranchId,
commit_oid: git2::Oid,
@ -2452,7 +2454,7 @@ pub fn undo_commit(
}
/// squashes a commit from a virtual branch into its parent.
pub fn squash(
pub(crate) fn squash(
project_repository: &ProjectRepository,
branch_id: BranchId,
commit_id: git2::Oid,
@ -2541,7 +2543,7 @@ pub fn squash(
}
// changes a commit message for commit_oid, rebases everything above it, updates branch head if successful
pub fn update_commit_message(
pub(crate) fn update_commit_message(
project_repository: &ProjectRepository,
branch_id: BranchId,
commit_id: git2::Oid,
@ -2615,10 +2617,11 @@ pub fn update_commit_message(
}
/// moves commit from the branch it's in to the top of the target branch
pub fn move_commit(
pub(crate) fn move_commit(
project_repository: &ProjectRepository,
target_branch_id: BranchId,
commit_id: git2::Oid,
perm: &mut WorktreeWritePermission,
) -> Result<()> {
project_repository.assure_resolved()?;
let vb_state = project_repository.project().virtual_branches();
@ -2631,11 +2634,14 @@ pub fn move_commit(
bail!("branch {target_branch_id} is not among applied branches")
}
let integration_commit_id =
crate::integration::get_workspace_head(&vb_state, project_repository)?;
let integration_commit_id = get_workspace_head(&vb_state, project_repository)?;
let (mut applied_statuses, _) =
get_applied_status(project_repository, &integration_commit_id, applied_branches)?;
let (mut applied_statuses, _) = get_applied_status(
project_repository,
&integration_commit_id,
applied_branches,
perm,
)?;
let (ref mut source_branch, source_status) = applied_statuses
.iter_mut()
@ -2747,7 +2753,7 @@ pub fn move_commit(
}
/// Just like [`diffy::apply()`], but on error it will attach hashes of the input `base_image` and `patch`.
pub fn apply<S: AsRef<[u8]>>(base_image: S, patch: &Patch<'_, [u8]>) -> Result<BString> {
pub(crate) fn apply<S: AsRef<[u8]>>(base_image: S, patch: &Patch<'_, [u8]>) -> Result<BString> {
fn md5_hash_hex(b: impl AsRef<[u8]>) -> String {
md5::compute(b).encode_hex()
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
use gitbutler_branch::ownership::BranchOwnershipClaims;
use gitbutler_branch::BranchOwnershipClaims;
use gitbutler_branch::{BranchCreateRequest, BranchUpdateRequest};
use super::*;
@ -35,7 +36,7 @@ async fn forcepush_allowed() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -90,14 +91,14 @@ async fn forcepush_forbidden() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
controller
.update_virtual_branch(
project,
branch::BranchUpdateRequest {
BranchUpdateRequest {
id: branch_id,
allow_rebasing: Some(false),
..Default::default()
@ -147,7 +148,7 @@ async fn non_locked_hunk() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -208,7 +209,7 @@ async fn locked_hunk() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -278,7 +279,7 @@ async fn non_existing_ownership() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();

View File

@ -1,3 +1,4 @@
use gitbutler_branch::BranchCreateRequest;
use gitbutler_reference::Refname;
use super::*;
@ -30,7 +31,7 @@ async fn rebase_commit() {
let mut branch1_id = {
// create a branch with some commited work
let branch1_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("another_file.txt"), "virtual").unwrap();
@ -145,7 +146,7 @@ async fn rebase_work() {
let mut branch1_id = {
// make a branch with some work
let branch1_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("another_file.txt"), "").unwrap();

View File

@ -1,3 +1,4 @@
use gitbutler_branch::BranchCreateRequest;
use gitbutler_reference::Refname;
use super::*;
@ -142,7 +143,7 @@ async fn delete_if_empty() {
.unwrap();
controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();

View File

@ -1,4 +1,4 @@
use gitbutler_branch::branch::Branch;
use gitbutler_branch::{Branch, BranchCreateRequest, BranchUpdateRequest};
use gitbutler_branch_actions::VirtualBranch;
use gitbutler_id::id::Id;
@ -19,7 +19,7 @@ async fn should_lock_updated_hunks() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -76,14 +76,14 @@ async fn should_reset_into_same_branch() {
.unwrap();
controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
let branch_2_id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
selected_for_changes: Some(true),
..Default::default()
},
@ -108,7 +108,7 @@ async fn should_reset_into_same_branch() {
controller
.update_virtual_branch(
project,
branch::BranchUpdateRequest {
BranchUpdateRequest {
id: branch_2_id,
selected_for_changes: Some(true),
..Default::default()

View File

@ -1,3 +1,4 @@
use gitbutler_branch::BranchCreateRequest;
use gitbutler_reference::LocalRefname;
use super::*;
@ -20,7 +21,7 @@ async fn integration() {
// make a remote branch
let branch_id = controller
.create_virtual_branch(project, &super::branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();

View File

@ -1,4 +1,5 @@
use super::*;
use gitbutler_branch::BranchCreateRequest;
#[tokio::test]
async fn should_unapply_diff() {
@ -53,7 +54,7 @@ async fn should_remove_reference() {
let id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},

View File

@ -1,4 +1,5 @@
use super::*;
use gitbutler_branch::BranchCreateRequest;
#[tokio::test]
async fn insert_blank_commit_down() {
@ -15,7 +16,7 @@ async fn insert_blank_commit_down() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -87,7 +88,7 @@ async fn insert_blank_commit_up() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();

View File

@ -1,14 +1,13 @@
use std::path::PathBuf;
use std::{fs, path, str::FromStr};
use gitbutler_branch::branch;
use gitbutler_branch::BranchCreateRequest;
use gitbutler_branch_actions::VirtualBranchActions;
use gitbutler_error::error::Marker;
use gitbutler_project::{self as projects, Project, ProjectId};
use gitbutler_reference::Refname;
use tempfile::TempDir;
use gitbutler_testsupport::{paths, TestProject, VAR_NO_CLEANUP};
use tempfile::TempDir;
struct Test {
repository: TestProject,
@ -111,7 +110,7 @@ async fn resolve_conflict_flow() {
{
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch1_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();

View File

@ -1,4 +1,5 @@
use gitbutler_branch::ownership::BranchOwnershipClaims;
use gitbutler_branch::BranchCreateRequest;
use gitbutler_branch::BranchOwnershipClaims;
use gitbutler_commit::commit_ext::CommitExt;
use super::*;
@ -18,7 +19,7 @@ async fn move_file_down() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -82,7 +83,7 @@ async fn move_file_up() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -141,7 +142,7 @@ async fn move_file_up_overlapping_hunks() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -180,7 +181,7 @@ async fn move_file_up_overlapping_hunks() {
.unwrap();
// move one line from middle commit two up to middle commit one
let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-6".parse().unwrap();
let to_amend: BranchOwnershipClaims = "file2.txt:1-6".parse().unwrap();
controller
.move_commit_file(project, branch_id, commit2_id, commit3_id, &to_amend)
.await

View File

@ -1,4 +1,4 @@
use gitbutler_branch::branch::{BranchCreateRequest, BranchId};
use gitbutler_branch::{BranchCreateRequest, BranchId};
use super::Test;

View File

@ -1,6 +1,6 @@
use super::*;
use gitbutler_branch::VirtualBranchesHandle;
use gitbutler_oplog::oplog::Oplog;
use gitbutler_branch::{BranchCreateRequest, VirtualBranchesHandle};
use gitbutler_oplog::OplogExt;
use itertools::Itertools;
use std::io::Write;
use std::path::Path;
@ -31,7 +31,7 @@ async fn workdir_vbranch_restore() -> anyhow::Result<()> {
let branch_id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
name: Some(round.to_string()),
..Default::default()
},
@ -113,7 +113,7 @@ async fn basic_oplog() -> anyhow::Result<()> {
.await?;
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await?;
// create commit
@ -146,7 +146,7 @@ async fn basic_oplog() -> anyhow::Result<()> {
// create state with conflict state
let _empty_branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await?;
std::fs::remove_file(&base_merge_parent_path)?;
@ -268,7 +268,7 @@ async fn restores_gitbutler_integration() -> anyhow::Result<()> {
0
);
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await?;
assert_eq!(
VirtualBranchesHandle::new(project.gb_dir())

View File

@ -1,9 +1,8 @@
use super::*;
mod create_virtual_branch {
use branch::BranchCreateRequest;
use super::*;
use gitbutler_branch::BranchCreateRequest;
#[tokio::test]
async fn simple() {
@ -20,7 +19,7 @@ mod create_virtual_branch {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -92,6 +91,7 @@ mod create_virtual_branch {
mod update_virtual_branch {
use super::*;
use gitbutler_branch::{BranchCreateRequest, BranchUpdateRequest};
#[tokio::test]
async fn simple() {
@ -110,7 +110,7 @@ mod update_virtual_branch {
let branch_id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
@ -121,7 +121,7 @@ mod update_virtual_branch {
controller
.update_virtual_branch(
project,
branch::BranchUpdateRequest {
BranchUpdateRequest {
id: branch_id,
name: Some("new name".to_string()),
..Default::default()
@ -161,7 +161,7 @@ mod update_virtual_branch {
let branch1_id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
@ -172,7 +172,7 @@ mod update_virtual_branch {
let branch2_id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
..Default::default()
},
)
@ -182,7 +182,7 @@ mod update_virtual_branch {
controller
.update_virtual_branch(
project,
branch::BranchUpdateRequest {
BranchUpdateRequest {
id: branch2_id,
name: Some("name".to_string()),
..Default::default()
@ -209,8 +209,8 @@ mod update_virtual_branch {
}
mod push_virtual_branch {
use super::*;
use gitbutler_branch::{BranchCreateRequest, BranchUpdateRequest};
#[tokio::test]
async fn simple() {
@ -229,7 +229,7 @@ mod push_virtual_branch {
let branch1_id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
@ -284,7 +284,7 @@ mod push_virtual_branch {
let branch1_id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
@ -307,7 +307,7 @@ mod push_virtual_branch {
controller
.update_virtual_branch(
project,
branch::BranchUpdateRequest {
BranchUpdateRequest {
id: branch1_id,
name: Some("updated name".to_string()),
..Default::default()
@ -321,7 +321,7 @@ mod push_virtual_branch {
let branch2_id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},

View File

@ -1,4 +1,5 @@
use super::*;
use gitbutler_branch::BranchCreateRequest;
#[tokio::test]
async fn reorder_commit_down() {
@ -15,7 +16,7 @@ async fn reorder_commit_down() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -76,7 +77,7 @@ async fn reorder_commit_up() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();

View File

@ -1,6 +1,6 @@
use std::fs;
use gitbutler_branch::branch::BranchCreateRequest;
use gitbutler_branch::BranchCreateRequest;
use super::Test;

View File

@ -1,4 +1,5 @@
use super::*;
use gitbutler_branch::{BranchCreateRequest, BranchUpdateRequest};
#[tokio::test]
async fn unapplying_selected_branch_selects_anther() {
@ -18,13 +19,13 @@ async fn unapplying_selected_branch_selects_anther() {
// first branch should be created as default
let b_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
// if default branch exists, new branch should not be created as default
let b2_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -65,13 +66,13 @@ async fn deleting_selected_branch_selects_anther() {
// first branch should be created as default
let b_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
// if default branch exists, new branch should not be created as default
let b2_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -111,7 +112,7 @@ async fn create_virtual_branch_should_set_selected_for_changes() {
// first branch should be created as default
let b_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
let branch = controller
@ -126,7 +127,7 @@ async fn create_virtual_branch_should_set_selected_for_changes() {
// if default branch exists, new branch should not be created as default
let b_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
let branch = controller
@ -143,7 +144,7 @@ async fn create_virtual_branch_should_set_selected_for_changes() {
let b_id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
selected_for_changes: Some(false),
..Default::default()
},
@ -164,7 +165,7 @@ async fn create_virtual_branch_should_set_selected_for_changes() {
let b_id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
selected_for_changes: Some(true),
..Default::default()
},
@ -196,7 +197,7 @@ async fn update_virtual_branch_should_reset_selected_for_changes() {
.unwrap();
let b1_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
let b1 = controller
@ -210,7 +211,7 @@ async fn update_virtual_branch_should_reset_selected_for_changes() {
assert!(b1.selected_for_changes);
let b2_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
let b2 = controller
@ -226,7 +227,7 @@ async fn update_virtual_branch_should_reset_selected_for_changes() {
controller
.update_virtual_branch(
project,
branch::BranchUpdateRequest {
BranchUpdateRequest {
id: b2_id,
selected_for_changes: Some(true),
..Default::default()
@ -271,7 +272,7 @@ async fn unapply_virtual_branch_should_reset_selected_for_changes() {
.unwrap();
let b1_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
@ -287,7 +288,7 @@ async fn unapply_virtual_branch_should_reset_selected_for_changes() {
assert!(b1.selected_for_changes);
let b2_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -337,7 +338,7 @@ async fn hunks_distribution() {
controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
selected_for_changes: Some(true),
..Default::default()
},
@ -423,7 +424,7 @@ async fn new_locked_hunk_without_modifying_existing() {
controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
selected_for_changes: Some(true),
..Default::default()
},

View File

@ -42,9 +42,9 @@ mod error {
}
mod go_back_to_integration {
use pretty_assertions::assert_eq;
use super::*;
use gitbutler_branch::BranchCreateRequest;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn should_preserve_applied_vbranches() {
@ -67,7 +67,7 @@ mod go_back_to_integration {
.unwrap();
let vbranch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();

View File

@ -1,4 +1,5 @@
use super::*;
use gitbutler_branch::{BranchCreateRequest, BranchUpdateRequest};
#[tokio::test]
async fn head() {
@ -15,7 +16,7 @@ async fn head() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -91,7 +92,7 @@ async fn middle() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -177,7 +178,7 @@ async fn forcepush_allowed() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -259,14 +260,14 @@ async fn forcepush_forbidden() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
controller
.update_virtual_branch(
project,
branch::BranchUpdateRequest {
BranchUpdateRequest {
id: branch_id,
allow_rebasing: Some(false),
..Default::default()
@ -337,7 +338,7 @@ async fn root_forbidden() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();

View File

@ -1,6 +1,6 @@
use std::fs;
use gitbutler_branch::{branch::BranchCreateRequest, ownership::BranchOwnershipClaims};
use gitbutler_branch::{BranchCreateRequest, BranchOwnershipClaims};
use super::Test;

View File

@ -1,4 +1,5 @@
use super::*;
use gitbutler_branch::BranchCreateRequest;
#[tokio::test]
async fn undo_commit_simple() {
@ -15,7 +16,7 @@ async fn undo_commit_simple() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();

View File

@ -1,8 +1,8 @@
use super::*;
mod applied_branch {
use super::*;
use gitbutler_branch::BranchCreateRequest;
#[tokio::test]
async fn conflicts_with_uncommitted_work() {
@ -31,7 +31,7 @@ mod applied_branch {
{
// make a branch that conflicts with the remote branch, but doesn't know about it yet
controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -97,7 +97,7 @@ mod applied_branch {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -168,7 +168,7 @@ mod applied_branch {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -244,7 +244,7 @@ mod applied_branch {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -317,7 +317,7 @@ mod applied_branch {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -364,6 +364,7 @@ mod applied_branch {
mod no_conflicts_pushed {
use super::*;
use gitbutler_branch::BranchUpdateRequest;
#[tokio::test]
async fn force_push_ok() {
@ -401,7 +402,7 @@ mod applied_branch {
let branch_id = {
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -467,7 +468,7 @@ mod applied_branch {
let branch_id = {
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -490,7 +491,7 @@ mod applied_branch {
controller
.update_virtual_branch(
project,
branch::BranchUpdateRequest {
BranchUpdateRequest {
id: branch_id,
allow_rebasing: Some(false),
..Default::default()
@ -547,7 +548,7 @@ mod applied_branch {
let branch_id = {
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -611,7 +612,7 @@ mod applied_branch {
let branch_id = {
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -706,7 +707,7 @@ mod applied_branch {
// branch has no conflict
let branch_id = {
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -810,7 +811,7 @@ mod applied_branch {
let branch_id = {
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -872,7 +873,7 @@ mod applied_branch {
let branch_id = {
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -944,7 +945,7 @@ mod applied_branch {
{
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -992,7 +993,7 @@ mod applied_branch {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -1050,7 +1051,7 @@ mod applied_branch {
.unwrap();
let branch_1_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -1063,7 +1064,7 @@ mod applied_branch {
let branch_2_id = controller
.create_virtual_branch(
project,
&branch::BranchCreateRequest {
&BranchCreateRequest {
selected_for_changes: Some(true),
..Default::default()
},

View File

@ -1,3 +1,4 @@
use gitbutler_branch::{BranchCreateRequest, BranchUpdateRequest};
use gitbutler_commit::commit_ext::CommitExt;
use super::*;
@ -17,7 +18,7 @@ async fn head() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -95,7 +96,7 @@ async fn middle() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -173,7 +174,7 @@ async fn forcepush_allowed() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -228,14 +229,14 @@ async fn forcepush_forbidden() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
controller
.update_virtual_branch(
project,
branch::BranchUpdateRequest {
BranchUpdateRequest {
id: branch_id,
allow_rebasing: Some(false),
..Default::default()
@ -282,7 +283,7 @@ async fn root() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -350,7 +351,7 @@ async fn empty() {
.unwrap();
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();

View File

@ -1,4 +1,5 @@
use super::*;
use gitbutler_branch::BranchCreateRequest;
#[tokio::test]
async fn detect_upstream_commits() {
@ -15,7 +16,7 @@ async fn detect_upstream_commits() {
.unwrap();
let branch1_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();
@ -82,7 +83,7 @@ async fn detect_integrated_commits() {
.unwrap();
let branch1_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.create_virtual_branch(project, &BranchCreateRequest::default())
.await
.unwrap();

View File

@ -25,21 +25,26 @@ pub fn dedup_fmt(existing: &[&str], new: &str, separator: &str) -> String {
)
}
#[test]
fn tests() {
for (existing, new, expected) in [
(vec!["bar", "baz"], "foo", "foo"),
(vec!["foo", "bar", "baz"], "foo", "foo 1"),
(vec!["foo", "foo 2"], "foo", "foo 3"),
(vec!["foo", "foo 1", "foo 2"], "foo", "foo 3"),
(vec!["foo", "foo 1", "foo 2"], "foo 1", "foo 1 1"),
(vec!["foo", "foo 1", "foo 2"], "foo 2", "foo 2 1"),
(vec!["foo", "foo 1", "foo 2"], "foo 3", "foo 3"),
(vec!["foo 2"], "foo", "foo 3"),
(vec!["foo", "foo 1", "foo 2", "foo 4"], "foo", "foo 5"),
(vec!["foo", "foo 0"], "foo", "foo 1"),
(vec!["foo 0"], "foo", "foo 1"),
] {
assert_eq!(dedup(&existing, new), expected.to_string());
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dedup() {
for (existing, new, expected) in [
(vec!["bar", "baz"], "foo", "foo"),
(vec!["foo", "bar", "baz"], "foo", "foo 1"),
(vec!["foo", "foo 2"], "foo", "foo 3"),
(vec!["foo", "foo 1", "foo 2"], "foo", "foo 3"),
(vec!["foo", "foo 1", "foo 2"], "foo 1", "foo 1 1"),
(vec!["foo", "foo 1", "foo 2"], "foo 2", "foo 2 1"),
(vec!["foo", "foo 1", "foo 2"], "foo 3", "foo 3"),
(vec!["foo 2"], "foo", "foo 3"),
(vec!["foo", "foo 1", "foo 2", "foo 4"], "foo", "foo 5"),
(vec!["foo", "foo 0"], "foo", "foo 1"),
(vec!["foo 0"], "foo", "foo 1"),
] {
assert_eq!(dedup(&existing, new), expected);
}
}
}

View File

@ -9,7 +9,7 @@ use tracing::instrument;
use gitbutler_id::id::Id;
use crate::branch::Branch;
use crate::Branch;
pub type DiffByPathMap = HashMap<PathBuf, FileDiff>;
@ -94,7 +94,7 @@ impl GitHunk {
/// Access
impl GitHunk {
pub fn contains(&self, line: u32) -> bool {
pub(crate) fn contains(&self, line: u32) -> bool {
self.new_start <= line && self.new_start + self.new_lines >= line
}

View File

@ -51,28 +51,10 @@ impl<'a> From<&'a OwnershipClaim> for (&'a Path, &'a [Hunk]) {
}
impl OwnershipClaim {
pub fn is_full(&self) -> bool {
pub(crate) fn is_full(&self) -> bool {
self.hunks.is_empty()
}
pub fn contains(&self, another: &OwnershipClaim) -> bool {
if !self.file_path.eq(&another.file_path) {
return false;
}
if self.hunks.is_empty() {
// full ownership contains any partial ownership
return true;
}
if another.hunks.is_empty() {
// partial ownership contains no full ownership
return false;
}
another.hunks.iter().all(|hunk| self.hunks.contains(hunk))
}
// return a copy of self, with another ranges added
pub fn plus(&self, another: OwnershipClaim) -> OwnershipClaim {
if self.file_path != another.file_path {

View File

@ -112,7 +112,7 @@ impl Hunk {
self
}
pub fn contains(&self, line: u32) -> bool {
pub(crate) fn contains(&self, line: u32) -> bool {
self.start <= line && self.end >= line
}
@ -123,10 +123,6 @@ impl Hunk {
|| another.contains(self.end)
}
pub fn shallow_eq(&self, other: &diff::GitHunk) -> bool {
self.start == other.new_start && self.end == other.new_start + other.new_lines
}
/// Produce a hash from `diff` as hex-string, which is **assumed to have a one-line diff header**!
/// `diff` can also be entirely empty, or not contain a diff header which is when it will just be hashed
/// with [`Self::hash()`].

View File

@ -1,12 +1,19 @@
pub mod branch;
pub mod branch_ext;
pub mod dedup;
mod branch;
pub use branch::{Branch, BranchCreateRequest, BranchId, BranchUpdateRequest};
mod branch_ext;
pub use branch_ext::BranchExt;
mod dedup;
pub use dedup::{dedup, dedup_fmt};
pub mod diff;
pub mod file_ownership;
pub mod hunk;
pub mod ownership;
mod file_ownership;
pub use file_ownership::OwnershipClaim;
mod hunk;
pub use hunk::{Hunk, HunkHash};
mod ownership;
pub use ownership::{reconcile_claims, BranchOwnershipClaims, ClaimOutcome};
pub mod serde;
pub mod target;
mod target;
pub use target::Target;
mod state;
pub use state::VirtualBranches as VirtualBranchesState;

View File

@ -4,7 +4,7 @@ use anyhow::Result;
use itertools::Itertools;
use serde::{Deserialize, Serialize, Serializer};
use crate::{branch::Branch, file_ownership::OwnershipClaim};
use crate::{file_ownership::OwnershipClaim, Branch};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BranchOwnershipClaims {
@ -49,37 +49,6 @@ impl FromStr for BranchOwnershipClaims {
}
impl BranchOwnershipClaims {
pub fn is_empty(&self) -> bool {
self.claims.is_empty()
}
pub fn contains(&self, another: &BranchOwnershipClaims) -> bool {
if another.is_empty() {
return true;
}
if self.is_empty() {
return false;
}
for file_ownership in &another.claims {
let mut found = false;
for self_file_ownership in &self.claims {
if self_file_ownership.file_path == file_ownership.file_path
&& self_file_ownership.contains(file_ownership)
{
found = true;
break;
}
}
if !found {
return false;
}
}
true
}
pub fn put(&mut self, ownership: OwnershipClaim) {
let target = self
.claims

View File

@ -10,7 +10,7 @@ use crate::{
target::Target,
};
use gitbutler_error::error::Code;
use gitbutler_fs::fs::read_toml_file_or_default;
use gitbutler_fs::read_toml_file_or_default;
// use gitbutler_project::Project;
use gitbutler_reference::Refname;
use itertools::Itertools;
@ -31,7 +31,7 @@ impl VirtualBranches {
/// Lists all virtual branches that are in the user's workspace.
///
/// Errors if the file cannot be read or written.
pub fn list_all_branches(&self) -> Result<Vec<Branch>> {
pub(crate) fn list_all_branches(&self) -> Result<Vec<Branch>> {
let branches: Vec<Branch> = self.branches.values().cloned().collect();
Ok(branches)
}
@ -88,8 +88,8 @@ impl VirtualBranchesHandle {
///
/// Errors if the file cannot be read or written.
pub fn get_default_target(&self) -> Result<Target> {
let virtual_branches = self.read_file();
virtual_branches?
let virtual_branches = self.read_file()?;
virtual_branches
.default_target
.ok_or(anyhow!("there is no default target").context(Code::DefaultTargetNotFound))
}
@ -202,13 +202,6 @@ impl VirtualBranchesHandle {
})
}
/// Checks if the state file exists.
///
/// This would only be false if the application just updated from a very old verion.
pub fn file_exists(&self) -> bool {
self.file_path.exists()
}
/// Reads and parses the state file.
///
/// If the file does not exist, it will be created.
@ -254,5 +247,5 @@ impl VirtualBranchesHandle {
}
fn write<P: AsRef<Path>>(file_path: P, virtual_branches: &VirtualBranches) -> Result<()> {
gitbutler_fs::fs::write(file_path, toml::to_string(&virtual_branches)?)
gitbutler_fs::write(file_path, toml::to_string(&virtual_branches)?)
}

View File

@ -1,4 +1,4 @@
use gitbutler_branch::file_ownership::OwnershipClaim;
use gitbutler_branch::OwnershipClaim;
#[test]
fn parse_ownership() {

View File

@ -1,4 +1,4 @@
use gitbutler_branch::hunk::Hunk;
use gitbutler_branch::Hunk;
#[test]
fn to_from_string() {

View File

@ -1,10 +1,7 @@
use std::{path::PathBuf, vec};
use gitbutler_branch::{
branch::{Branch, BranchId},
file_ownership::OwnershipClaim,
hunk::Hunk,
ownership::{reconcile_claims, BranchOwnershipClaims},
Hunk, OwnershipClaim, {reconcile_claims, BranchOwnershipClaims}, {Branch, BranchId},
};
#[test]

View File

@ -1,5 +1,5 @@
use anyhow::Result;
use gitbutler_oplog::oplog::Oplog;
use gitbutler_oplog::OplogExt;
use clap::{arg, Command};
use gitbutler_project::Project;
@ -63,6 +63,7 @@ fn list_snapshots(repo_dir: &str) -> Result<()> {
fn restore_snapshot(repo_dir: &str, snapshot_id: &str) -> Result<()> {
let project = project_from_path(repo_dir);
let _guard = project.try_exclusive_access()?;
project.restore_snapshot(snapshot_id.parse()?)?;
Ok(())
}

View File

@ -12,9 +12,5 @@ walkdir = "2.5.0"
sha2 = "0.10.8"
gitbutler-project.workspace = true
[[test]]
name="feedback"
path = "tests/mod.rs"
[dev-dependencies]
tempfile = "3.10"

View File

@ -1,52 +1,34 @@
use anyhow::Result;
use std::path;
use gitbutler_project as projects;
use gitbutler_project::ProjectId;
use std::path::PathBuf;
use crate::zipper::Zipper;
#[derive(Clone)]
pub struct Controller {
local_data_dir: path::PathBuf,
logs_dir: path::PathBuf,
zipper: Zipper,
#[allow(clippy::struct_field_names)]
projects_controller: projects::Controller,
pub struct Archival {
pub cache_dir: PathBuf,
pub logs_dir: PathBuf,
pub projects_controller: projects::Controller,
}
impl Controller {
pub fn new(
local_data_dir: path::PathBuf,
logs_dir: path::PathBuf,
zipper: Zipper,
projects_controller: projects::Controller,
) -> Self {
Self {
local_data_dir,
logs_dir,
zipper,
projects_controller,
}
}
pub fn archive(&self, project_id: ProjectId) -> Result<path::PathBuf> {
let project = self.projects_controller.get(project_id)?;
self.zipper.zip(project.path).map_err(Into::into)
}
pub fn data_archive(&self, project_id: ProjectId) -> Result<path::PathBuf> {
let project = self.projects_controller.get(project_id)?;
self.zipper
.zip(
self.local_data_dir
.join("projects")
.join(project.id.to_string()),
)
.map_err(Into::into)
}
pub fn logs_archive(&self) -> Result<path::PathBuf> {
self.zipper.zip(&self.logs_dir).map_err(Into::into)
impl Archival {
fn zipper(&self) -> Zipper {
Zipper::new(self.cache_dir.clone())
}
}
impl Archival {
pub fn archive(&self, project_id: ProjectId) -> Result<PathBuf> {
let project = self.projects_controller.get(project_id)?;
self.zipper().zip(project.path).map_err(Into::into)
}
pub fn data_archive(&self, project_id: ProjectId) -> Result<PathBuf> {
let dir_to_archive = self.projects_controller.project_metadata_dir(project_id);
self.zipper().zip(dir_to_archive).map_err(Into::into)
}
pub fn logs_archive(&self) -> Result<PathBuf> {
self.zipper().zip(&self.logs_dir).map_err(Into::into)
}
}

View File

@ -1,2 +1,3 @@
pub mod controller;
pub mod zipper;
mod controller;
pub use controller::Archival;
mod zipper;

View File

@ -159,3 +159,6 @@ fn file_hash<P: AsRef<path::Path>>(digest: &mut Sha256, path: P) -> Result<()> {
);
Ok(())
}
#[cfg(test)]
mod tests;

View File

@ -1,6 +1,6 @@
use std::{fs::File, io::Write};
use super::*;
use gitbutler_feedback::zipper::Zipper;
use std::{fs::File, io::Write};
use tempfile::tempdir;
use walkdir::WalkDir;

View File

@ -1,115 +0,0 @@
use std::fs::File;
use std::io::Read;
use std::{
io::Write,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use bstr::BString;
use gix::{
dir::walk::EmissionMode,
tempfile::{create_dir::Retries, AutoRemove, ContainingDirectory},
};
use serde::de::DeserializeOwned;
use walkdir::WalkDir;
// Returns an ordered list of relative paths for files inside a directory recursively.
pub fn list_files<P: AsRef<Path>>(dir_path: P, ignore_prefixes: &[P]) -> Result<Vec<PathBuf>> {
let mut files = vec![];
let dir_path = dir_path.as_ref();
if !dir_path.exists() {
return Ok(files);
}
for entry in WalkDir::new(dir_path) {
let entry = entry?;
if !entry.file_type().is_dir() {
let path = entry.path();
let path = path.strip_prefix(dir_path)?;
let path = path.to_path_buf();
if ignore_prefixes
.iter()
.any(|prefix| path.starts_with(prefix.as_ref()))
{
continue;
}
files.push(path);
}
}
files.sort();
Ok(files)
}
// Return an iterator of worktree-relative slash-separated paths for files inside the `worktree_dir`, recursively.
// Fails if the `worktree_dir` isn't a valid git repository.
pub fn iter_worktree_files(
worktree_dir: impl AsRef<Path>,
) -> Result<impl Iterator<Item = BString>> {
let repo = gix::open(worktree_dir.as_ref())?;
let index = repo.index_or_empty()?;
let disabled_interrupt_handling = Default::default();
let options = repo
.dirwalk_options()?
.emit_tracked(true)
.emit_untracked(EmissionMode::Matching);
Ok(repo
.dirwalk_iter(index, None::<&str>, disabled_interrupt_handling, options)?
.filter_map(Result::ok)
.map(|e| e.entry.rela_path))
}
/// Write a single file so that the write either fully succeeds, or fully fails,
/// assuming the containing directory already exists.
pub fn write<P: AsRef<Path>>(file_path: P, contents: impl AsRef<[u8]>) -> anyhow::Result<()> {
let mut temp_file = gix::tempfile::new(
file_path.as_ref().parent().unwrap(),
ContainingDirectory::Exists,
AutoRemove::Tempfile,
)?;
temp_file.write_all(contents.as_ref())?;
Ok(persist_tempfile(temp_file, file_path)?)
}
/// Write a single file so that the write either fully succeeds, or fully fails,
/// and create all leading directories.
pub fn create_dirs_then_write<P: AsRef<Path>>(
file_path: P,
contents: impl AsRef<[u8]>,
) -> std::io::Result<()> {
let mut temp_file = gix::tempfile::new(
file_path.as_ref().parent().unwrap(),
ContainingDirectory::CreateAllRaceProof(Retries::default()),
AutoRemove::Tempfile,
)?;
temp_file.write_all(contents.as_ref())?;
persist_tempfile(temp_file, file_path)
}
fn persist_tempfile(
tempfile: gix::tempfile::Handle<gix::tempfile::handle::Writable>,
to_path: impl AsRef<Path>,
) -> std::io::Result<()> {
match tempfile.persist(to_path) {
Ok(Some(_opened_file)) => Ok(()),
Ok(None) => unreachable!(
"BUG: a signal has caused the tempfile to be removed, but we didn't install a handler"
),
Err(err) => Err(err.error),
}
}
/// Reads and parses the state file.
///
/// If the file does not exist, it will be created.
pub fn read_toml_file_or_default<T: DeserializeOwned + Default>(path: &Path) -> Result<T> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(T::default()),
Err(err) => return Err(err.into()),
};
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let value: T =
toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display()))?;
Ok(value)
}

View File

@ -1 +1,115 @@
pub mod fs;
use std::fs::File;
use std::io::Read;
use std::{
io::Write,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use bstr::BString;
use gix::{
dir::walk::EmissionMode,
tempfile::{create_dir::Retries, AutoRemove, ContainingDirectory},
};
use serde::de::DeserializeOwned;
use walkdir::WalkDir;
// Returns an ordered list of relative paths for files inside a directory recursively.
pub fn list_files<P: AsRef<Path>>(dir_path: P, ignore_prefixes: &[P]) -> Result<Vec<PathBuf>> {
let mut files = vec![];
let dir_path = dir_path.as_ref();
if !dir_path.exists() {
return Ok(files);
}
for entry in WalkDir::new(dir_path) {
let entry = entry?;
if !entry.file_type().is_dir() {
let path = entry.path();
let path = path.strip_prefix(dir_path)?;
let path = path.to_path_buf();
if ignore_prefixes
.iter()
.any(|prefix| path.starts_with(prefix.as_ref()))
{
continue;
}
files.push(path);
}
}
files.sort();
Ok(files)
}
// Return an iterator of worktree-relative slash-separated paths for files inside the `worktree_dir`, recursively.
// Fails if the `worktree_dir` isn't a valid git repository.
pub fn iter_worktree_files(
worktree_dir: impl AsRef<Path>,
) -> Result<impl Iterator<Item = BString>> {
let repo = gix::open(worktree_dir.as_ref())?;
let index = repo.index_or_empty()?;
let disabled_interrupt_handling = Default::default();
let options = repo
.dirwalk_options()?
.emit_tracked(true)
.emit_untracked(EmissionMode::Matching);
Ok(repo
.dirwalk_iter(index, None::<&str>, disabled_interrupt_handling, options)?
.filter_map(Result::ok)
.map(|e| e.entry.rela_path))
}
/// Write a single file so that the write either fully succeeds, or fully fails,
/// assuming the containing directory already exists.
pub fn write<P: AsRef<Path>>(file_path: P, contents: impl AsRef<[u8]>) -> anyhow::Result<()> {
let mut temp_file = gix::tempfile::new(
file_path.as_ref().parent().unwrap(),
ContainingDirectory::Exists,
AutoRemove::Tempfile,
)?;
temp_file.write_all(contents.as_ref())?;
Ok(persist_tempfile(temp_file, file_path)?)
}
/// Write a single file so that the write either fully succeeds, or fully fails,
/// and create all leading directories.
pub fn create_dirs_then_write<P: AsRef<Path>>(
file_path: P,
contents: impl AsRef<[u8]>,
) -> std::io::Result<()> {
let mut temp_file = gix::tempfile::new(
file_path.as_ref().parent().unwrap(),
ContainingDirectory::CreateAllRaceProof(Retries::default()),
AutoRemove::Tempfile,
)?;
temp_file.write_all(contents.as_ref())?;
persist_tempfile(temp_file, file_path)
}
fn persist_tempfile(
tempfile: gix::tempfile::Handle<gix::tempfile::handle::Writable>,
to_path: impl AsRef<Path>,
) -> std::io::Result<()> {
match tempfile.persist(to_path) {
Ok(Some(_opened_file)) => Ok(()),
Ok(None) => unreachable!(
"BUG: a signal has caused the tempfile to be removed, but we didn't install a handler"
),
Err(err) => Err(err.error),
}
}
/// Reads and parses the state file.
///
/// If the file does not exist, it will be created.
pub fn read_toml_file_or_default<T: DeserializeOwned + Default>(path: &Path) -> Result<T> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(T::default()),
Err(err) => return Err(err.into()),
};
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let value: T =
toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display()))?;
Ok(value)
}

View File

@ -1,7 +1,9 @@
pub mod entry;
pub mod oplog;
mod oplog;
pub use oplog::OplogExt;
mod reflog;
pub mod snapshot;
mod snapshot;
pub use snapshot::SnapshotExt;
mod state;
/// The name of the file holding our state, useful for watching for changes.

View File

@ -1,8 +1,7 @@
use anyhow::{anyhow, bail, Context};
use git2::{DiffOptions, FileMode};
use gitbutler_branch::branch::Branch;
use gitbutler_branch::diff::{hunks_by_filepath, FileDiff};
use gitbutler_branch::{VirtualBranchesHandle, VirtualBranchesState};
use gitbutler_branch::{Branch, VirtualBranchesHandle, VirtualBranchesState};
use gitbutler_project::Project;
use gitbutler_repo::RepositoryExt;
use std::collections::HashMap;
@ -14,15 +13,15 @@ use std::{fs, path::PathBuf};
use anyhow::Result;
use tracing::instrument;
use gitbutler_branch::{
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL, GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
};
use super::{
entry::{OperationKind, Snapshot, SnapshotDetails, Trailer},
reflog::set_reference_to_oplog,
state::OplogHandle,
};
use gitbutler_branch::{
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL, GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
};
use gitbutler_project::access::{WorktreeReadPermission, WorktreeWritePermission};
const SNAPSHOT_FILE_LIMIT_BYTES: u64 = 32 * 1024 * 1024;
@ -44,11 +43,11 @@ const SNAPSHOT_FILE_LIMIT_BYTES: u64 = 32 * 1024 * 1024;
/// │ └── tree (subtree)
/// └── virtual_branches.toml
/// ```
pub trait Oplog {
pub trait OplogExt {
/// Prepares a snapshot of the current state of the working directory as well as GitButler data.
/// Returns a tree hash of the snapshot. The snapshot is not discoverable until it is committed with [`commit_snapshot`](Self::commit_snapshot())
/// If there are files that are untracked and larger than `SNAPSHOT_FILE_LIMIT_BYTES`, they are excluded from snapshot creation and restoring.
fn prepare_snapshot(&self) -> Result<git2::Oid>;
fn prepare_snapshot(&self, perm: &WorktreeReadPermission) -> Result<git2::Oid>;
/// Commits the snapshot tree that is created with the [`prepare_snapshot`](Self::prepare_snapshot) method,
/// which yielded the `snapshot_tree_id` for the entire snapshot state.
@ -63,6 +62,7 @@ pub trait Oplog {
&self,
snapshot_tree_id: git2::Oid,
details: SnapshotDetails,
perm: &mut WorktreeWritePermission,
) -> Result<Option<git2::Oid>>;
/// Creates a snapshot of the current state of the working directory as well as GitButler data.
@ -73,7 +73,11 @@ pub trait Oplog {
/// commit and the current one (after comparing trees).
///
/// Note that errors in snapshot creation is typically ignored, so we want to learn about them.
fn create_snapshot(&self, details: SnapshotDetails) -> Result<Option<git2::Oid>>;
fn create_snapshot(
&self,
details: SnapshotDetails,
perm: &mut WorktreeWritePermission,
) -> Result<Option<git2::Oid>>;
/// Lists the snapshots that have been created for the given repository, up to the given limit,
/// and with the most recent snapshot first, and at the end of the vec.
@ -129,167 +133,28 @@ pub trait Oplog {
fn oplog_head(&self) -> Result<Option<git2::Oid>>;
}
impl Oplog for Project {
fn prepare_snapshot(&self) -> Result<git2::Oid> {
let worktree_dir = self.path.as_path();
let repo = git2::Repository::open(worktree_dir)?;
let vb_state = VirtualBranchesHandle::new(self.gb_dir());
// grab the target commit
let default_target_commit = repo.find_commit(vb_state.get_default_target()?.sha)?;
let target_tree_id = default_target_commit.tree_id();
// Create a blob out of `.git/gitbutler/virtual_branches.toml`
let vb_path = repo.path().join("gitbutler").join("virtual_branches.toml");
let vb_content = fs::read(vb_path)?;
let vb_blob_id = repo.blob(&vb_content)?;
// Create a tree out of the conflicts state if present
let conflicts_tree_id = write_conflicts_tree(worktree_dir, &repo)?;
// write out the index as a tree to store
let mut index = repo.index()?;
let index_tree_oid = index.write_tree()?;
// start building our snapshot tree
let mut tree_builder = repo.treebuilder(None)?;
tree_builder.insert("index", index_tree_oid, FileMode::Tree.into())?;
tree_builder.insert("target_tree", target_tree_id, FileMode::Tree.into())?;
tree_builder.insert("conflicts", conflicts_tree_id, FileMode::Tree.into())?;
tree_builder.insert("virtual_branches.toml", vb_blob_id, FileMode::Blob.into())?;
// go through all virtual branches and create a subtree for each with the tree and any commits encoded
let mut branches_tree_builder = repo.treebuilder(None)?;
let mut head_tree_ids = Vec::new();
for branch in vb_state.list_branches_in_workspace()? {
head_tree_ids.push(branch.tree);
// commits in virtual branches (tree and commit data)
// calculate all the commits between branch.head and the target and codify them
let mut branch_tree_builder = repo.treebuilder(None)?;
branch_tree_builder.insert("tree", branch.tree, FileMode::Tree.into())?;
// let's get all the commits between the branch head and the target
let mut revwalk = repo.revwalk()?;
revwalk.push(branch.head)?;
revwalk.hide(default_target_commit.id())?;
let mut commits_tree_builder = repo.treebuilder(None)?;
for commit_id in revwalk {
let commit_id = commit_id?;
let commit = repo.find_commit(commit_id)?;
let commit_tree = commit.tree()?;
let mut commit_tree_builder = repo.treebuilder(None)?;
let commit_data_blob_id = repo.blob(&serialize_commit(&commit))?;
commit_tree_builder.insert("commit", commit_data_blob_id, FileMode::Blob.into())?;
commit_tree_builder.insert("tree", commit_tree.id(), FileMode::Tree.into())?;
let commit_tree_id = commit_tree_builder.write()?;
commits_tree_builder.insert(
commit_id.to_string(),
commit_tree_id,
FileMode::Tree.into(),
)?;
}
let commits_tree_id = commits_tree_builder.write()?;
branch_tree_builder.insert("commits", commits_tree_id, FileMode::Tree.into())?;
let branch_tree_id = branch_tree_builder.write()?;
branches_tree_builder.insert(
branch.id.to_string(),
branch_tree_id,
FileMode::Tree.into(),
)?;
}
// also add the gitbutler/integration commit to the branches tree
let head = repo.head()?;
if head.name() == Some("refs/heads/gitbutler/integration") {
let head_commit = head.peel_to_commit()?;
let head_tree = head_commit.tree()?;
let mut head_commit_tree_builder = repo.treebuilder(None)?;
// convert that data into a blob
let commit_data_blob = repo.blob(&serialize_commit(&head_commit))?;
head_commit_tree_builder.insert("commit", commit_data_blob, FileMode::Blob.into())?;
head_commit_tree_builder.insert("tree", head_tree.id(), FileMode::Tree.into())?;
let head_commit_tree_id = head_commit_tree_builder.write()?;
// have to make a subtree to match
let mut commits_tree_builder = repo.treebuilder(None)?;
commits_tree_builder.insert(
head_commit.id().to_string(),
head_commit_tree_id,
FileMode::Tree.into(),
)?;
let commits_tree_id = commits_tree_builder.write()?;
let mut branch_tree_builder = repo.treebuilder(None)?;
branch_tree_builder.insert("tree", head_tree.id(), FileMode::Tree.into())?;
branch_tree_builder.insert("commits", commits_tree_id, FileMode::Tree.into())?;
let branch_tree_id = branch_tree_builder.write()?;
branches_tree_builder.insert("integration", branch_tree_id, FileMode::Tree.into())?;
}
let branch_tree_id = branches_tree_builder.write()?;
tree_builder.insert("virtual_branches", branch_tree_id, FileMode::Tree.into())?;
let tree_id = tree_builder.write()?;
Ok(tree_id)
impl OplogExt for Project {
fn prepare_snapshot(&self, perm: &WorktreeReadPermission) -> Result<git2::Oid> {
prepare_snapshot(self, perm)
}
fn commit_snapshot(
&self,
snapshot_tree_id: git2::Oid,
details: SnapshotDetails,
perm: &mut WorktreeWritePermission,
) -> Result<Option<git2::Oid>> {
let repo = git2::Repository::open(self.path.as_path())?;
let snapshot_tree = repo.find_tree(snapshot_tree_id)?;
let oplog_state = OplogHandle::new(&self.gb_dir());
let oplog_head_commit = oplog_state
.oplog_head()?
.and_then(|head_id| repo.find_commit(head_id).ok());
// Construct a new commit
let signature = git2::Signature::now(
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL,
)
.unwrap();
let parents = oplog_head_commit
.as_ref()
.map(|head| vec![head])
.unwrap_or_default();
let snapshot_commit_id = repo.commit(
None,
&signature,
&signature,
&details.to_string(),
&snapshot_tree,
parents.as_slice(),
)?;
oplog_state.set_oplog_head(snapshot_commit_id)?;
let vb_state = VirtualBranchesHandle::new(self.gb_dir());
let target_commit_id = vb_state.get_default_target()?.sha;
set_reference_to_oplog(&self.path, target_commit_id, snapshot_commit_id)?;
Ok(Some(snapshot_commit_id))
commit_snapshot(self, snapshot_tree_id, details, perm)
}
#[instrument(skip(details), err(Debug))]
fn create_snapshot(&self, details: SnapshotDetails) -> Result<Option<git2::Oid>> {
let tree_id = self.prepare_snapshot()?;
self.commit_snapshot(tree_id, details)
#[instrument(skip(details, perm), err(Debug))]
fn create_snapshot(
&self,
details: SnapshotDetails,
perm: &mut WorktreeWritePermission,
) -> Result<Option<git2::Oid>> {
let tree_id = prepare_snapshot(self, perm.read_permission())?;
commit_snapshot(self, tree_id, details, perm)
}
fn list_snapshots(
@ -397,153 +262,8 @@ impl Oplog for Project {
}
fn restore_snapshot(&self, snapshot_commit_id: git2::Oid) -> Result<Option<git2::Oid>> {
let worktree_dir = self.path.as_path();
let repo = git2::Repository::open(worktree_dir)?;
let before_restore_snapshot_result = self.prepare_snapshot();
let snapshot_commit = repo.find_commit(snapshot_commit_id)?;
let snapshot_tree = snapshot_commit.tree()?;
let vb_toml_entry = snapshot_tree
.get_name("virtual_branches.toml")
.context("failed to get virtual_branches.toml blob")?;
// virtual_branches.toml blob
let vb_toml_blob = repo
.find_blob(vb_toml_entry.id())
.context("failed to convert virtual_branches tree entry to blob")?;
if let Err(err) = restore_conflicts_tree(&snapshot_tree, &repo) {
tracing::warn!("failed to restore conflicts tree - ignoring: {err}")
}
// make sure we reconstitute any commits that were in the snapshot that are not here for some reason
// for every entry in the virtual_branches subtree, reconsitute the commits
let vb_tree_entry = snapshot_tree
.get_name("virtual_branches")
.context("failed to get virtual_branches tree entry")?;
let vb_tree = repo
.find_tree(vb_tree_entry.id())
.context("failed to convert virtual_branches tree entry to tree")?;
// walk through all the entries (branches by id)
let walker = vb_tree.iter();
for branch_entry in walker {
let branch_tree = repo
.find_tree(branch_entry.id())
.context("failed to convert virtual_branches tree entry to tree")?;
let branch_name = branch_entry.name();
let commits_tree_entry = branch_tree
.get_name("commits")
.context("failed to get commits tree entry")?;
let commits_tree = repo
.find_tree(commits_tree_entry.id())
.context("failed to convert commits tree entry to tree")?;
// walk through all the commits in the branch
for commit_entry in commits_tree.iter() {
// for each commit, recreate the commit from the commit data if it doesn't exist
if let Some(commit_id) = commit_entry.name() {
// check for the oid in the repo
let commit_oid = git2::Oid::from_str(commit_id)?;
if repo.find_commit(commit_oid).is_err() {
// commit is not in the repo, let's build it from our data
let new_commit_oid = deserialize_commit(&repo, &commit_entry)?;
if new_commit_oid != commit_oid {
bail!("commit id mismatch: failed to recreate a commit from its parts");
}
}
// if branch_name is 'integration', we need to create or update the gitbutler/integration branch
if branch_name == Some("integration") {
// TODO(ST): with `gitoxide`, just update the branch without this dance,
// similar to `git update-ref`.
// Then a missing integration branch also doesn't have to be
// fatal, but we wouldn't want to `set_head()` if we are
// not already on the integration branch.
let mut integration_ref = repo.integration_ref_from_head()?;
// reset the branch if it's there, otherwise bail as we don't meddle with other branches
// need to detach the head for just a moment.
repo.set_head_detached(commit_oid)?;
integration_ref.delete()?;
// ok, now we set the branch to what it was and update HEAD
let integration_commit = repo.find_commit(commit_oid)?;
repo.branch("gitbutler/integration", &integration_commit, true)?;
// make sure head is gitbutler/integration
repo.set_head("refs/heads/gitbutler/integration")?;
}
}
}
}
repo.integration_ref_from_head().context(
"We will not change a worktree which for some reason isn't on the integration branch",
)?;
let workdir_tree_id = tree_from_applied_vbranches(&repo, snapshot_commit_id)?;
let workdir_tree = repo.find_tree(workdir_tree_id)?;
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
let files_to_exclude =
worktree_files_larger_than_limit_as_git2_ignore_rule(&repo, worktree_dir)?;
// In-memory, libgit2 internal ignore rule
repo.add_ignore_rule(&files_to_exclude)?;
// Define the checkout builder
let mut checkout_builder = git2::build::CheckoutBuilder::new();
checkout_builder.remove_untracked(true);
checkout_builder.force();
// Checkout the tree
repo.checkout_tree(workdir_tree.as_object(), Some(&mut checkout_builder))?;
// Update virtual_branches.toml with the state from the snapshot
fs::write(
repo.path().join("gitbutler").join("virtual_branches.toml"),
vb_toml_blob.content(),
)?;
// reset the repo index to our index tree
let index_tree_entry = snapshot_tree
.get_name("index")
.context("failed to get virtual_branches.toml blob")?;
let index_tree = repo
.find_tree(index_tree_entry.id())
.context("failed to convert index tree entry to tree")?;
let mut index = repo.index()?;
index.read_tree(&index_tree)?;
let restored_operation = snapshot_commit
.message()
.and_then(|msg| SnapshotDetails::from_str(msg).ok())
.map(|d| d.operation.to_string())
.unwrap_or_default();
// create new snapshot
let before_restore_snapshot_tree_id = before_restore_snapshot_result?;
let restored_date_ms = snapshot_commit.time().seconds() * 1000;
let details = SnapshotDetails {
version: Default::default(),
operation: OperationKind::RestoreFromSnapshot,
title: "Restored from snapshot".to_string(),
body: None,
trailers: vec![
Trailer {
key: "restored_from".to_string(),
value: snapshot_commit_id.to_string(),
},
Trailer {
key: "restored_operation".to_string(),
value: restored_operation,
},
Trailer {
key: "restored_date".to_string(),
value: restored_date_ms.to_string(),
},
],
};
self.commit_snapshot(before_restore_snapshot_tree_id, details)
let mut guard = self.exclusive_worktree_access();
restore_snapshot(self, snapshot_commit_id, guard.write_permission())
}
fn should_auto_snapshot(&self, check_if_last_snapshot_older_than: Duration) -> Result<bool> {
@ -597,6 +317,321 @@ impl Oplog for Project {
oplog_state.oplog_head()
}
}
fn prepare_snapshot(ctx: &Project, _shared_access: &WorktreeReadPermission) -> Result<git2::Oid> {
let worktree_dir = ctx.path.as_path();
let repo = git2::Repository::open(worktree_dir)?;
let vb_state = VirtualBranchesHandle::new(ctx.gb_dir());
// grab the target commit
let default_target_commit = repo.find_commit(vb_state.get_default_target()?.sha)?;
let target_tree_id = default_target_commit.tree_id();
// Create a blob out of `.git/gitbutler/virtual_branches.toml`
let vb_path = repo.path().join("gitbutler").join("virtual_branches.toml");
let vb_content = fs::read(vb_path)?;
let vb_blob_id = repo.blob(&vb_content)?;
// Create a tree out of the conflicts state if present
let conflicts_tree_id = write_conflicts_tree(worktree_dir, &repo)?;
// write out the index as a tree to store
let mut index = repo.index()?;
let index_tree_oid = index.write_tree()?;
// start building our snapshot tree
let mut tree_builder = repo.treebuilder(None)?;
tree_builder.insert("index", index_tree_oid, FileMode::Tree.into())?;
tree_builder.insert("target_tree", target_tree_id, FileMode::Tree.into())?;
tree_builder.insert("conflicts", conflicts_tree_id, FileMode::Tree.into())?;
tree_builder.insert("virtual_branches.toml", vb_blob_id, FileMode::Blob.into())?;
// go through all virtual branches and create a subtree for each with the tree and any commits encoded
let mut branches_tree_builder = repo.treebuilder(None)?;
let mut head_tree_ids = Vec::new();
for branch in vb_state.list_branches_in_workspace()? {
head_tree_ids.push(branch.tree);
// commits in virtual branches (tree and commit data)
// calculate all the commits between branch.head and the target and codify them
let mut branch_tree_builder = repo.treebuilder(None)?;
branch_tree_builder.insert("tree", branch.tree, FileMode::Tree.into())?;
// let's get all the commits between the branch head and the target
let mut revwalk = repo.revwalk()?;
revwalk.push(branch.head)?;
revwalk.hide(default_target_commit.id())?;
let mut commits_tree_builder = repo.treebuilder(None)?;
for commit_id in revwalk {
let commit_id = commit_id?;
let commit = repo.find_commit(commit_id)?;
let commit_tree = commit.tree()?;
let mut commit_tree_builder = repo.treebuilder(None)?;
let commit_data_blob_id = repo.blob(&serialize_commit(&commit))?;
commit_tree_builder.insert("commit", commit_data_blob_id, FileMode::Blob.into())?;
commit_tree_builder.insert("tree", commit_tree.id(), FileMode::Tree.into())?;
let commit_tree_id = commit_tree_builder.write()?;
commits_tree_builder.insert(
commit_id.to_string(),
commit_tree_id,
FileMode::Tree.into(),
)?;
}
let commits_tree_id = commits_tree_builder.write()?;
branch_tree_builder.insert("commits", commits_tree_id, FileMode::Tree.into())?;
let branch_tree_id = branch_tree_builder.write()?;
branches_tree_builder.insert(
branch.id.to_string(),
branch_tree_id,
FileMode::Tree.into(),
)?;
}
// also add the gitbutler/integration commit to the branches tree
let head = repo.head()?;
if head.name() == Some("refs/heads/gitbutler/integration") {
let head_commit = head.peel_to_commit()?;
let head_tree = head_commit.tree()?;
let mut head_commit_tree_builder = repo.treebuilder(None)?;
// convert that data into a blob
let commit_data_blob = repo.blob(&serialize_commit(&head_commit))?;
head_commit_tree_builder.insert("commit", commit_data_blob, FileMode::Blob.into())?;
head_commit_tree_builder.insert("tree", head_tree.id(), FileMode::Tree.into())?;
let head_commit_tree_id = head_commit_tree_builder.write()?;
// have to make a subtree to match
let mut commits_tree_builder = repo.treebuilder(None)?;
commits_tree_builder.insert(
head_commit.id().to_string(),
head_commit_tree_id,
FileMode::Tree.into(),
)?;
let commits_tree_id = commits_tree_builder.write()?;
let mut branch_tree_builder = repo.treebuilder(None)?;
branch_tree_builder.insert("tree", head_tree.id(), FileMode::Tree.into())?;
branch_tree_builder.insert("commits", commits_tree_id, FileMode::Tree.into())?;
let branch_tree_id = branch_tree_builder.write()?;
branches_tree_builder.insert("integration", branch_tree_id, FileMode::Tree.into())?;
}
let branch_tree_id = branches_tree_builder.write()?;
tree_builder.insert("virtual_branches", branch_tree_id, FileMode::Tree.into())?;
let tree_id = tree_builder.write()?;
Ok(tree_id)
}
fn commit_snapshot(
ctx: &Project,
snapshot_tree_id: git2::Oid,
details: SnapshotDetails,
_exclusive_access: &mut WorktreeWritePermission,
) -> Result<Option<git2::Oid>> {
let repo = git2::Repository::open(ctx.path.as_path())?;
let snapshot_tree = repo.find_tree(snapshot_tree_id)?;
let oplog_state = OplogHandle::new(&ctx.gb_dir());
let oplog_head_commit = oplog_state
.oplog_head()?
.and_then(|head_id| repo.find_commit(head_id).ok());
// Construct a new commit
let signature = git2::Signature::now(
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL,
)
.unwrap();
let parents = oplog_head_commit
.as_ref()
.map(|head| vec![head])
.unwrap_or_default();
let snapshot_commit_id = repo.commit(
None,
&signature,
&signature,
&details.to_string(),
&snapshot_tree,
parents.as_slice(),
)?;
oplog_state.set_oplog_head(snapshot_commit_id)?;
let vb_state = VirtualBranchesHandle::new(ctx.gb_dir());
let target_commit_id = vb_state.get_default_target()?.sha;
set_reference_to_oplog(&ctx.path, target_commit_id, snapshot_commit_id)?;
Ok(Some(snapshot_commit_id))
}
fn restore_snapshot(
ctx: &Project,
snapshot_commit_id: git2::Oid,
exclusive_access: &mut WorktreeWritePermission,
) -> Result<Option<git2::Oid>> {
let worktree_dir = ctx.path.as_path();
let repo = git2::Repository::open(worktree_dir)?;
let before_restore_snapshot_result = prepare_snapshot(ctx, exclusive_access.read_permission());
let snapshot_commit = repo.find_commit(snapshot_commit_id)?;
let snapshot_tree = snapshot_commit.tree()?;
let vb_toml_entry = snapshot_tree
.get_name("virtual_branches.toml")
.context("failed to get virtual_branches.toml blob")?;
// virtual_branches.toml blob
let vb_toml_blob = repo
.find_blob(vb_toml_entry.id())
.context("failed to convert virtual_branches tree entry to blob")?;
if let Err(err) = restore_conflicts_tree(&snapshot_tree, &repo) {
tracing::warn!("failed to restore conflicts tree - ignoring: {err}")
}
// make sure we reconstitute any commits that were in the snapshot that are not here for some reason
// for every entry in the virtual_branches subtree, reconsitute the commits
let vb_tree_entry = snapshot_tree
.get_name("virtual_branches")
.context("failed to get virtual_branches tree entry")?;
let vb_tree = repo
.find_tree(vb_tree_entry.id())
.context("failed to convert virtual_branches tree entry to tree")?;
// walk through all the entries (branches by id)
let walker = vb_tree.iter();
for branch_entry in walker {
let branch_tree = repo
.find_tree(branch_entry.id())
.context("failed to convert virtual_branches tree entry to tree")?;
let branch_name = branch_entry.name();
let commits_tree_entry = branch_tree
.get_name("commits")
.context("failed to get commits tree entry")?;
let commits_tree = repo
.find_tree(commits_tree_entry.id())
.context("failed to convert commits tree entry to tree")?;
// walk through all the commits in the branch
for commit_entry in commits_tree.iter() {
// for each commit, recreate the commit from the commit data if it doesn't exist
if let Some(commit_id) = commit_entry.name() {
// check for the oid in the repo
let commit_oid = git2::Oid::from_str(commit_id)?;
if repo.find_commit(commit_oid).is_err() {
// commit is not in the repo, let's build it from our data
let new_commit_oid = deserialize_commit(&repo, &commit_entry)?;
if new_commit_oid != commit_oid {
bail!("commit id mismatch: failed to recreate a commit from its parts");
}
}
// if branch_name is 'integration', we need to create or update the gitbutler/integration branch
if branch_name == Some("integration") {
// TODO(ST): with `gitoxide`, just update the branch without this dance,
// similar to `git update-ref`.
// Then a missing integration branch also doesn't have to be
// fatal, but we wouldn't want to `set_head()` if we are
// not already on the integration branch.
let mut integration_ref = repo.integration_ref_from_head()?;
// reset the branch if it's there, otherwise bail as we don't meddle with other branches
// need to detach the head for just a moment.
repo.set_head_detached(commit_oid)?;
integration_ref.delete()?;
// ok, now we set the branch to what it was and update HEAD
let integration_commit = repo.find_commit(commit_oid)?;
repo.branch("gitbutler/integration", &integration_commit, true)?;
// make sure head is gitbutler/integration
repo.set_head("refs/heads/gitbutler/integration")?;
}
}
}
}
repo.integration_ref_from_head().context(
"We will not change a worktree which for some reason isn't on the integration branch",
)?;
let workdir_tree_id = tree_from_applied_vbranches(&repo, snapshot_commit_id)?;
let workdir_tree = repo.find_tree(workdir_tree_id)?;
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
let files_to_exclude =
worktree_files_larger_than_limit_as_git2_ignore_rule(&repo, worktree_dir)?;
// In-memory, libgit2 internal ignore rule
repo.add_ignore_rule(&files_to_exclude)?;
// Define the checkout builder
let mut checkout_builder = git2::build::CheckoutBuilder::new();
checkout_builder.remove_untracked(true);
checkout_builder.force();
// Checkout the tree
repo.checkout_tree(workdir_tree.as_object(), Some(&mut checkout_builder))?;
// Update virtual_branches.toml with the state from the snapshot
fs::write(
repo.path().join("gitbutler").join("virtual_branches.toml"),
vb_toml_blob.content(),
)?;
// reset the repo index to our index tree
let index_tree_entry = snapshot_tree
.get_name("index")
.context("failed to get virtual_branches.toml blob")?;
let index_tree = repo
.find_tree(index_tree_entry.id())
.context("failed to convert index tree entry to tree")?;
let mut index = repo.index()?;
index.read_tree(&index_tree)?;
let restored_operation = snapshot_commit
.message()
.and_then(|msg| SnapshotDetails::from_str(msg).ok())
.map(|d| d.operation.to_string())
.unwrap_or_default();
// create new snapshot
let before_restore_snapshot_tree_id = before_restore_snapshot_result?;
let restored_date_ms = snapshot_commit.time().seconds() * 1000;
let details = SnapshotDetails {
version: Default::default(),
operation: OperationKind::RestoreFromSnapshot,
title: "Restored from snapshot".to_string(),
body: None,
trailers: vec![
Trailer {
key: "restored_from".to_string(),
value: snapshot_commit_id.to_string(),
},
Trailer {
key: "restored_operation".to_string(),
value: restored_operation,
},
Trailer {
key: "restored_date".to_string(),
value: restored_date_ms.to_string(),
},
],
};
commit_snapshot(
ctx,
before_restore_snapshot_tree_id,
details,
exclusive_access,
)
}
/// Restore the state of .git/base_merge_parent and .git/conflicts from the snapshot
/// Will remove those files if they are not present in the snapshot

View File

@ -2,7 +2,7 @@ use anyhow::{Context, Result};
use gitbutler_branch::{
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL, GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
};
use gitbutler_fs::fs::write;
use gitbutler_fs::write;
use gix::config::tree::Key;
use std::path::Path;

View File

@ -1,21 +1,22 @@
use crate::{
entry::{OperationKind, SnapshotDetails},
oplog::OplogExt,
};
use anyhow::Result;
use gitbutler_branch::branch::{Branch, BranchUpdateRequest};
use gitbutler_branch::{Branch, BranchUpdateRequest};
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_project::Project;
use gitbutler_reference::ReferenceName;
use std::vec;
use crate::{
entry::{OperationKind, SnapshotDetails},
oplog::Oplog,
};
use super::entry::Trailer;
pub trait Snapshot {
pub trait SnapshotExt {
fn snapshot_branch_unapplied(
&self,
snapshot_tree: git2::Oid,
result: Result<&ReferenceName, &anyhow::Error>,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()>;
fn snapshot_commit_undo(
@ -23,6 +24,7 @@ pub trait Snapshot {
snapshot_tree: git2::Oid,
result: Result<&(), &anyhow::Error>,
commit_sha: git2::Oid,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()>;
fn snapshot_commit_creation(
@ -31,30 +33,41 @@ pub trait Snapshot {
error: Option<&anyhow::Error>,
commit_message: String,
sha: Option<git2::Oid>,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()>;
fn snapshot_branch_creation(&self, branch_name: String) -> anyhow::Result<()>;
fn snapshot_branch_deletion(&self, branch_name: String) -> anyhow::Result<()>;
fn snapshot_branch_creation(
&self,
branch_name: String,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()>;
fn snapshot_branch_deletion(
&self,
branch_name: String,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()>;
fn snapshot_branch_update(
&self,
snapshot_tree: git2::Oid,
old_branch: &Branch,
update: &BranchUpdateRequest,
error: Option<&anyhow::Error>,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()>;
}
/// Snapshot functionality
impl Snapshot for Project {
impl SnapshotExt for Project {
fn snapshot_branch_unapplied(
&self,
snapshot_tree: git2::Oid,
result: Result<&ReferenceName, &anyhow::Error>,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()> {
let result = result.map(|s| Some(s.to_string()));
let details = SnapshotDetails::new(OperationKind::UnapplyBranch)
.with_trailers(result_trailer(result, "name".to_string()));
self.commit_snapshot(snapshot_tree, details)?;
self.commit_snapshot(snapshot_tree, details, perm)?;
Ok(())
}
fn snapshot_commit_undo(
@ -62,11 +75,12 @@ impl Snapshot for Project {
snapshot_tree: git2::Oid,
result: Result<&(), &anyhow::Error>,
commit_sha: git2::Oid,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()> {
let result = result.map(|_| Some(commit_sha.to_string()));
let details = SnapshotDetails::new(OperationKind::UndoCommit)
.with_trailers(result_trailer(result, "sha".to_string()));
self.commit_snapshot(snapshot_tree, details)?;
self.commit_snapshot(snapshot_tree, details, perm)?;
Ok(())
}
fn snapshot_commit_creation(
@ -75,6 +89,7 @@ impl Snapshot for Project {
error: Option<&anyhow::Error>,
commit_message: String,
sha: Option<git2::Oid>,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()> {
let details = SnapshotDetails::new(OperationKind::CreateCommit).with_trailers(
[
@ -92,26 +107,34 @@ impl Snapshot for Project {
]
.concat(),
);
self.commit_snapshot(snapshot_tree, details)?;
self.commit_snapshot(snapshot_tree, details, perm)?;
Ok(())
}
fn snapshot_branch_creation(&self, branch_name: String) -> anyhow::Result<()> {
fn snapshot_branch_creation(
&self,
branch_name: String,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()> {
let details =
SnapshotDetails::new(OperationKind::CreateBranch).with_trailers(vec![Trailer {
key: "name".to_string(),
value: branch_name,
}]);
self.create_snapshot(details)?;
self.create_snapshot(details, perm)?;
Ok(())
}
fn snapshot_branch_deletion(&self, branch_name: String) -> anyhow::Result<()> {
fn snapshot_branch_deletion(
&self,
branch_name: String,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()> {
let details =
SnapshotDetails::new(OperationKind::DeleteBranch).with_trailers(vec![Trailer {
key: "name".to_string(),
value: branch_name.to_string(),
}]);
self.create_snapshot(details)?;
self.create_snapshot(details, perm)?;
Ok(())
}
fn snapshot_branch_update(
@ -120,6 +143,7 @@ impl Snapshot for Project {
old_branch: &Branch,
update: &BranchUpdateRequest,
error: Option<&anyhow::Error>,
perm: &mut WorktreeWritePermission,
) -> anyhow::Result<()> {
let details = if update.ownership.is_some() {
SnapshotDetails::new(OperationKind::MoveHunk).with_trailers(
@ -212,7 +236,7 @@ impl Snapshot for Project {
} else {
SnapshotDetails::new(OperationKind::GenericBranchUpdate)
};
self.commit_snapshot(snapshot_tree, details)?;
self.commit_snapshot(snapshot_tree, details, perm)?;
Ok(())
}
}

View File

@ -4,7 +4,7 @@ use std::{
time::SystemTime,
};
use gitbutler_fs::fs::read_toml_file_or_default;
use gitbutler_fs::read_toml_file_or_default;
use serde::{Deserialize, Deserializer, Serialize};
use super::OPLOG_FILE_NAME;
@ -92,6 +92,6 @@ impl OplogHandle {
fn write_file(&self, mut oplog: Oplog) -> Result<()> {
oplog.modified_at = SystemTime::now();
gitbutler_fs::fs::write(&self.file_path, toml::to_string(&oplog)?)
gitbutler_fs::write(&self.file_path, toml::to_string(&oplog)?)
}
}

View File

@ -7,6 +7,7 @@ publish = false
[dependencies]
anyhow = "1.0.86"
parking_lot = { workspace = true, features = ["arc_lock"] }
serde = { workspace = true, features = ["std"]}
serde_json = { version = "1.0", features = [ "std", "arbitrary_precision" ] }
gitbutler-error.workspace = true
@ -19,6 +20,9 @@ uuid.workspace = true
tracing = "0.1.40"
resolve-path = "0.1.0"
# for locking
fslock.workspace = true
[[test]]
name="project"
path = "tests/mod.rs"

View File

@ -0,0 +1,104 @@
use crate::{Project, ProjectId};
use anyhow::{bail, Context};
use parking_lot::RawRwLock;
use std::collections::BTreeMap;
use std::sync::Arc;
/// Access Control
impl Project {
/// Try to obtain the exclusive inter-process lock on the entire project, preventing other GitButler
/// instances to operate on it entirely.
/// This lock should be obtained and held for as long as a user interface is observing the project.
///
/// Note that the lock is automatically released on `Drop`, or when the process quits for any reason,
/// so it can't go stale.
pub fn try_exclusive_access(&self) -> anyhow::Result<fslock::LockFile> {
// MIGRATION: bluntly remove old lock files, which are now more generally named to also fit
// the CLI.
std::fs::remove_file(self.gb_dir().join("window.lock").as_os_str()).ok();
let mut lock = fslock::LockFile::open(self.gb_dir().join("project.lock").as_os_str())?;
let got_lock = lock
.try_lock()
.context("Failed to check if lock is taken")?;
if !got_lock {
bail!(
"Project '{}' is already opened in another window",
self.title
);
}
Ok(lock)
}
/// Return a guard for exclusive (read+write) worktree access, blocking while waiting for someone else,
/// in the same process only, to release it, or for all readers to disappear.
/// Locking is fair.
///
/// Note that this in-process locking works only under the assumption that no two instances of
/// GitButler are able to read or write the same repository.
pub fn exclusive_worktree_access(&self) -> WriteWorkspaceGuard {
let mut map = WORKTREE_LOCKS.lock();
WriteWorkspaceGuard {
_inner: map.entry(self.id).or_default().write_arc(),
perm: WorktreeWritePermission(()),
}
}
/// Return a guard for shared (read) worktree access, and block while waiting for writers to disappear.
/// There can be multiple readers, but only a single writer. Waiting writers will be handled with priority,
/// thus block readers to prevent writer starvation.
pub fn shared_worktree_access(&self) -> WorkspaceReadGuard {
let mut map = WORKTREE_LOCKS.lock();
WorkspaceReadGuard(map.entry(self.id).or_default().read_arc())
}
}
pub struct WriteWorkspaceGuard {
_inner: parking_lot::ArcRwLockWriteGuard<RawRwLock, ()>,
perm: WorktreeWritePermission,
}
impl WriteWorkspaceGuard {
/// Signal that a write-permission is available - useful as API-marker to assure these
/// can only be called when the respective protection/permission is present.
pub fn write_permission(&mut self) -> &mut WorktreeWritePermission {
&mut self.perm
}
/// Signal that a read-permission is available - useful as API-marker to assure these
/// can only be called when the respective protection/permission is present.
pub fn read_permission(&self) -> &WorktreeReadPermission {
self.perm.read_permission()
}
}
pub struct WorkspaceReadGuard(#[allow(dead_code)] parking_lot::ArcRwLockReadGuard<RawRwLock, ()>);
impl WorkspaceReadGuard {
/// Signal that a read-permission is available - useful as API-marker to assure these
/// can only be called when the respective protection/permission is present.
pub fn read_permission(&self) -> &WorktreeReadPermission {
static READ: WorktreeReadPermission = WorktreeReadPermission(());
&READ
}
}
/// A token to indicate read-only access was granted to the worktree, assuring there are no writers
/// *within this process*.
pub struct WorktreeReadPermission(());
/// A token to indicate exclusive access was granted to the worktree, assuring there are no readers or other writers
/// *within this process*.
pub struct WorktreeWritePermission(());
impl WorktreeWritePermission {
/// Signal that a read-permission is available - useful as API-marker to assure these
/// can only be called when the respective protection/permission is present.
pub fn read_permission(&self) -> &WorktreeReadPermission {
static READ: WorktreeReadPermission = WorktreeReadPermission(());
&READ
}
}
static WORKTREE_LOCKS: parking_lot::Mutex<BTreeMap<ProjectId, Arc<parking_lot::RwLock<()>>>> =
parking_lot::Mutex::new(BTreeMap::new());

View File

@ -13,13 +13,6 @@ pub struct Controller {
}
impl Controller {
pub fn new(local_data_dir: PathBuf, projects_storage: storage::Storage) -> Self {
Self {
local_data_dir,
projects_storage,
}
}
pub fn from_path(path: impl Into<PathBuf>) -> Self {
let path = path.into();
Self {
@ -164,11 +157,7 @@ impl Controller {
.purge(project.id)
.map_err(anyhow::Error::from)?;
if let Err(error) = std::fs::remove_dir_all(
self.local_data_dir
.join("projects")
.join(project.id.to_string()),
) {
if let Err(error) = std::fs::remove_dir_all(self.project_metadata_dir(project.id)) {
tracing::error!(project_id = %id, ?error, "failed to remove project data",);
}
@ -184,4 +173,8 @@ impl Controller {
Ok(())
}
pub fn project_metadata_dir(&self, id: ProjectId) -> PathBuf {
self.local_data_dir.join("projects").join(id.to_string())
}
}

View File

@ -1,8 +1,9 @@
pub mod controller;
pub mod access;
mod controller;
mod default_true;
mod project;
pub mod storage;
mod storage;
pub use controller::*;
pub use controller::Controller;
pub use project::{ApiProject, AuthKey, CodePushState, FetchResult, Project, ProjectId};
pub use storage::UpdateRequest;

View File

@ -1,10 +1,9 @@
use serde::{Deserialize, Serialize};
use std::{
path::{self, PathBuf},
time,
};
use serde::{Deserialize, Serialize};
use crate::default_true::DefaultTrue;
use gitbutler_id::id::Id;

View File

@ -2,15 +2,13 @@ use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use gitbutler_storage::storage;
use crate::{ApiProject, AuthKey, CodePushState, FetchResult, Project, ProjectId};
const PROJECTS_FILE: &str = "projects.json";
#[derive(Debug, Clone)]
pub struct Storage {
inner: storage::Storage,
pub(crate) struct Storage {
inner: gitbutler_storage::Storage,
}
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
@ -31,12 +29,10 @@ pub struct UpdateRequest {
}
impl Storage {
pub fn new(storage: storage::Storage) -> Self {
Self { inner: storage }
}
pub fn from_path(path: impl Into<PathBuf>) -> Self {
Self::new(storage::Storage::new(path))
Storage {
inner: gitbutler_storage::Storage::new(path),
}
}
pub fn list(&self) -> Result<Vec<Project>> {

View File

@ -1,18 +1,17 @@
use std::{collections::HashMap, path::Path, sync::Arc};
use gitbutler_branch::branch::BranchId;
use gitbutler_branch::BranchId;
use gitbutler_id::id::Id;
use serde::Serialize;
use tokio::sync::{oneshot, Mutex};
use gitbutler_id::id::Id;
static mut GLOBAL_ASKPASS_BROKER: Option<AskpassBroker> = None;
/// Initialize the global askpass broker.
///
/// # Safety
/// This function **must** be called **at least once**, from only one thread at a time,
/// before any other function from this module is called. **Calls to [`get`] before [`init`] will panic.**
/// before any other function from this module is called. **Calls to [`get_broker`] before [`init`] will panic.**
///
/// This function is **NOT** thread safe.
pub unsafe fn init(submit_prompt: impl Fn(PromptEvent<Context>) + Send + Sync + 'static) {

View File

@ -1,7 +1,7 @@
pub mod rebase;
mod repository;
pub use repository::{LogUntil, RepoActions};
pub use repository::{LogUntil, RepoActionsExt};
mod commands;
pub use commands::RepoCommands;

View File

@ -4,7 +4,7 @@ use gitbutler_command_context::ProjectRepository;
use gitbutler_commit::{commit_ext::CommitExt, commit_headers::HasCommitHeaders};
use gitbutler_error::error::Marker;
use crate::{LogUntil, RepoActions, RepositoryExt};
use crate::{LogUntil, RepoActionsExt, RepositoryExt};
/// cherry-pick based rebase, which handles empty commits
/// this function takes a commit range and generates a Vector of commit oids

View File

@ -2,7 +2,7 @@ use std::str::FromStr;
use anyhow::{anyhow, Context, Result};
use gitbutler_branch::branch::{Branch, BranchId};
use gitbutler_branch::{Branch, BranchId};
use gitbutler_command_context::ProjectRepository;
use gitbutler_commit::commit_headers::CommitHeadersV2;
use gitbutler_error::error::Code;
@ -12,7 +12,7 @@ use crate::{askpass, ssh, Config};
use gitbutler_project::AuthKey;
use crate::{credentials::Helper, RepositoryExt};
pub trait RepoActions {
pub trait RepoActionsExt {
fn fetch(&self, remote_name: &str, credentials: &Helper, askpass: Option<String>)
-> Result<()>;
fn push(
@ -47,7 +47,7 @@ pub trait RepoActions {
) -> Result<()>;
}
impl RepoActions for ProjectRepository {
impl RepoActionsExt for ProjectRepository {
fn git_test_push(
&self,
credentials: &Helper,

View File

@ -1 +1,2 @@
pub mod storage;
mod storage;
pub use storage::Storage;

View File

@ -43,7 +43,7 @@ impl Storage {
/// Generally, the filesystem is used for synchronization, not in-memory primitives.
pub fn write(&self, rela_path: impl AsRef<Path>, content: &str) -> std::io::Result<()> {
let file_path = self.local_data_dir.join(rela_path);
gitbutler_fs::fs::create_dirs_then_write(file_path, content)
gitbutler_fs::create_dirs_then_write(file_path, content)
}
/// Delete the file or directory at `rela_path`.

View File

@ -3,12 +3,12 @@ use std::sync::Arc;
use std::time;
use anyhow::{anyhow, Context, Result};
use gitbutler_branch::target::Target;
use gitbutler_branch::Target;
use gitbutler_branch::VirtualBranchesHandle;
use gitbutler_command_context::ProjectRepository;
use gitbutler_error::error::Code;
use gitbutler_id::id::Id;
use gitbutler_oplog::oplog::Oplog;
use gitbutler_oplog::OplogExt;
use gitbutler_project as projects;
use gitbutler_project::{CodePushState, Project};
use gitbutler_reference::Refname;

View File

@ -26,7 +26,7 @@ anyhow = "1.0.86"
backtrace = { version = "0.3.72", optional = true }
console-subscriber = "0.2.0"
dirs = "5.0.1"
fslock = "0.2.1"
fslock.workspace = true
futures = "0.3"
git2.workspace = true
once_cell = "1.19"

View File

@ -1,24 +1,32 @@
use anyhow::{Context, Result};
use gitbutler_branch::branch::BranchId;
use gitbutler_branch::BranchId;
use gitbutler_branch_actions::conflicts;
use gitbutler_command_context::ProjectRepository;
use gitbutler_project as projects;
use gitbutler_project::ProjectId;
use gitbutler_reference::RemoteRefname;
use gitbutler_repo::{credentials::Helper, RepoActions, RepositoryExt};
use gitbutler_repo::{credentials, RepoActionsExt, RepositoryExt};
use std::path::PathBuf;
#[derive(Clone)]
pub struct App {
projects: projects::Controller,
pub app_data_dir: PathBuf,
}
/// Access to primary categories of data.
impl App {
pub fn projects(&self) -> projects::Controller {
projects::Controller::from_path(self.app_data_dir.clone())
}
pub fn users(&self) -> gitbutler_user::Controller {
gitbutler_user::Controller::from_path(&self.app_data_dir)
}
}
impl App {
pub fn new(projects: projects::Controller) -> Self {
Self { projects }
}
pub fn mark_resolved(&self, project_id: ProjectId, path: &str) -> Result<()> {
let project = self.projects.get(project_id)?;
let project = self.projects().get(project_id)?;
let project_repository = ProjectRepository::open(&project)?;
// mark file as resolved
conflicts::resolve(&project_repository, path)?;
@ -26,7 +34,7 @@ impl App {
}
pub fn git_remote_branches(&self, project_id: ProjectId) -> Result<Vec<RemoteRefname>> {
let project = self.projects.get(project_id)?;
let project = self.projects().get(project_id)?;
let project_repository = ProjectRepository::open(&project)?;
project_repository.repo().remote_branches()
}
@ -36,10 +44,10 @@ impl App {
project_id: ProjectId,
remote_name: &str,
branch_name: &str,
credentials: &Helper,
credentials: &credentials::Helper,
askpass: Option<Option<BranchId>>,
) -> Result<()> {
let project = self.projects.get(project_id)?;
let project = self.projects().get(project_id)?;
let project_repository = ProjectRepository::open(&project)?;
project_repository.git_test_push(credentials, remote_name, branch_name, askpass)
}
@ -48,16 +56,16 @@ impl App {
&self,
project_id: ProjectId,
remote_name: &str,
credentials: &Helper,
credentials: &credentials::Helper,
askpass: Option<String>,
) -> Result<()> {
let project = self.projects.get(project_id)?;
let project = self.projects().get(project_id)?;
let project_repository = ProjectRepository::open(&project)?;
project_repository.fetch(remote_name, credentials, askpass)
}
pub fn git_index_size(&self, project_id: ProjectId) -> Result<usize> {
let project = self.projects.get(project_id)?;
let project = self.projects().get(project_id)?;
let project_repository = ProjectRepository::open(&project)?;
let size = project_repository
.repo()
@ -68,7 +76,7 @@ impl App {
}
pub fn git_head(&self, project_id: ProjectId) -> Result<String> {
let project = self.projects.get(project_id)?;
let project = self.projects().get(project_id)?;
let project_repository = ProjectRepository::open(&project)?;
let head = project_repository
.repo()
@ -104,8 +112,9 @@ impl App {
}
pub async fn delete_all_data(&self) -> Result<()> {
for project in self.projects.list().context("failed to list projects")? {
self.projects
let controller = self.projects();
for project in controller.list().context("failed to list projects")? {
controller
.delete(project.id)
.await
.map_err(|err| err.context("failed to delete project"))?;

View File

@ -1,33 +1,29 @@
use crate::error::Error;
use crate::App;
use gitbutler_project::ProjectId;
use gitbutler_reference::RemoteRefname;
use gitbutler_repo::credentials::Helper;
use tauri::Manager;
use gitbutler_repo::credentials;
use tauri::State;
use tracing::instrument;
use crate::app;
use crate::error::Error;
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(app), err(Debug))]
pub async fn git_remote_branches(
handle: tauri::AppHandle,
app: State<'_, App>,
project_id: ProjectId,
) -> Result<Vec<RemoteRefname>, Error> {
let app = handle.state::<app::App>();
let branches = app.git_remote_branches(project_id)?;
Ok(branches)
Ok(app.git_remote_branches(project_id)?)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(app, helper), err(Debug))]
pub async fn git_test_push(
handle: tauri::AppHandle,
app: State<'_, App>,
helper: State<'_, credentials::Helper>,
project_id: ProjectId,
remote_name: &str,
branch_name: &str,
) -> Result<(), Error> {
let app = handle.state::<app::App>();
let helper = handle.state::<Helper>();
Ok(app.git_test_push(
project_id,
remote_name,
@ -39,15 +35,14 @@ pub async fn git_test_push(
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(app, helper), err(Debug))]
pub async fn git_test_fetch(
handle: tauri::AppHandle,
app: State<'_, App>,
helper: State<'_, credentials::Helper>,
project_id: ProjectId,
remote_name: &str,
action: Option<String>,
) -> Result<(), Error> {
let app = handle.state::<app::App>();
let helper = handle.state::<Helper>();
Ok(app.git_test_fetch(
project_id,
remote_name,
@ -57,66 +52,49 @@ pub async fn git_test_fetch(
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn git_index_size(
handle: tauri::AppHandle,
project_id: ProjectId,
) -> Result<usize, Error> {
let app = handle.state::<app::App>();
#[instrument(skip(app), err(Debug))]
pub async fn git_index_size(app: State<'_, App>, project_id: ProjectId) -> Result<usize, Error> {
Ok(app.git_index_size(project_id).expect("git index size"))
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn git_head(handle: tauri::AppHandle, project_id: ProjectId) -> Result<String, Error> {
let app = handle.state::<app::App>();
let head = app.git_head(project_id)?;
Ok(head)
#[instrument(skip(app), err(Debug))]
pub async fn git_head(app: State<'_, App>, project_id: ProjectId) -> Result<String, Error> {
Ok(app.git_head(project_id)?)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn delete_all_data(handle: tauri::AppHandle) -> Result<(), Error> {
let app = handle.state::<app::App>();
#[instrument(skip(app), err(Debug))]
pub async fn delete_all_data(app: State<'_, App>) -> Result<(), Error> {
app.delete_all_data().await?;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(app), err(Debug))]
pub async fn mark_resolved(
handle: tauri::AppHandle,
app: State<'_, App>,
project_id: ProjectId,
path: &str,
) -> Result<(), Error> {
let app = handle.state::<app::App>();
app.mark_resolved(project_id, path)?;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(_handle), err(Debug))]
pub async fn git_set_global_config(
_handle: tauri::AppHandle,
key: &str,
value: &str,
) -> Result<String, Error> {
let result = app::App::git_set_global_config(key, value)?;
Ok(result)
#[instrument(err(Debug))]
pub async fn git_set_global_config(key: &str, value: &str) -> Result<String, Error> {
Ok(App::git_set_global_config(key, value)?)
}
#[tauri::command(async)]
#[instrument(err(Debug))]
pub async fn git_remove_global_config(key: &str) -> Result<(), Error> {
Ok(app::App::git_remove_global_config(key)?)
Ok(App::git_remove_global_config(key)?)
}
#[tauri::command(async)]
#[instrument(skip(_handle), err(Debug))]
pub async fn git_get_global_config(
_handle: tauri::AppHandle,
key: &str,
) -> Result<Option<String>, Error> {
let result = app::App::git_get_global_config(key)?;
Ok(result)
#[instrument(err(Debug))]
pub async fn git_get_global_config(key: &str) -> Result<Option<String>, Error> {
Ok(App::git_get_global_config(key)?)
}

View File

@ -2,31 +2,26 @@ use crate::error::Error;
use gitbutler_config::{api::ProjectCommands, git::GbConfig};
use gitbutler_project as projects;
use gitbutler_project::ProjectId;
use tauri::Manager;
use tauri::State;
use tracing::instrument;
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn get_gb_config(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
) -> Result<GbConfig, Error> {
handle
.state::<projects::Controller>()
.get(project_id)?
.gb_config()
.map_err(Into::into)
projects.get(project_id)?.gb_config().map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn set_gb_config(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
config: GbConfig,
) -> Result<(), Error> {
handle
.state::<projects::Controller>()
projects
.get(project_id)?
.set_gb_config(config)
.map_err(Into::into)

View File

@ -2,19 +2,16 @@
//!
//! ## How to use this
//!
//! Just make sure this [`Error`] type is used for each provided `tauri` command. The rest happens automatically
//! such that:
//! Just make sure this `Error` type is used for each provided `tauri` command. The rest happens automatically
//! such that [context](gitbutler_error::error::Context) is handled correctly.
//!
//! * The frontend shows the root error as string by default…
//! * …or it shows the provided [`Context`](gitbutler_core::error::Context) as controlled by the `core` crate.
//!
//! ### Interfacing with `tauri` using [`Error`]
//! ### Interfacing with `tauri` using `Error`
//!
//! `tauri` serializes backend errors and makes these available as JSON objects to the frontend. The format
//! is an implementation detail, but here it's implemented to turn each [`Error`] into a dict with `code`
//! is an implementation detail, but here it's implemented to turn each `Error` into a dict with `code`
//! and `messsage` fields.
//!
//! The values in these fields are controlled by attaching context, please [see the `core` docs](gitbutler_core::error))
//! The values in these fields are controlled by attaching context, please [see the `error` docs](gitbutler_error::error))
//! on how to do this.
pub(crate) use frontend::Error;

View File

@ -13,7 +13,9 @@
clippy::too_many_lines
)]
pub mod app;
mod app;
pub use app::App;
pub mod commands;
pub mod logs;

View File

@ -13,11 +13,10 @@
clippy::too_many_lines
)]
use gitbutler_repo::credentials::Helper;
use gitbutler_storage::storage;
use gitbutler_repo::credentials;
use gitbutler_tauri::{
app, askpass, commands, config, github, logs, menu, projects, remotes, repo, secret, undo,
users, virtual_branches, zip, WindowState,
askpass, commands, config, github, logs, menu, projects, remotes, repo, secret, undo, users,
virtual_branches, zip, App, WindowState,
};
use tauri::{generate_context, Manager};
use tauri_plugin_log::LogTarget;
@ -42,13 +41,18 @@ fn main() {
tauri::Builder::default()
.setup(move |tauri_app| {
let window =
gitbutler_tauri::window::create(&tauri_app.handle(), "main", "index.html".into()).expect("Failed to create window");
let window = gitbutler_tauri::window::create(
&tauri_app.handle(),
"main",
"index.html".into(),
)
.expect("Failed to create window");
#[cfg(debug_assertions)]
window.open_devtools();
tokio::task::spawn(async move {
let mut six_hours = tokio::time::interval(tokio::time::Duration::new(6 * 60 * 60, 0));
let mut six_hours =
tokio::time::interval(tokio::time::Duration::new(6 * 60 * 60, 0));
loop {
six_hours.tick().await;
_ = window.emit_and_trigger("tauri://update", ());
@ -73,52 +77,41 @@ fn main() {
gitbutler_repo::askpass::init({
let handle = app_handle.clone();
move |event| {
handle.emit_all("git_prompt", event).expect("tauri event emission doesn't fail in practice")
handle
.emit_all("git_prompt", event)
.expect("tauri event emission doesn't fail in practice")
}
});
}
let app_data_dir = app_handle.path_resolver().app_data_dir().expect("missing app data dir");
let app_cache_dir = app_handle.path_resolver().app_cache_dir().expect("missing app cache dir");
let app_log_dir = app_handle.path_resolver().app_log_dir().expect("missing app log dir");
let (app_data_dir, app_cache_dir, app_log_dir) = {
let paths = app_handle.path_resolver();
(
paths.app_data_dir().expect("missing app data dir"),
paths.app_cache_dir().expect("missing app cache dir"),
paths.app_log_dir().expect("missing app log dir"),
)
};
std::fs::create_dir_all(&app_data_dir).expect("failed to create app data dir");
std::fs::create_dir_all(&app_cache_dir).expect("failed to create cache dir");
tracing::info!(version = %app_handle.package_info().version, name = %app_handle.package_info().name, "starting app");
let storage_controller = storage::Storage::new(&app_data_dir);
app_handle.manage(storage_controller.clone());
tracing::info!(version = %app_handle.package_info().version,
name = %app_handle.package_info().name, "starting app");
app_handle.manage(WindowState::new(app_handle.clone()));
let projects_storage_controller = gitbutler_project::storage::Storage::new(storage_controller.clone());
app_handle.manage(projects_storage_controller.clone());
let users_storage_controller = gitbutler_user::storage::Storage::new(storage_controller.clone());
app_handle.manage(users_storage_controller.clone());
let users_controller = gitbutler_user::Controller::new(users_storage_controller.clone());
app_handle.manage(users_controller.clone());
let projects_controller = gitbutler_project::Controller::new(
app_data_dir.clone(),
projects_storage_controller.clone()
);
app_handle.manage(projects_controller.clone());
let zipper = gitbutler_feedback::zipper::Zipper::new(&app_cache_dir);
app_handle.manage(zipper.clone());
app_handle.manage(gitbutler_feedback::controller::Controller::new(app_data_dir.clone(), app_log_dir.clone(), zipper.clone(), projects_controller.clone()));
let git_credentials_controller = Helper::default();
app_handle.manage(git_credentials_controller.clone());
let app = app::App::new(
projects_controller,
);
let app = App {
app_data_dir: app_data_dir.clone(),
};
app_handle.manage(app.users());
app_handle.manage(app.projects());
app_handle.manage(gitbutler_feedback::Archival {
cache_dir: app_cache_dir,
logs_dir: app_log_dir,
projects_controller: app.projects(),
});
app_handle.manage(credentials::Helper::default());
app_handle.manage(app);
Ok(())
@ -199,28 +192,33 @@ fn main() {
remotes::add_remote
])
.menu(menu::build(tauri_context.package_info()))
.on_menu_event(|event|menu::handle_event(&event))
.on_menu_event(|event| menu::handle_event(&event))
.on_window_event(|event| {
let window = event.window();
match event.event() {
#[cfg(target_os = "macos")]
tauri::WindowEvent::CloseRequested { api, .. } => {
if window.app_handle().windows().len() == 1 {
tracing::debug!("Hiding all application windows and preventing exit");
tracing::debug!(
"Hiding all application windows and preventing exit"
);
window.app_handle().hide().ok();
api.prevent_close();
}
}
tauri::WindowEvent::Destroyed => {
window.app_handle()
tauri::WindowEvent::Destroyed => {
window
.app_handle()
.state::<WindowState>()
.remove(window.label());
}
tauri::WindowEvent::Focused(focused) if *focused => {
window.app_handle()
window
.app_handle()
.state::<WindowState>()
.flush(window.label()).ok();
},
.flush(window.label())
.ok();
}
_ => {}
}
})

View File

@ -14,40 +14,40 @@ pub mod commands {
use crate::{window, WindowState};
#[tauri::command(async)]
#[instrument(skip(controller), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn update_project(
controller: State<'_, Controller>,
projects: State<'_, Controller>,
project: projects::UpdateRequest,
) -> Result<projects::Project, Error> {
Ok(controller.update(&project).await?)
Ok(projects.update(&project).await?)
}
#[tauri::command(async)]
#[instrument(skip(controller), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn add_project(
controller: State<'_, Controller>,
projects: State<'_, Controller>,
path: &path::Path,
) -> Result<projects::Project, Error> {
Ok(controller.add(path)?)
Ok(projects.add(path)?)
}
#[tauri::command(async)]
#[instrument(skip(controller), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn get_project(
controller: State<'_, Controller>,
projects: State<'_, Controller>,
id: ProjectId,
) -> Result<projects::Project, Error> {
Ok(controller.get(id)?)
Ok(projects.get(id)?)
}
#[tauri::command(async)]
#[instrument(skip(controller, window_state), err(Debug))]
#[instrument(skip(projects, window_state), err(Debug))]
pub async fn list_projects(
window_state: State<'_, WindowState>,
controller: State<'_, Controller>,
projects: State<'_, Controller>,
) -> Result<Vec<ProjectForFrontend>, Error> {
let open_projects = window_state.open_projects();
controller.list().map_err(Into::into).map(|projects| {
projects.list().map_err(Into::into).map(|projects| {
projects
.into_iter()
.map(|project| ProjectForFrontend {
@ -62,14 +62,14 @@ pub mod commands {
///
/// We use it to start watching for filesystem events.
#[tauri::command(async)]
#[instrument(skip(controller, window_state, window), err(Debug))]
#[instrument(skip(projects, window_state, window), err(Debug))]
pub async fn set_project_active(
controller: State<'_, Controller>,
projects: State<'_, Controller>,
window_state: State<'_, WindowState>,
window: Window,
id: ProjectId,
) -> Result<(), Error> {
let project = controller.get(id).context("project not found")?;
let project = projects.get(id).context("project not found")?;
Ok(window_state.set_project_to_window(window.label(), &project)?)
}
@ -93,12 +93,12 @@ pub mod commands {
}
#[tauri::command(async)]
#[instrument(skip(controller), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn delete_project(
controller: State<'_, Controller>,
projects: State<'_, Controller>,
id: ProjectId,
) -> Result<(), Error> {
controller.delete(id).await.map_err(Into::into)
projects.delete(id).await.map_err(Into::into)
}
}

View File

@ -2,27 +2,27 @@ use crate::error::Error;
use gitbutler_project as projects;
use gitbutler_project::ProjectId;
use gitbutler_repo::RepoCommands;
use tauri::Manager;
use tauri::State;
use tracing::instrument;
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn list_remotes(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
) -> Result<Vec<String>, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
project.remotes().map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn add_remote(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
name: &str,
url: &str,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
project.add_remote(name, url).map_err(Into::into)
}

View File

@ -3,39 +3,39 @@ pub mod commands {
use gitbutler_project as projects;
use gitbutler_project::ProjectId;
use gitbutler_repo::RepoCommands;
use tauri::Manager;
use tauri::State;
use tracing::instrument;
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn git_get_local_config(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
id: ProjectId,
key: &str,
) -> Result<Option<String>, Error> {
let project = handle.state::<projects::Controller>().get(id)?;
let project = projects.get(id)?;
Ok(project.get_local_config(key)?)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn git_set_local_config(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
id: ProjectId,
key: &str,
value: &str,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(id)?;
let project = projects.get(id)?;
project.set_local_config(key, value).map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn check_signing_settings(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
id: ProjectId,
) -> Result<bool, Error> {
let project = handle.state::<projects::Controller>().get(id)?;
let project = projects.get(id)?;
project.check_signing_settings().map_err(Into::into)
}
}

View File

@ -2,26 +2,23 @@ use crate::error::Error;
use anyhow::Context;
use gitbutler_branch::diff::FileDiff;
use gitbutler_oplog::entry::Snapshot;
use gitbutler_oplog::oplog::Oplog;
use gitbutler_oplog::OplogExt;
use gitbutler_project as projects;
use gitbutler_project::ProjectId;
use std::collections::HashMap;
use std::path::PathBuf;
use tauri::Manager;
use tauri::State;
use tracing::instrument;
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn list_snapshots(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
limit: usize,
sha: Option<String>,
) -> Result<Vec<Snapshot>, Error> {
let project = handle
.state::<projects::Controller>()
.get(project_id)
.context("failed to get project")?;
let project = projects.get(project_id).context("failed to get project")?;
let snapshots = project.list_snapshots(
limit,
sha.map(|hex| hex.parse().map_err(anyhow::Error::from))
@ -31,31 +28,26 @@ pub async fn list_snapshots(
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn restore_snapshot(
projects: State<'_, projects::Controller>,
handle: tauri::AppHandle,
project_id: ProjectId,
sha: String,
) -> Result<(), Error> {
let project = handle
.state::<projects::Controller>()
.get(project_id)
.context("failed to get project")?;
let project = projects.get(project_id).context("failed to get project")?;
project.restore_snapshot(sha.parse().map_err(anyhow::Error::from)?)?;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn snapshot_diff(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
sha: String,
) -> Result<HashMap<PathBuf, FileDiff>, Error> {
let project = handle
.state::<projects::Controller>()
.get(project_id)
.context("failed to get project")?;
let project = projects.get(project_id).context("failed to get project")?;
let diff = project.snapshot_diff(sha.parse().map_err(anyhow::Error::from)?)?;
Ok(diff)
}

View File

@ -1,20 +1,18 @@
pub mod commands {
use gitbutler_user::{controller::Controller, User};
use gitbutler_user::{Controller, User};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager};
use tauri::State;
use tracing::instrument;
use crate::error::Error;
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn get_user(handle: AppHandle) -> Result<Option<UserWithSecrets>, Error> {
let app = handle.state::<Controller>();
match app.get_user()? {
#[instrument(skip(login), err(Debug))]
pub async fn get_user(login: State<'_, Controller>) -> Result<Option<UserWithSecrets>, Error> {
match login.get_user()? {
Some(user) => {
if let Err(err) = user.access_token() {
app.delete_user()?;
login.delete_user()?;
return Err(err.context("Please login to GitButler again").into());
}
Ok(Some(user.try_into()?))
@ -24,21 +22,16 @@ pub mod commands {
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn set_user(handle: AppHandle, user: User) -> Result<User, Error> {
let app = handle.state::<Controller>();
app.set_user(&user)?;
#[instrument(skip(login), err(Debug))]
pub async fn set_user(login: State<'_, Controller>, user: User) -> Result<User, Error> {
login.set_user(&user)?;
Ok(user)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn delete_user(handle: AppHandle) -> Result<(), Error> {
let app = handle.state::<Controller>();
app.delete_user()?;
#[instrument(skip(login), err(Debug))]
pub async fn delete_user(login: State<'_, Controller>) -> Result<(), Error> {
login.delete_user()?;
Ok(())
}

View File

@ -1,113 +1,114 @@
pub mod commands {
use crate::error::Error;
use anyhow::{anyhow, Context};
use gitbutler_branch::branch::{BranchCreateRequest, BranchId, BranchUpdateRequest};
use gitbutler_branch::ownership::BranchOwnershipClaims;
use gitbutler_branch_actions::base::BaseBranch;
use gitbutler_branch_actions::files::RemoteBranchFile;
use gitbutler_branch_actions::remote::{RemoteBranch, RemoteBranchData};
use gitbutler_branch_actions::{NameConflitResolution, VirtualBranchActions, VirtualBranches};
use gitbutler_branch::BranchOwnershipClaims;
use gitbutler_branch::{BranchCreateRequest, BranchId, BranchUpdateRequest};
use gitbutler_branch_actions::BaseBranch;
use gitbutler_branch_actions::RemoteBranchFile;
use gitbutler_branch_actions::{NameConflictResolution, VirtualBranchActions, VirtualBranches};
use gitbutler_branch_actions::{RemoteBranch, RemoteBranchData};
use gitbutler_error::error::Code;
use gitbutler_project as projects;
use gitbutler_project::ProjectId;
use gitbutler_reference::ReferenceName;
use gitbutler_reference::{Refname, RemoteRefname};
use tauri::{AppHandle, Manager};
use tauri::State;
use tracing::instrument;
use crate::WindowState;
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn commit_virtual_branch(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch: BranchId,
message: &str,
ownership: Option<BranchOwnershipClaims>,
run_hooks: bool,
) -> Result<String, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let oid = VirtualBranchActions::default()
let project = projects.get(project_id)?;
let oid = VirtualBranchActions
.create_commit(&project, branch, message, ownership.as_ref(), run_hooks)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(oid.to_string())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn list_virtual_branches(
handle: AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
) -> Result<VirtualBranches, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let (branches, skipped_files) = VirtualBranchActions::default()
let project = projects.get(project_id)?;
VirtualBranchActions
.list_virtual_branches(&project)
.await?;
Ok(VirtualBranches {
branches,
skipped_files,
})
.await
.map_err(Into::into)
.map(|(branches, skipped_files)| VirtualBranches {
branches,
skipped_files,
})
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn create_virtual_branch(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch: BranchCreateRequest,
) -> Result<BranchId, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let branch_id = VirtualBranchActions::default()
let project = projects.get(project_id)?;
let branch_id = VirtualBranchActions
.create_virtual_branch(&project, &branch)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(branch_id)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn create_virtual_branch_from_branch(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch: Refname,
) -> Result<BranchId, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let branch_id = VirtualBranchActions::default()
let project = projects.get(project_id)?;
let branch_id = VirtualBranchActions
.create_virtual_branch_from_branch(&project, &branch)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(branch_id)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn integrate_upstream_commits(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch: BranchId,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
VirtualBranchActions::default()
let project = projects.get(project_id)?;
VirtualBranchActions
.integrate_upstream_commits(&project, branch)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn get_base_branch_data(
handle: AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
) -> Result<Option<BaseBranch>, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
if let Ok(base_branch) = VirtualBranchActions::default()
.get_base_branch_data(&project)
.await
{
let project = projects.get(project_id)?;
if let Ok(base_branch) = VirtualBranchActions::get_base_branch_data(&project).await {
Ok(Some(base_branch))
} else {
Ok(None)
@ -115,221 +116,228 @@ pub mod commands {
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn set_base_branch(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch: &str,
push_remote: Option<&str>, // optional different name of a remote to push to (defaults to same as the branch)
) -> Result<BaseBranch, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
let branch_name = format!("refs/remotes/{}", branch)
.parse()
.context("Invalid branch name")?;
let base_branch = VirtualBranchActions::default()
let base_branch = VirtualBranchActions
.set_base_branch(&project, &branch_name)
.await?;
// if they also sent a different push remote, set that too
if let Some(push_remote) = push_remote {
VirtualBranchActions::default()
VirtualBranchActions
.set_target_push_remote(&project, push_remote)
.await?;
}
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(base_branch)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn update_base_branch(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
) -> Result<Vec<ReferenceName>, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let unapplied_branches = VirtualBranchActions::default()
.update_base_branch(&project)
.await?;
emit_vbranches(&handle, project_id).await;
let project = projects.get(project_id)?;
let unapplied_branches = VirtualBranchActions.update_base_branch(&project).await?;
emit_vbranches(&windows, project_id).await;
Ok(unapplied_branches)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn update_virtual_branch(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch: BranchUpdateRequest,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
VirtualBranchActions::default()
let project = projects.get(project_id)?;
VirtualBranchActions
.update_virtual_branch(&project, branch)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn delete_virtual_branch(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
VirtualBranchActions::default()
let project = projects.get(project_id)?;
VirtualBranchActions
.delete_virtual_branch(&project, branch_id)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn convert_to_real_branch(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch: BranchId,
name_conflict_resolution: NameConflitResolution,
name_conflict_resolution: NameConflictResolution,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
VirtualBranchActions::default()
let project = projects.get(project_id)?;
VirtualBranchActions
.convert_to_real_branch(&project, branch, name_conflict_resolution)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn unapply_ownership(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
ownership: BranchOwnershipClaims,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
VirtualBranchActions::default()
let project = projects.get(project_id)?;
VirtualBranchActions
.unapply_ownership(&project, &ownership)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn reset_files(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
files: &str,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
// convert files to Vec<String>
let files = files
.split('\n')
.map(std::string::ToString::to_string)
.collect::<Vec<String>>();
VirtualBranchActions::default()
.reset_files(&project, &files)
.await?;
emit_vbranches(&handle, project_id).await;
VirtualBranchActions.reset_files(&project, &files).await?;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn push_virtual_branch(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch_id: BranchId,
with_force: bool,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
VirtualBranchActions::default()
let project = projects.get(project_id)?;
VirtualBranchActions
.push_virtual_branch(&project, branch_id, with_force, Some(Some(branch_id)))
.await
.map_err(|err| err.context(Code::Unknown))?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn can_apply_remote_branch(
handle: AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch: RemoteRefname,
) -> Result<bool, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
Ok(VirtualBranchActions::default()
let project = projects.get(project_id)?;
Ok(VirtualBranchActions
.can_apply_remote_branch(&project, &branch)
.await?)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn list_remote_commit_files(
handle: AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
commit_oid: String,
) -> Result<Vec<RemoteBranchFile>, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
let commit_oid = git2::Oid::from_str(&commit_oid).map_err(|e| anyhow!(e))?;
VirtualBranchActions::default()
VirtualBranchActions
.list_remote_commit_files(&project, commit_oid)
.await
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn reset_virtual_branch(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch_id: BranchId,
target_commit_oid: String,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
let target_commit_oid = git2::Oid::from_str(&target_commit_oid).map_err(|e| anyhow!(e))?;
VirtualBranchActions::default()
VirtualBranchActions
.reset_virtual_branch(&project, branch_id, target_commit_oid)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn amend_virtual_branch(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch_id: BranchId,
commit_oid: String,
ownership: BranchOwnershipClaims,
) -> Result<String, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
let commit_oid = git2::Oid::from_str(&commit_oid).map_err(|e| anyhow!(e))?;
let oid = VirtualBranchActions::default()
let oid = VirtualBranchActions
.amend(&project, branch_id, commit_oid, &ownership)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(oid.to_string())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn move_commit_file(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch_id: BranchId,
from_commit_oid: String,
to_commit_oid: String,
ownership: BranchOwnershipClaims,
) -> Result<String, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
let from_commit_oid = git2::Oid::from_str(&from_commit_oid).map_err(|e| anyhow!(e))?;
let to_commit_oid = git2::Oid::from_str(&to_commit_oid).map_err(|e| anyhow!(e))?;
let oid = VirtualBranchActions::default()
let oid = VirtualBranchActions
.move_commit_file(
&project,
branch_id,
@ -338,118 +346,119 @@ pub mod commands {
&ownership,
)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(oid.to_string())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn undo_commit(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch_id: BranchId,
commit_oid: String,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
let commit_oid = git2::Oid::from_str(&commit_oid).map_err(|e| anyhow!(e))?;
VirtualBranchActions::default()
VirtualBranchActions
.undo_commit(&project, branch_id, commit_oid)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn insert_blank_commit(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch_id: BranchId,
commit_oid: String,
offset: i32,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
let commit_oid = git2::Oid::from_str(&commit_oid).map_err(|e| anyhow!(e))?;
VirtualBranchActions::default()
VirtualBranchActions
.insert_blank_commit(&project, branch_id, commit_oid, offset)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn reorder_commit(
handle: AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch_id: BranchId,
commit_oid: String,
offset: i32,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
let commit_oid = git2::Oid::from_str(&commit_oid).map_err(|e| anyhow!(e))?;
VirtualBranchActions::default()
VirtualBranchActions
.reorder_commit(&project, branch_id, commit_oid, offset)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn list_remote_branches(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
) -> Result<Vec<RemoteBranch>, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let branches = VirtualBranchActions::default()
.list_remote_branches(project)
.await?;
let project = projects.get(project_id)?;
let branches = VirtualBranchActions::list_remote_branches(project).await?;
Ok(branches)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn get_remote_branch_data(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
refname: Refname,
) -> Result<RemoteBranchData, Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let branch_data = VirtualBranchActions::default()
let project = projects.get(project_id)?;
let branch_data = VirtualBranchActions
.get_remote_branch_data(&project, &refname)
.await?;
Ok(branch_data)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn squash_branch_commit(
handle: tauri::AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch_id: BranchId,
target_commit_oid: String,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
let target_commit_oid = git2::Oid::from_str(&target_commit_oid).map_err(|e| anyhow!(e))?;
VirtualBranchActions::default()
VirtualBranchActions
.squash(&project, branch_id, target_commit_oid)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects), err(Debug))]
pub async fn fetch_from_remotes(
handle: tauri::AppHandle,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
action: Option<String>,
) -> Result<BaseBranch, Error> {
let projects = handle.state::<projects::Controller>();
let project = projects.get(project_id)?;
let project_data_last_fetched = VirtualBranchActions::default()
let project_data_last_fetched = VirtualBranchActions
.fetch_from_remotes(
&project,
Some(action.unwrap_or_else(|| "unknown".to_string())),
@ -468,50 +477,49 @@ pub mod commands {
.await
.context("failed to update project with last fetched timestamp")?;
let base_branch = VirtualBranchActions::default()
.get_base_branch_data(&project)
.await?;
let base_branch = VirtualBranchActions::get_base_branch_data(&project).await?;
Ok(base_branch)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn move_commit(
handle: tauri::AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
commit_oid: String,
target_branch_id: BranchId,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
let commit_oid = git2::Oid::from_str(&commit_oid).map_err(|e| anyhow!(e))?;
VirtualBranchActions::default()
VirtualBranchActions
.move_commit(&project, target_branch_id, commit_oid)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(projects, windows), err(Debug))]
pub async fn update_commit_message(
handle: tauri::AppHandle,
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch_id: BranchId,
commit_oid: String,
message: &str,
) -> Result<(), Error> {
let project = handle.state::<projects::Controller>().get(project_id)?;
let project = projects.get(project_id)?;
let commit_oid = git2::Oid::from_str(&commit_oid).map_err(|e| anyhow!(e))?;
VirtualBranchActions::default()
VirtualBranchActions
.update_commit_message(&project, branch_id, commit_oid, message)
.await?;
emit_vbranches(&handle, project_id).await;
emit_vbranches(&windows, project_id).await;
Ok(())
}
async fn emit_vbranches(handle: &AppHandle, project_id: projects::ProjectId) {
if let Err(error) = handle
.state::<WindowState>()
async fn emit_vbranches(windows: &WindowState, project_id: projects::ProjectId) {
if let Err(error) = windows
.post(gitbutler_watcher::Action::CalculateVirtualBranches(
project_id,
))

View File

@ -2,7 +2,7 @@ pub(super) mod state {
use std::collections::BTreeMap;
use std::sync::Arc;
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result};
use futures::executor::block_on;
use gitbutler_project as projects;
use gitbutler_project::ProjectId;
@ -66,9 +66,6 @@ pub(super) mod state {
}
use event::ChangeForFrontend;
/// The name of the lock file to signal exclusive access to other windows.
const WINDOW_LOCK_FILE: &str = "window.lock";
struct State {
/// The id of the project displayed by the window.
project_id: ProjectId,
@ -104,7 +101,7 @@ pub(super) mod state {
fn handler_from_app(app: &AppHandle) -> Result<gitbutler_watcher::Handler> {
let projects = app.state::<projects::Controller>().inner().clone();
let users = app.state::<users::Controller>().inner().clone();
let vbranches = gitbutler_branch_actions::VirtualBranchActions::default();
let vbranches = gitbutler_branch_actions::VirtualBranchActions;
Ok(gitbutler_watcher::Handler::new(
projects,
@ -141,18 +138,7 @@ pub(super) mod state {
return Ok(());
}
}
let mut lock_file =
fslock::LockFile::open(project.gb_dir().join(WINDOW_LOCK_FILE).as_os_str())?;
let got_lock = lock_file
.try_lock()
.context("Failed to check if lock is taken")?;
if !got_lock {
bail!(
"Project '{}' is already opened in another window",
project.title
);
}
let exclusive_access = project.try_exclusive_access()?;
let handler = handler_from_app(&self.app_handle)?;
let worktree_dir = project.path.clone();
let project_id = project.id;
@ -163,7 +149,7 @@ pub(super) mod state {
State {
project_id,
watcher,
exclusive_access: lock_file,
exclusive_access,
},
);
tracing::debug!("Maintaining {} Windows", state_by_label.len());

View File

@ -1,54 +1,44 @@
pub mod commands {
#![allow(clippy::used_underscore_binding)]
use anyhow::Context;
use gitbutler_feedback::controller;
use std::path;
use gitbutler_error::error;
use gitbutler_error::error::Code;
use tauri::{AppHandle, Manager};
use gitbutler_feedback::Archival;
use std::path::PathBuf;
use tauri::State;
use tracing::instrument;
use crate::error::Error;
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(archival), err(Debug))]
pub async fn get_project_archive_path(
handle: AppHandle,
archival: State<'_, Archival>,
project_id: &str,
) -> Result<path::PathBuf, Error> {
) -> Result<PathBuf, Error> {
let project_id = project_id.parse().context(error::Context::new_static(
Code::Validation,
"Malformed project id",
))?;
handle
.state::<controller::Controller>()
.archive(project_id)
.map_err(Into::into)
archival.archive(project_id).map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
#[instrument(skip(archival), err(Debug))]
pub async fn get_project_data_archive_path(
handle: AppHandle,
archival: State<'_, Archival>,
project_id: &str,
) -> Result<path::PathBuf, Error> {
) -> Result<PathBuf, Error> {
let project_id = project_id.parse().context(error::Context::new_static(
Code::Validation,
"Malformed project id",
))?;
handle
.state::<controller::Controller>()
.data_archive(project_id)
.map_err(Into::into)
archival.data_archive(project_id).map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn get_logs_archive_path(handle: AppHandle) -> Result<path::PathBuf, Error> {
handle
.state::<controller::Controller>()
.logs_archive()
.map_err(Into::into)
#[instrument(skip(archival), err(Debug))]
pub async fn get_logs_archive_path(archival: State<'_, Archival>) -> Result<PathBuf, Error> {
archival.logs_archive().map_err(Into::into)
}
}

View File

@ -18,7 +18,7 @@ pub mod paths {
}
pub mod virtual_branches {
use gitbutler_branch::target::Target;
use gitbutler_branch::Target;
use gitbutler_branch::VirtualBranchesHandle;
use gitbutler_command_context::ProjectRepository;
@ -42,11 +42,8 @@ pub mod virtual_branches {
})
.expect("failed to write target");
gitbutler_branch_actions::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)
.expect("failed to update integration");
gitbutler_branch_actions::update_gitbutler_integration(&vb_state, project_repository)
.expect("failed to update integration");
Ok(())
}

View File

@ -12,7 +12,7 @@ use crate::{init_opts, init_opts_bare, VAR_NO_CLEANUP};
pub struct Suite {
pub local_app_data: Option<TempDir>,
pub storage: gitbutler_storage::storage::Storage,
pub storage: gitbutler_storage::Storage,
pub users: gitbutler_user::Controller,
pub projects: gitbutler_project::Controller,
}
@ -28,7 +28,7 @@ impl Drop for Suite {
impl Default for Suite {
fn default() -> Self {
let local_app_data = temp_dir();
let storage = gitbutler_storage::storage::Storage::new(local_app_data.path());
let storage = gitbutler_storage::Storage::new(local_app_data.path());
let users = gitbutler_user::Controller::from_path(local_app_data.path());
let projects = gitbutler_project::Controller::from_path(local_app_data.path());
Self {

View File

@ -17,12 +17,10 @@ pub struct Controller {
}
impl Controller {
pub fn new(storage: Storage) -> Controller {
Controller { storage }
}
pub fn from_path(path: impl Into<PathBuf>) -> Controller {
Controller::new(Storage::from_path(path))
Controller {
storage: Storage::from_path(path),
}
}
/// Return the current login, or `None` if there is none yet.

View File

@ -1,6 +1,7 @@
pub mod controller;
pub mod storage;
mod user;
mod controller;
pub use controller::*;
mod storage;
pub use controller::Controller;
mod user;
pub use user::User;

View File

@ -1,24 +1,20 @@
use anyhow::Result;
use std::path::PathBuf;
use gitbutler_storage::storage as core_storage;
use crate::User;
const USER_FILE: &str = "user.json";
#[derive(Debug, Clone)]
pub struct Storage {
inner: core_storage::Storage,
pub(crate) struct Storage {
inner: gitbutler_storage::Storage,
}
impl Storage {
pub fn new(storage: core_storage::Storage) -> Storage {
Storage { inner: storage }
}
pub fn from_path(path: impl Into<PathBuf>) -> Storage {
Storage::new(core_storage::Storage::new(path))
Storage {
inner: gitbutler_storage::Storage::new(path),
}
}
pub fn get(&self) -> Result<Option<User>> {

View File

@ -7,7 +7,7 @@ use gitbutler_command_context::ProjectRepository;
use gitbutler_error::error::Marker;
use gitbutler_oplog::{
entry::{OperationKind, SnapshotDetails},
oplog::Oplog,
OplogExt,
};
use gitbutler_project as projects;
use gitbutler_project::ProjectId;
@ -122,13 +122,8 @@ impl Handler {
paths: Vec<PathBuf>,
project_id: ProjectId,
) -> Result<()> {
// Create a snapshot every time there are more than a configurable number of new lines of code (default 20)
let handle_snapshots = tokio::task::spawn_blocking({
let this = self.clone();
move || this.maybe_create_snapshot(project_id)
});
self.maybe_create_snapshot(project_id).ok();
self.calculate_virtual_branches(project_id).await?;
let _ = handle_snapshots.await;
Ok(())
}
@ -141,7 +136,11 @@ impl Handler {
.should_auto_snapshot(std::time::Duration::from_secs(300))
.unwrap_or_default()
{
project.create_snapshot(SnapshotDetails::new(OperationKind::FileChanges))?;
let mut guard = project.exclusive_worktree_access();
project.create_snapshot(
SnapshotDetails::new(OperationKind::FileChanges),
guard.write_permission(),
)?;
}
Ok(())
}

View File

@ -14,7 +14,7 @@ mock_instant = ["dep:mock_instant"]
tracing = "0.1.40"
notify = { version = "6.0.1" }
parking_lot = "0.12.3"
parking_lot.workspace = true
file-id = "0.2.1"
walkdir = "2.2.2"
crossbeam-channel = "0.5.13"