Merge pull request #3694 from gitbutlerapp/refactor-snapshot-functions-as-trait-methods

refactor snapshotting as an Oplog trait
This commit is contained in:
Kiril Videlov 2024-05-05 22:46:08 +02:00 committed by GitHub
commit f2b2434d7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 334 additions and 337 deletions

View File

@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use gitbutler_core::{projects::Project, snapshots::snapshot}; use gitbutler_core::{projects::Project, snapshots::snapshot::Oplog};
use clap::{arg, Command}; use clap::{arg, Command};
#[cfg(not(windows))] #[cfg(not(windows))]
@ -49,7 +49,7 @@ fn main() -> Result<()> {
fn list_snapshots(repo_dir: &str) -> Result<()> { fn list_snapshots(repo_dir: &str) -> Result<()> {
let project = project_from_path(repo_dir); let project = project_from_path(repo_dir);
let snapshots = snapshot::list(&project, 100)?; let snapshots = project.list_snapshots(100)?;
for snapshot in snapshots { for snapshot in snapshots {
let ts = chrono::DateTime::from_timestamp(snapshot.created_at / 1000, 0); let ts = chrono::DateTime::from_timestamp(snapshot.created_at / 1000, 0);
let details = snapshot.details; let details = snapshot.details;
@ -62,7 +62,7 @@ fn list_snapshots(repo_dir: &str) -> Result<()> {
fn restore_snapshot(repo_dir: &str, snapshot_id: &str) -> Result<()> { fn restore_snapshot(repo_dir: &str, snapshot_id: &str) -> Result<()> {
let project = project_from_path(repo_dir); let project = project_from_path(repo_dir);
snapshot::restore(&project, snapshot_id.to_owned())?; project.restore_snapshot(snapshot_id.to_owned())?;
Ok(()) Ok(())
} }

View File

@ -16,257 +16,270 @@ use super::{
const SNAPSHOT_FILE_LIMIT_BYTES: u64 = 32 * 1024 * 1024; const SNAPSHOT_FILE_LIMIT_BYTES: u64 = 32 * 1024 * 1024;
/// Creates a snapshot of the current state of the repository and virtual branches using the given label. /// The Oplog trait allows for crating snapshots of the current state of the project as well as restoring to a previous snapshot.
/// /// Snapshots include the state of the working directory as well as all additional GitButler state (e.g virtual branches, conflict state).
/// If this is the first shapshot created, supporting structures are initialized: pub trait Oplog {
/// - The current oplog head is persisted in `.git/gitbutler/oplog.toml`. /// Creates a snapshot of the current state of the repository and virtual branches using the given label.
/// - A fake branch `gitbutler/target` is created and maintained in order to keep the oplog head reachable. ///
/// /// If this is the first shapshot created, supporting structures are initialized:
/// The snapshot tree contains: /// - The current oplog head is persisted in `.git/gitbutler/oplog.toml`.
/// - The current state of the working directory under a subtree `workdir`. /// - A fake branch `gitbutler/target` is created and maintained in order to keep the oplog head reachable.
/// - The state of virtual branches from `.git/gitbutler/virtual_branches.toml` as a blob `virtual_branches.toml`. ///
/// - The state of conflicts from `.git/base_merge_parent` and `.git/conflicts` if present as blobs under a subtree `conflicts` /// The snapshot tree contains:
/// /// - The current state of the working directory under a subtree `workdir`.
/// If there are files that are untracked and larger than SNAPSHOT_FILE_LIMIT_BYTES, they are excluded from snapshot creation and restoring. /// - The state of virtual branches from `.git/gitbutler/virtual_branches.toml` as a blob `virtual_branches.toml`.
/// Returns the sha of the created snapshot commit or None if snapshots are disabled. /// - The state of conflicts from `.git/base_merge_parent` and `.git/conflicts` if present as blobs under a subtree `conflicts`
pub fn create(project: &Project, details: SnapshotDetails) -> Result<Option<String>> { ///
if project.enable_snapshots.is_none() || project.enable_snapshots == Some(false) { /// If there are files that are untracked and larger than SNAPSHOT_FILE_LIMIT_BYTES, they are excluded from snapshot creation and restoring.
return Ok(None); /// Returns the sha of the created snapshot commit or None if snapshots are disabled.
} fn create_snapshot(&self, details: SnapshotDetails) -> Result<Option<String>>;
/// Lists the snapshots that have been created for the given repository, up to the given limit.
let repo_path = project.path.as_path(); /// An alternative way of retrieving the snapshots would be to manually the oplog head `git log <oplog_head>` available in `.git/gitbutler/oplog.toml`.
let repo = git2::Repository::init(repo_path)?; ///
/// If there are no snapshots, an empty list is returned.
let vb_state = VirtualBranchesHandle::new(&project.gb_dir()); fn list_snapshots(&self, limit: usize) -> Result<Vec<Snapshot>>;
let default_target_sha = vb_state.get_default_target()?.sha; /// Reverts to a previous state of the working directory, virtual branches and commits.
/// The provided sha must refer to a valid snapshot commit.
let oplog_state = OplogHandle::new(&project.gb_dir()); /// Upon success, a new snapshot is created.
let oplog_head_commit = match oplog_state.get_oplog_head()? { ///
Some(head_sha) => match repo.find_commit(git2::Oid::from_str(&head_sha)?) { /// This will restore the following:
Ok(commit) => commit, /// - The state of the working directory is checked out from the subtree `workdir` in the snapshot.
Err(_) => repo.find_commit(default_target_sha.into())?, /// - The state of virtual branches is restored from the blob `virtual_branches.toml` in the snapshot.
}, /// - The state of conflicts (.git/base_merge_parent and .git/conflicts) is restored from the subtree `conflicts` in the snapshot (if not present, existing files are deleted).
// This is the first snapshot - use the default target as starting point ///
None => repo.find_commit(default_target_sha.into())?, /// If there are files that are untracked and larger than SNAPSHOT_FILE_LIMIT_BYTES, they are excluded from snapshot creation and restoring.
}; /// Returns the sha of the created revert snapshot commit or None if snapshots are disabled.
fn restore_snapshot(&self, sha: String) -> Result<Option<String>>;
// Create a blob out of `.git/gitbutler/virtual_branches.toml` /// Returns the number of lines of code (added plus removed) since the last snapshot. Includes untracked files.
let vb_path = repo_path ///
.join(".git") /// If there are no snapshots, 0 is returned.
.join("gitbutler") fn lines_since_snapshot(&self) -> Result<usize>;
.join("virtual_branches.toml");
let vb_content = fs::read(vb_path)?;
let vb_blob = repo.blob(&vb_content)?;
// Create a tree out of the conflicts state if present
let conflicts_tree = write_conflicts_tree(repo_path, &repo)?;
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
let files_to_exclude = get_exclude_list(&repo)?;
// In-memory, libgit2 internal ignore rule
repo.add_ignore_rule(&files_to_exclude)?;
// Add everything in the workdir to the index
let mut index = repo.index()?;
index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
index.write()?;
// Create a tree out of the index
let tree_id = index.write_tree()?;
let mut tree_builder = repo.treebuilder(None)?;
tree_builder.insert("workdir", tree_id, FileMode::Tree.into())?;
tree_builder.insert("virtual_branches.toml", vb_blob, FileMode::Blob.into())?;
tree_builder.insert("conflicts", conflicts_tree, FileMode::Tree.into())?;
let tree_id = tree_builder.write()?;
let tree = repo.find_tree(tree_id)?;
// Construct a new commit
let name = "GitButler";
let email = "gitbutler@gitbutler.com";
let signature = git2::Signature::now(name, email).unwrap();
let new_commit_oid = repo.commit(
None,
&signature,
&signature,
&details.to_string(),
&tree,
&[&oplog_head_commit],
)?;
// Reset the workdir to how it was
let integration_branch = repo
.find_branch("gitbutler/integration", git2::BranchType::Local)?
.get()
.peel_to_commit()?;
repo.reset(
&integration_branch.into_object(),
git2::ResetType::Mixed,
None,
)?;
oplog_state.set_oplog_head(new_commit_oid.to_string())?;
set_reference_to_oplog(
project,
&default_target_sha.to_string(),
&new_commit_oid.to_string(),
)?;
Ok(Some(new_commit_oid.to_string()))
} }
/// Lists the snapshots that have been created for the given repository, up to the given limit. impl Oplog for Project {
/// An alternative way of retrieving the snapshots would be to manually the oplog head `git log <oplog_head>` available in `.git/gitbutler/oplog.toml`. fn create_snapshot(&self, details: SnapshotDetails) -> Result<Option<String>> {
/// if self.enable_snapshots.is_none() || self.enable_snapshots == Some(false) {
/// If there are no snapshots, an empty list is returned. return Ok(None);
pub fn list(project: &Project, limit: usize) -> Result<Vec<Snapshot>> {
let repo_path = project.path.as_path();
let repo = git2::Repository::init(repo_path)?;
let oplog_state = OplogHandle::new(&project.gb_dir());
let head_sha = oplog_state.get_oplog_head()?;
if head_sha.is_none() {
// there are no snapshots to return
return Ok(vec![]);
}
let head_sha = head_sha.unwrap();
let oplog_head_commit = repo.find_commit(git2::Oid::from_str(&head_sha)?)?;
let mut revwalk = repo.revwalk()?;
revwalk.push(oplog_head_commit.id())?;
let mut snapshots = Vec::new();
for commit_id in revwalk {
let commit_id = commit_id?;
let commit = repo.find_commit(commit_id)?;
if commit.parent_count() > 1 {
break;
} }
let details = commit let repo_path = self.path.as_path();
.message() let repo = git2::Repository::init(repo_path)?;
.and_then(|msg| SnapshotDetails::from_str(msg).ok());
snapshots.push(Snapshot { let vb_state = VirtualBranchesHandle::new(&self.gb_dir());
id: commit_id.to_string(), let default_target_sha = vb_state.get_default_target()?.sha;
details,
created_at: commit.time().seconds() * 1000,
});
if snapshots.len() >= limit { let oplog_state = OplogHandle::new(&self.gb_dir());
break; let oplog_head_commit = match oplog_state.get_oplog_head()? {
} Some(head_sha) => match repo.find_commit(git2::Oid::from_str(&head_sha)?) {
} Ok(commit) => commit,
Err(_) => repo.find_commit(default_target_sha.into())?,
},
// This is the first snapshot - use the default target as starting point
None => repo.find_commit(default_target_sha.into())?,
};
Ok(snapshots) // Create a blob out of `.git/gitbutler/virtual_branches.toml`
} let vb_path = repo_path
/// Reverts to a previous state of the working directory, virtual branches and commits.
/// The provided sha must refer to a valid snapshot commit.
/// Upon success, a new snapshot is created.
///
/// This will restore the following:
/// - The state of the working directory is checked out from the subtree `workdir` in the snapshot.
/// - The state of virtual branches is restored from the blob `virtual_branches.toml` in the snapshot.
/// - The state of conflicts (.git/base_merge_parent and .git/conflicts) is restored from the subtree `conflicts` in the snapshot (if not present, existing files are deleted).
///
/// If there are files that are untracked and larger than SNAPSHOT_FILE_LIMIT_BYTES, they are excluded from snapshot creation and restoring.
/// Returns the sha of the created revert snapshot commit or None if snapshots are disabled.
pub fn restore(project: &Project, sha: String) -> Result<Option<String>> {
let repo_path = project.path.as_path();
let repo = git2::Repository::init(repo_path)?;
let commit = repo.find_commit(git2::Oid::from_str(&sha)?)?;
// Top tree
let tree = commit.tree()?;
let vb_tree_entry = tree
.get_name("virtual_branches.toml")
.ok_or(anyhow!("failed to get virtual_branches tree entry"))?;
// virtual_branches.toml blob
let vb_blob = vb_tree_entry
.to_object(&repo)?
.into_blob()
.map_err(|_| anyhow!("failed to convert virtual_branches tree entry to blob"))?;
// 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
_ = restore_conflicts_tree(&tree, &repo, repo_path);
let wd_tree_entry = tree
.get_name("workdir")
.ok_or(anyhow!("failed to get workdir tree entry"))?;
// workdir tree
let tree = repo.find_tree(wd_tree_entry.id())?;
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
let files_to_exclude = get_exclude_list(&repo)?;
// 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(tree.as_object(), Some(&mut checkout_builder))?;
// Update virtual_branches.toml with the state from the snapshot
fs::write(
repo_path
.join(".git") .join(".git")
.join("gitbutler") .join("gitbutler")
.join("virtual_branches.toml"), .join("virtual_branches.toml");
vb_blob.content(), let vb_content = fs::read(vb_path)?;
)?; let vb_blob = repo.blob(&vb_content)?;
// create new snapshot // Create a tree out of the conflicts state if present
let details = SnapshotDetails { let conflicts_tree = write_conflicts_tree(repo_path, &repo)?;
version: Default::default(),
operation: OperationType::RestoreFromSnapshot,
title: "Restored from snapshot".to_string(),
body: None,
trailers: vec![Trailer {
key: "restored_from".to_string(),
value: sha,
}],
};
create(project, details)
}
/// Returns the number of uncommitted lines of code (added plus removed) since the last snapshot. Included untracked files. // Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
pub fn changed_lines_count(project: &Project) -> Result<usize> { let files_to_exclude = get_exclude_list(&repo)?;
let repo_path = project.path.as_path(); // In-memory, libgit2 internal ignore rule
let repo = git2::Repository::init(repo_path)?; repo.add_ignore_rule(&files_to_exclude)?;
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed) // Add everything in the workdir to the index
let files_to_exclude = get_exclude_list(&repo)?; let mut index = repo.index()?;
// In-memory, libgit2 internal ignore rule index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
repo.add_ignore_rule(&files_to_exclude)?; index.write()?;
let oplog_state = OplogHandle::new(&project.gb_dir()); // Create a tree out of the index
let head_sha = oplog_state.get_oplog_head()?; let tree_id = index.write_tree()?;
if head_sha.is_none() {
return Ok(0); let mut tree_builder = repo.treebuilder(None)?;
tree_builder.insert("workdir", tree_id, FileMode::Tree.into())?;
tree_builder.insert("virtual_branches.toml", vb_blob, FileMode::Blob.into())?;
tree_builder.insert("conflicts", conflicts_tree, FileMode::Tree.into())?;
let tree_id = tree_builder.write()?;
let tree = repo.find_tree(tree_id)?;
// Construct a new commit
let name = "GitButler";
let email = "gitbutler@gitbutler.com";
let signature = git2::Signature::now(name, email).unwrap();
let new_commit_oid = repo.commit(
None,
&signature,
&signature,
&details.to_string(),
&tree,
&[&oplog_head_commit],
)?;
// Reset the workdir to how it was
let integration_branch = repo
.find_branch("gitbutler/integration", git2::BranchType::Local)?
.get()
.peel_to_commit()?;
repo.reset(
&integration_branch.into_object(),
git2::ResetType::Mixed,
None,
)?;
oplog_state.set_oplog_head(new_commit_oid.to_string())?;
set_reference_to_oplog(
self,
&default_target_sha.to_string(),
&new_commit_oid.to_string(),
)?;
Ok(Some(new_commit_oid.to_string()))
} }
let head_sha = head_sha.unwrap();
let commit = repo.find_commit(git2::Oid::from_str(&head_sha)?)?; fn list_snapshots(&self, limit: usize) -> Result<Vec<Snapshot>> {
let head_tree = commit.tree()?; let repo_path = self.path.as_path();
let repo = git2::Repository::init(repo_path)?;
let wd_tree_entry = head_tree let oplog_state = OplogHandle::new(&self.gb_dir());
.get_name("workdir") let head_sha = oplog_state.get_oplog_head()?;
.ok_or(anyhow!("failed to get workdir tree entry"))?; if head_sha.is_none() {
let wd_tree = repo.find_tree(wd_tree_entry.id())?; // there are no snapshots to return
return Ok(vec![]);
}
let head_sha = head_sha.unwrap();
let mut opts = git2::DiffOptions::new(); let oplog_head_commit = repo.find_commit(git2::Oid::from_str(&head_sha)?)?;
opts.include_untracked(true);
let diff = repo.diff_tree_to_workdir_with_index(Some(&wd_tree), Some(&mut opts)); let mut revwalk = repo.revwalk()?;
let stats = diff?.stats()?; revwalk.push(oplog_head_commit.id())?;
Ok(stats.deletions() + stats.insertions())
let mut snapshots = Vec::new();
for commit_id in revwalk {
let commit_id = commit_id?;
let commit = repo.find_commit(commit_id)?;
if commit.parent_count() > 1 {
break;
}
let details = commit
.message()
.and_then(|msg| SnapshotDetails::from_str(msg).ok());
snapshots.push(Snapshot {
id: commit_id.to_string(),
details,
created_at: commit.time().seconds() * 1000,
});
if snapshots.len() >= limit {
break;
}
}
Ok(snapshots)
}
fn restore_snapshot(&self, sha: String) -> Result<Option<String>> {
let repo_path = self.path.as_path();
let repo = git2::Repository::init(repo_path)?;
let commit = repo.find_commit(git2::Oid::from_str(&sha)?)?;
// Top tree
let tree = commit.tree()?;
let vb_tree_entry = tree
.get_name("virtual_branches.toml")
.ok_or(anyhow!("failed to get virtual_branches tree entry"))?;
// virtual_branches.toml blob
let vb_blob = vb_tree_entry
.to_object(&repo)?
.into_blob()
.map_err(|_| anyhow!("failed to convert virtual_branches tree entry to blob"))?;
// 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
_ = restore_conflicts_tree(&tree, &repo, repo_path);
let wd_tree_entry = tree
.get_name("workdir")
.ok_or(anyhow!("failed to get workdir tree entry"))?;
// workdir tree
let tree = repo.find_tree(wd_tree_entry.id())?;
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
let files_to_exclude = get_exclude_list(&repo)?;
// 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(tree.as_object(), Some(&mut checkout_builder))?;
// Update virtual_branches.toml with the state from the snapshot
fs::write(
repo_path
.join(".git")
.join("gitbutler")
.join("virtual_branches.toml"),
vb_blob.content(),
)?;
// create new snapshot
let details = SnapshotDetails {
version: Default::default(),
operation: OperationType::RestoreFromSnapshot,
title: "Restored from snapshot".to_string(),
body: None,
trailers: vec![Trailer {
key: "restored_from".to_string(),
value: sha,
}],
};
self.create_snapshot(details)
}
fn lines_since_snapshot(&self) -> Result<usize> {
let repo_path = self.path.as_path();
let repo = git2::Repository::init(repo_path)?;
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
let files_to_exclude = get_exclude_list(&repo)?;
// In-memory, libgit2 internal ignore rule
repo.add_ignore_rule(&files_to_exclude)?;
let oplog_state = OplogHandle::new(&self.gb_dir());
let head_sha = oplog_state.get_oplog_head()?;
if head_sha.is_none() {
return Ok(0);
}
let head_sha = head_sha.unwrap();
let commit = repo.find_commit(git2::Oid::from_str(&head_sha)?)?;
let head_tree = commit.tree()?;
let wd_tree_entry = head_tree
.get_name("workdir")
.ok_or(anyhow!("failed to get workdir tree entry"))?;
let wd_tree = repo.find_tree(wd_tree_entry.id())?;
let mut opts = git2::DiffOptions::new();
opts.include_untracked(true);
let diff = repo.diff_tree_to_workdir_with_index(Some(&wd_tree), Some(&mut opts));
let stats = diff?.stats()?;
Ok(stats.deletions() + stats.insertions())
}
} }
fn restore_conflicts_tree( fn restore_conflicts_tree(
@ -443,7 +456,9 @@ mod tests {
std::fs::write(&base_merge_parent_path, "parent A").unwrap(); std::fs::write(&base_merge_parent_path, "parent A").unwrap();
// create a snapshot // create a snapshot
create(&project, SnapshotDetails::new(OperationType::CreateCommit)).unwrap(); project
.create_snapshot(SnapshotDetails::new(OperationType::CreateCommit))
.unwrap();
// The large file is still here but it will not be part of the snapshot // The large file is still here but it will not be part of the snapshot
let file_path = dir.path().join("large.txt"); let file_path = dir.path().join("large.txt");
@ -473,14 +488,14 @@ mod tests {
std::fs::remove_file(&conflicts_path).unwrap(); std::fs::remove_file(&conflicts_path).unwrap();
std::fs::remove_file(&base_merge_parent_path).unwrap(); std::fs::remove_file(&base_merge_parent_path).unwrap();
// New snapshot with the conflicts removed // New snapshot with the conflicts removed
let conflicts_removed_snapshot = create( let conflicts_removed_snapshot = project
&project, .create_snapshot(SnapshotDetails::new(OperationType::UpdateWorkspaceBase))
SnapshotDetails::new(OperationType::UpdateWorkspaceBase), .unwrap();
)
.unwrap();
// restore from the initial snapshot // restore from the initial snapshot
restore(&project, list(&project, 10).unwrap()[1].id.clone()).unwrap(); project
.restore_snapshot(project.list_snapshots(10).unwrap()[1].id.clone())
.unwrap();
let file_path = dir.path().join("1.txt"); let file_path = dir.path().join("1.txt");
let file_lines = std::fs::read_to_string(file_path).unwrap(); let file_lines = std::fs::read_to_string(file_path).unwrap();
@ -509,7 +524,9 @@ mod tests {
assert_eq!(file_lines, "parent A"); assert_eq!(file_lines, "parent A");
// Restore from the second snapshot // Restore from the second snapshot
restore(&project, conflicts_removed_snapshot.unwrap()).unwrap(); project
.restore_snapshot(conflicts_removed_snapshot.unwrap())
.unwrap();
// The conflicts are not present // The conflicts are not present
assert!(!&conflicts_path.exists()); assert!(!&conflicts_path.exists());

View File

@ -2,7 +2,7 @@ use crate::{
error::Error, error::Error,
snapshots::{ snapshots::{
entry::{OperationType, SnapshotDetails}, entry::{OperationType, SnapshotDetails},
snapshot, snapshot::Oplog,
}, },
}; };
use std::{collections::HashMap, path::Path, sync::Arc}; use std::{collections::HashMap, path::Path, sync::Arc};
@ -473,10 +473,9 @@ impl ControllerInner {
) )
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::CreateCommit), .create_snapshot(SnapshotDetails::new(OperationType::CreateCommit));
)?;
result result
}) })
} }
@ -524,10 +523,9 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, _| { self.with_verify_branch(project_id, |project_repository, _| {
let branch_id = super::create_virtual_branch(project_repository, create)?.id; let branch_id = super::create_virtual_branch(project_repository, create)?.id;
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::CreateBranch), .create_snapshot(SnapshotDetails::new(OperationType::CreateBranch));
)?;
Ok(branch_id) Ok(branch_id)
}) })
} }
@ -556,10 +554,9 @@ impl ControllerInner {
signing_key.as_ref(), signing_key.as_ref(),
user, user,
)?; )?;
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::CreateBranch), .create_snapshot(SnapshotDetails::new(OperationType::CreateBranch));
)?;
Ok(result) Ok(result)
}) })
} }
@ -592,10 +589,9 @@ impl ControllerInner {
let project = self.projects.get(project_id)?; let project = self.projects.get(project_id)?;
let project_repository = project_repository::Repository::open(&project)?; let project_repository = project_repository::Repository::open(&project)?;
let result = super::set_base_branch(&project_repository, target_branch)?; let result = super::set_base_branch(&project_repository, target_branch)?;
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::SetBaseBranch), .create_snapshot(SnapshotDetails::new(OperationType::SetBaseBranch));
)?;
Ok(result) Ok(result)
} }
@ -625,10 +621,9 @@ impl ControllerInner {
user, user,
) )
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::MergeUpstream), .create_snapshot(SnapshotDetails::new(OperationType::MergeUpstream));
)?;
result result
}) })
} }
@ -650,10 +645,9 @@ impl ControllerInner {
let result = super::update_base_branch(project_repository, user, signing_key.as_ref()) let result = super::update_base_branch(project_repository, user, signing_key.as_ref())
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::UpdateWorkspaceBase), .create_snapshot(SnapshotDetails::new(OperationType::UpdateWorkspaceBase));
)?;
result result
}) })
} }
@ -682,7 +676,7 @@ impl ControllerInner {
SnapshotDetails::new(OperationType::GenericBranchUpdate) SnapshotDetails::new(OperationType::GenericBranchUpdate)
}; };
super::update_branch(project_repository, branch_update)?; super::update_branch(project_repository, branch_update)?;
snapshot::create(project_repository.project(), details)?; let _ = project_repository.project().create_snapshot(details);
Ok(()) Ok(())
}) })
} }
@ -696,10 +690,9 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, _| { self.with_verify_branch(project_id, |project_repository, _| {
super::delete_branch(project_repository, branch_id)?; super::delete_branch(project_repository, branch_id)?;
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::DeleteBranch), .create_snapshot(SnapshotDetails::new(OperationType::DeleteBranch));
)?;
Ok(()) Ok(())
}) })
} }
@ -726,10 +719,9 @@ impl ControllerInner {
let result = let result =
super::apply_branch(project_repository, branch_id, signing_key.as_ref(), user) super::apply_branch(project_repository, branch_id, signing_key.as_ref(), user)
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::ApplyBranch), .create_snapshot(SnapshotDetails::new(OperationType::ApplyBranch));
)?;
result result
}) })
} }
@ -744,10 +736,9 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, _| { self.with_verify_branch(project_id, |project_repository, _| {
let result = let result =
super::unapply_ownership(project_repository, ownership).map_err(Into::into); super::unapply_ownership(project_repository, ownership).map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::DiscardHunk), .create_snapshot(SnapshotDetails::new(OperationType::DiscardHunk));
)?;
result result
}) })
} }
@ -761,10 +752,9 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, _| { self.with_verify_branch(project_id, |project_repository, _| {
let result = super::reset_files(project_repository, ownership).map_err(Into::into); let result = super::reset_files(project_repository, ownership).map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::DiscardFile), .create_snapshot(SnapshotDetails::new(OperationType::DiscardFile));
)?;
result result
}) })
} }
@ -781,10 +771,9 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, _| { self.with_verify_branch(project_id, |project_repository, _| {
let result = super::amend(project_repository, branch_id, commit_oid, ownership) let result = super::amend(project_repository, branch_id, commit_oid, ownership)
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::AmendCommit), .create_snapshot(SnapshotDetails::new(OperationType::AmendCommit));
)?;
result result
}) })
} }
@ -808,10 +797,9 @@ impl ControllerInner {
ownership, ownership,
) )
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::MoveCommitFile), .create_snapshot(SnapshotDetails::new(OperationType::MoveCommitFile));
)?;
result result
}) })
} }
@ -827,10 +815,9 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, _| { self.with_verify_branch(project_id, |project_repository, _| {
let result = let result =
super::undo_commit(project_repository, branch_id, commit_oid).map_err(Into::into); super::undo_commit(project_repository, branch_id, commit_oid).map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::UndoCommit), .create_snapshot(SnapshotDetails::new(OperationType::UndoCommit));
)?;
result result
}) })
} }
@ -848,10 +835,9 @@ impl ControllerInner {
let result = let result =
super::insert_blank_commit(project_repository, branch_id, commit_oid, user, offset) super::insert_blank_commit(project_repository, branch_id, commit_oid, user, offset)
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::InsertBlankCommit), .create_snapshot(SnapshotDetails::new(OperationType::InsertBlankCommit));
)?;
result result
}) })
} }
@ -868,10 +854,9 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, _| { self.with_verify_branch(project_id, |project_repository, _| {
let result = super::reorder_commit(project_repository, branch_id, commit_oid, offset) let result = super::reorder_commit(project_repository, branch_id, commit_oid, offset)
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::ReorderCommit), .create_snapshot(SnapshotDetails::new(OperationType::ReorderCommit));
)?;
result result
}) })
} }
@ -887,10 +872,9 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, _| { self.with_verify_branch(project_id, |project_repository, _| {
let result = super::reset_branch(project_repository, branch_id, target_commit_oid) let result = super::reset_branch(project_repository, branch_id, target_commit_oid)
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::UndoCommit), .create_snapshot(SnapshotDetails::new(OperationType::UndoCommit));
)?;
result result
}) })
} }
@ -906,10 +890,9 @@ impl ControllerInner {
let result = super::unapply_branch(project_repository, branch_id) let result = super::unapply_branch(project_repository, branch_id)
.map(|_| ()) .map(|_| ())
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::UnapplyBranch), .create_snapshot(SnapshotDetails::new(OperationType::UnapplyBranch));
)?;
result result
}) })
} }
@ -949,10 +932,9 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, _| { self.with_verify_branch(project_id, |project_repository, _| {
let result = let result =
super::cherry_pick(project_repository, branch_id, commit_oid).map_err(Into::into); super::cherry_pick(project_repository, branch_id, commit_oid).map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::CherryPick), .create_snapshot(SnapshotDetails::new(OperationType::CherryPick));
)?;
result result
}) })
} }
@ -987,10 +969,9 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, _| { self.with_verify_branch(project_id, |project_repository, _| {
let result = let result =
super::squash(project_repository, branch_id, commit_oid).map_err(Into::into); super::squash(project_repository, branch_id, commit_oid).map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::SquashCommit), .create_snapshot(SnapshotDetails::new(OperationType::SquashCommit));
)?;
result result
}) })
} }
@ -1007,10 +988,9 @@ impl ControllerInner {
let result = let result =
super::update_commit_message(project_repository, branch_id, commit_oid, message) super::update_commit_message(project_repository, branch_id, commit_oid, message)
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::UpdateCommitMessage), .create_snapshot(SnapshotDetails::new(OperationType::UpdateCommitMessage));
)?;
result result
}) })
} }
@ -1089,10 +1069,9 @@ impl ControllerInner {
signing_key.as_ref(), signing_key.as_ref(),
) )
.map_err(Into::into); .map_err(Into::into);
snapshot::create( let _ = project_repository
project_repository.project(), .project()
SnapshotDetails::new(OperationType::MoveCommit), .create_snapshot(SnapshotDetails::new(OperationType::MoveCommit));
)?;
result result
}) })
} }

