mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-22 19:14:31 +03:00
enforce in-process-synchronization during worktree updates and prolonged reads in oplog
That way it's assured that reads and writes don't intersect, but assure we only hold such lock for the shortest amount of time for reads and and for the full duration of writes.
This commit is contained in:
parent
3e79238e7f
commit
09ca2d0284
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2176,6 +2176,7 @@ name = "gitbutler-project"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"fslock",
|
||||
"git2",
|
||||
"gitbutler-error",
|
||||
"gitbutler-id",
|
||||
@ -2183,6 +2184,7 @@ dependencies = [
|
||||
"gitbutler-storage",
|
||||
"gitbutler-testsupport",
|
||||
"gix",
|
||||
"parking_lot 0.12.3",
|
||||
"resolve-path",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -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" }
|
||||
|
@ -5,8 +5,7 @@ use gitbutler_branch::{
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_oplog::{
|
||||
entry::{OperationKind, SnapshotDetails},
|
||||
oplog::OplogExt,
|
||||
snapshot::SnapshotExt,
|
||||
OplogExt, SnapshotExt,
|
||||
};
|
||||
use gitbutler_project::{FetchResult, Project};
|
||||
use gitbutler_reference::ReferenceName;
|
||||
|
@ -11,7 +11,7 @@ use gitbutler_branch::{
|
||||
};
|
||||
use gitbutler_commit::commit_headers::HasCommitHeaders;
|
||||
use gitbutler_error::error::Marker;
|
||||
use gitbutler_oplog::snapshot::SnapshotExt;
|
||||
use gitbutler_oplog::SnapshotExt;
|
||||
use gitbutler_reference::Refname;
|
||||
use gitbutler_repo::{rebase::cherry_rebase, RepoActionsExt, RepositoryExt};
|
||||
use gitbutler_time::time::now_since_unix_epoch_ms;
|
||||
|
@ -8,7 +8,7 @@ use anyhow::{anyhow, Context, Result};
|
||||
use git2::build::TreeUpdateBuilder;
|
||||
use gitbutler_branch::{Branch, BranchExt, BranchId};
|
||||
use gitbutler_commit::commit_headers::CommitHeadersV2;
|
||||
use gitbutler_oplog::snapshot::SnapshotExt;
|
||||
use gitbutler_oplog::SnapshotExt;
|
||||
use gitbutler_reference::ReferenceName;
|
||||
use gitbutler_reference::{normalize_branch_name, Refname};
|
||||
use gitbutler_repo::{RepoActionsExt, RepositoryExt};
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use gitbutler_branch::{BranchCreateRequest, VirtualBranchesHandle};
|
||||
use gitbutler_oplog::oplog::OplogExt;
|
||||
use gitbutler_oplog::OplogExt;
|
||||
use itertools::Itertools;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use gitbutler_oplog::oplog::OplogExt;
|
||||
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(())
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -13,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;
|
||||
|
||||
@ -130,118 +130,8 @@ pub trait OplogExt {
|
||||
|
||||
impl OplogExt 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)
|
||||
let guard = self.shared_worktree_access();
|
||||
prepare_snapshot(self, guard.read_permission())
|
||||
}
|
||||
|
||||
fn commit_snapshot(
|
||||
@ -249,46 +139,15 @@ impl OplogExt for Project {
|
||||
snapshot_tree_id: git2::Oid,
|
||||
details: SnapshotDetails,
|
||||
) -> 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))
|
||||
let mut guard = self.exclusive_worktree_access();
|
||||
commit_snapshot(self, snapshot_tree_id, details, guard.write_permission())
|
||||
}
|
||||
|
||||
#[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)
|
||||
let mut guard = self.exclusive_worktree_access();
|
||||
let tree_id = prepare_snapshot(self, guard.read_permission())?;
|
||||
commit_snapshot(self, tree_id, details, guard.write_permission())
|
||||
}
|
||||
|
||||
fn list_snapshots(
|
||||
@ -396,153 +255,8 @@ impl OplogExt 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> {
|
||||
@ -596,6 +310,321 @@ impl OplogExt 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
|
||||
|
@ -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"
|
||||
|
134
crates/gitbutler-project/src/access.rs
Normal file
134
crates/gitbutler-project/src/access.rs
Normal file
@ -0,0 +1,134 @@
|
||||
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.
|
||||
/// The guard can be upgraded to allow for writes, which is useful if a mutation is prepared by various reads
|
||||
/// first, followed by conclusive writes.
|
||||
pub fn shared_upgradable_worktree_access(&self) -> UpgradableWorkspaceReadGuard {
|
||||
let mut map = WORKTREE_LOCKS.lock();
|
||||
UpgradableWorkspaceReadGuard(map.entry(self.id).or_default().upgradable_read_arc())
|
||||
}
|
||||
|
||||
/// 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 UpgradableWorkspaceReadGuard(parking_lot::ArcRwLockUpgradableReadGuard<RawRwLock, ()>);
|
||||
|
||||
impl UpgradableWorkspaceReadGuard {
|
||||
/// Wait until a write-lock for exclusive access can be acquired, and return a handle to it.
|
||||
/// It must be kept alive until the write operation completes.
|
||||
pub fn upgrade_to_exclusive_worktree_access(self) -> WriteWorkspaceGuard {
|
||||
WriteWorkspaceGuard {
|
||||
_inner: parking_lot::ArcRwLockUpgradableReadGuard::upgrade(self.0),
|
||||
perm: 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
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
@ -1,3 +1,4 @@
|
||||
pub mod access;
|
||||
mod controller;
|
||||
mod default_true;
|
||||
mod project;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -8,7 +8,7 @@ use gitbutler_branch::VirtualBranchesHandle;
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_error::error::Code;
|
||||
use gitbutler_id::id::Id;
|
||||
use gitbutler_oplog::oplog::OplogExt;
|
||||
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"
|
||||
|
@ -2,7 +2,7 @@ use crate::error::Error;
|
||||
use anyhow::Context;
|
||||
use gitbutler_branch::diff::FileDiff;
|
||||
use gitbutler_oplog::entry::Snapshot;
|
||||
use gitbutler_oplog::oplog::OplogExt;
|
||||
use gitbutler_oplog::OplogExt;
|
||||
use gitbutler_project as projects;
|
||||
use gitbutler_project::ProjectId;
|
||||
use std::collections::HashMap;
|
||||
|
@ -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,
|
||||
@ -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());
|
||||
|
@ -7,7 +7,7 @@ use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_error::error::Marker;
|
||||
use gitbutler_oplog::{
|
||||
entry::{OperationKind, SnapshotDetails},
|
||||
oplog::OplogExt,
|
||||
OplogExt,
|
||||
};
|
||||
use gitbutler_project as projects;
|
||||
use gitbutler_project::ProjectId;
|
||||
|
@ -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