mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2025-01-04 07:25:44 +03:00
Merge pull request #4349 from Byron/refactor
refactor and PoC for worktree mutability handling
This commit is contained in:
commit
8ccd8d511e
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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>> {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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()
|
||||
},
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
use gitbutler_branch::branch::{BranchCreateRequest, BranchId};
|
||||
use gitbutler_branch::{BranchCreateRequest, BranchId};
|
||||
|
||||
use super::Test;
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
},
|
||||
|
@ -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();
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::fs;
|
||||
|
||||
use gitbutler_branch::branch::BranchCreateRequest;
|
||||
use gitbutler_branch::BranchCreateRequest;
|
||||
|
||||
use super::Test;
|
||||
|
||||
|
@ -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()
|
||||
},
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::fs;
|
||||
|
||||
use gitbutler_branch::{branch::BranchCreateRequest, ownership::BranchOwnershipClaims};
|
||||
use gitbutler_branch::{BranchCreateRequest, BranchOwnershipClaims};
|
||||
|
||||
use super::Test;
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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()
|
||||
},
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()`].
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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)?)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use gitbutler_branch::file_ownership::OwnershipClaim;
|
||||
use gitbutler_branch::OwnershipClaim;
|
||||
|
||||
#[test]
|
||||
fn parse_ownership() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use gitbutler_branch::hunk::Hunk;
|
||||
use gitbutler_branch::Hunk;
|
||||
|
||||
#[test]
|
||||
fn to_from_string() {
|
||||
|
@ -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]
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
pub mod controller;
|
||||
pub mod zipper;
|
||||
mod controller;
|
||||
pub use controller::Archival;
|
||||
mod zipper;
|
||||
|
@ -159,3 +159,6 @@ fn file_hash<P: AsRef<path::Path>>(digest: &mut Sha256, path: P) -> Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
@ -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;
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
@ -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)?)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
104
crates/gitbutler-project/src/access.rs
Normal file
104
crates/gitbutler-project/src/access.rs
Normal 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());
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>> {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -1 +1,2 @@
|
||||
pub mod storage;
|
||||
mod storage;
|
||||
pub use storage::Storage;
|
||||
|
@ -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`.
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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"))?;
|
||||
|
@ -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)?)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -13,7 +13,9 @@
|
||||
clippy::too_many_lines
|
||||
)]
|
||||
|
||||
pub mod app;
|
||||
mod app;
|
||||
pub use app::App;
|
||||
|
||||
pub mod commands;
|
||||
|
||||
pub mod logs;
|
||||
|
@ -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();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
))
|
||||
|
@ -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());
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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>> {
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user