View File

@ -1,7 +1,8 @@
use crate::error::Error; use crate::error::Error;
use anyhow::Context; use anyhow::Context;
use gitbutler_core::{ use gitbutler_core::{
projects, projects::ProjectId, snapshots::entry::Snapshot, snapshots::snapshot, projects::{self, ProjectId},
snapshots::{entry::Snapshot, snapshot::Oplog},
}; };
use tauri::Manager; use tauri::Manager;
use tracing::instrument; use tracing::instrument;
@ -17,7 +18,7 @@ pub async fn list_snapshots(
.state::<projects::Controller>() .state::<projects::Controller>()
.get(&project_id) .get(&project_id)
.context("failed to get project")?; .context("failed to get project")?;
let snapshots = snapshot::list(&project, limit)?; let snapshots = project.list_snapshots(limit)?;
Ok(snapshots) Ok(snapshots)
} }
@ -32,6 +33,6 @@ pub async fn restore_snapshot(
.state::<projects::Controller>() .state::<projects::Controller>()
.get(&project_id) .get(&project_id)
.context("failed to get project")?; .context("failed to get project")?;
snapshot::restore(&project, sha)?; project.restore_snapshot(sha)?;
Ok(()) Ok(())
} }

View File

@ -10,7 +10,7 @@ use anyhow::{bail, Context, Result};
use gitbutler_core::projects::ProjectId; use gitbutler_core::projects::ProjectId;
use gitbutler_core::sessions::SessionId; use gitbutler_core::sessions::SessionId;
use gitbutler_core::snapshots::entry::{OperationType, SnapshotDetails, Trailer}; use gitbutler_core::snapshots::entry::{OperationType, SnapshotDetails, Trailer};
use gitbutler_core::snapshots::snapshot; use gitbutler_core::snapshots::snapshot::Oplog;
use gitbutler_core::virtual_branches::VirtualBranches; use gitbutler_core::virtual_branches::VirtualBranches;
use gitbutler_core::{ use gitbutler_core::{
assets, deltas, gb_repository, git, project_repository, projects, reader, sessions, users, assets, deltas, gb_repository, git, project_repository, projects, reader, sessions, users,
@ -305,7 +305,7 @@ impl Handler {
.projects .projects
.get(&project_id) .get(&project_id)
.context("failed to get project")?; .context("failed to get project")?;
let changed_lines = snapshot::changed_lines_count(&project)?; let changed_lines = project.lines_since_snapshot()?;
if changed_lines > project.snapshot_lines_threshold { if changed_lines > project.snapshot_lines_threshold {
let details = SnapshotDetails { let details = SnapshotDetails {
version: Default::default(), version: Default::default(),
@ -317,7 +317,7 @@ impl Handler {
value: paths, value: paths,
}], }],
}; };
snapshot::create(&project, details)?; project.create_snapshot(details)?;
} }
Ok(()) Ok(())
} }