mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-29 20:43:37 +03:00
refactor snapshotting as an Oplog trait
Implemented on Project
This commit is contained in:
parent
988291886b
commit
5f6ff257d1
@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use gitbutler_core::{projects::Project, snapshots::snapshot};
|
||||
use gitbutler_core::{projects::Project, snapshots::snapshot::Oplog};
|
||||
|
||||
use clap::{arg, Command};
|
||||
#[cfg(not(windows))]
|
||||
@ -49,7 +49,7 @@ fn main() -> Result<()> {
|
||||
|
||||
fn list_snapshots(repo_dir: &str) -> Result<()> {
|
||||
let project = project_from_path(repo_dir);
|
||||
let snapshots = snapshot::list(&project, 100)?;
|
||||
let snapshots = project.list_snapshots(100)?;
|
||||
for snapshot in snapshots {
|
||||
let ts = chrono::DateTime::from_timestamp(snapshot.created_at / 1000, 0);
|
||||
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<()> {
|
||||
let project = project_from_path(repo_dir);
|
||||
snapshot::restore(&project, snapshot_id.to_owned())?;
|
||||
project.restore_snapshot(snapshot_id.to_owned())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -16,257 +16,270 @@ use super::{
|
||||
|
||||
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.
|
||||
///
|
||||
/// If this is the first shapshot created, supporting structures are initialized:
|
||||
/// - The current oplog head is persisted in `.git/gitbutler/oplog.toml`.
|
||||
/// - A fake branch `gitbutler/target` is created and maintained in order to keep the oplog head reachable.
|
||||
///
|
||||
/// The snapshot tree contains:
|
||||
/// - The current state of the working directory under a subtree `workdir`.
|
||||
/// - 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`
|
||||
///
|
||||
/// 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 snapshot commit or None if snapshots are disabled.
|
||||
pub fn create(project: &Project, details: SnapshotDetails) -> Result<Option<String>> {
|
||||
if project.enable_snapshots.is_none() || project.enable_snapshots == Some(false) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let repo_path = project.path.as_path();
|
||||
let repo = git2::Repository::init(repo_path)?;
|
||||
|
||||
let vb_state = VirtualBranchesHandle::new(&project.gb_dir());
|
||||
let default_target_sha = vb_state.get_default_target()?.sha;
|
||||
|
||||
let oplog_state = OplogHandle::new(&project.gb_dir());
|
||||
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())?,
|
||||
};
|
||||
|
||||
// Create a blob out of `.git/gitbutler/virtual_branches.toml`
|
||||
let vb_path = repo_path
|
||||
.join(".git")
|
||||
.join("gitbutler")
|
||||
.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()))
|
||||
/// 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).
|
||||
pub trait Oplog {
|
||||
/// Creates a snapshot of the current state of the repository and virtual branches using the given label.
|
||||
///
|
||||
/// If this is the first shapshot created, supporting structures are initialized:
|
||||
/// - The current oplog head is persisted in `.git/gitbutler/oplog.toml`.
|
||||
/// - A fake branch `gitbutler/target` is created and maintained in order to keep the oplog head reachable.
|
||||
///
|
||||
/// The snapshot tree contains:
|
||||
/// - The current state of the working directory under a subtree `workdir`.
|
||||
/// - 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`
|
||||
///
|
||||
/// 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 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.
|
||||
/// An alternative way of retrieving the snapshots would be to manually the oplog head `git log <oplog_head>` available in `.git/gitbutler/oplog.toml`.
|
||||
///
|
||||
/// If there are no snapshots, an empty list is returned.
|
||||
fn list_snapshots(&self, limit: usize) -> Result<Vec<Snapshot>>;
|
||||
/// 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.
|
||||
fn restore_snapshot(&self, sha: String) -> Result<Option<String>>;
|
||||
/// Returns the number of lines of code (added plus removed) since the last snapshot. Includes untracked files.
|
||||
///
|
||||
/// If there are no snapshots, 0 is returned.
|
||||
fn lines_since_snapshot(&self) -> Result<usize>;
|
||||
}
|
||||
|
||||
/// Lists the snapshots that have been created for the given repository, up to the given limit.
|
||||
/// An alternative way of retrieving the snapshots would be to manually the oplog head `git log <oplog_head>` available in `.git/gitbutler/oplog.toml`.
|
||||
///
|
||||
/// If there are no snapshots, an empty list is returned.
|
||||
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;
|
||||
impl Oplog for Project {
|
||||
fn create_snapshot(&self, details: SnapshotDetails) -> Result<Option<String>> {
|
||||
if self.enable_snapshots.is_none() || self.enable_snapshots == Some(false) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let details = commit
|
||||
.message()
|
||||
.and_then(|msg| SnapshotDetails::from_str(msg).ok());
|
||||
let repo_path = self.path.as_path();
|
||||
let repo = git2::Repository::init(repo_path)?;
|
||||
|
||||
snapshots.push(Snapshot {
|
||||
id: commit_id.to_string(),
|
||||
details,
|
||||
created_at: commit.time().seconds() * 1000,
|
||||
});
|
||||
let vb_state = VirtualBranchesHandle::new(&self.gb_dir());
|
||||
let default_target_sha = vb_state.get_default_target()?.sha;
|
||||
|
||||
if snapshots.len() >= limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let oplog_state = OplogHandle::new(&self.gb_dir());
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
// Create a blob out of `.git/gitbutler/virtual_branches.toml`
|
||||
let vb_path = repo_path
|
||||
.join(".git")
|
||||
.join("gitbutler")
|
||||
.join("virtual_branches.toml"),
|
||||
vb_blob.content(),
|
||||
)?;
|
||||
.join("virtual_branches.toml");
|
||||
let vb_content = fs::read(vb_path)?;
|
||||
let vb_blob = repo.blob(&vb_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,
|
||||
}],
|
||||
};
|
||||
create(project, details)
|
||||
}
|
||||
// Create a tree out of the conflicts state if present
|
||||
let conflicts_tree = write_conflicts_tree(repo_path, &repo)?;
|
||||
|
||||
/// Returns the number of uncommitted lines of code (added plus removed) since the last snapshot. Included untracked files.
|
||||
pub fn changed_lines_count(project: &Project) -> Result<usize> {
|
||||
let repo_path = project.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)?;
|
||||
|
||||
// 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()?;
|
||||
|
||||
let oplog_state = OplogHandle::new(&project.gb_dir());
|
||||
let head_sha = oplog_state.get_oplog_head()?;
|
||||
if head_sha.is_none() {
|
||||
return Ok(0);
|
||||
// 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(
|
||||
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)?)?;
|
||||
let head_tree = commit.tree()?;
|
||||
fn list_snapshots(&self, limit: usize) -> Result<Vec<Snapshot>> {
|
||||
let repo_path = self.path.as_path();
|
||||
let repo = git2::Repository::init(repo_path)?;
|
||||
|
||||
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 oplog_state = OplogHandle::new(&self.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 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())
|
||||
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
|
||||
.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(
|
||||
@ -443,7 +456,9 @@ mod tests {
|
||||
std::fs::write(&base_merge_parent_path, "parent A").unwrap();
|
||||
|
||||
// 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
|
||||
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(&base_merge_parent_path).unwrap();
|
||||
// New snapshot with the conflicts removed
|
||||
let conflicts_removed_snapshot = create(
|
||||
&project,
|
||||
SnapshotDetails::new(OperationType::UpdateWorkspaceBase),
|
||||
)
|
||||
.unwrap();
|
||||
let conflicts_removed_snapshot = project
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::UpdateWorkspaceBase))
|
||||
.unwrap();
|
||||
|
||||
// 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_lines = std::fs::read_to_string(file_path).unwrap();
|
||||
@ -509,7 +524,9 @@ mod tests {
|
||||
assert_eq!(file_lines, "parent A");
|
||||
|
||||
// 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
|
||||
assert!(!&conflicts_path.exists());
|
||||
|
@ -2,7 +2,7 @@ use crate::{
|
||||
error::Error,
|
||||
snapshots::{
|
||||
entry::{OperationType, SnapshotDetails},
|
||||
snapshot,
|
||||
snapshot::Oplog,
|
||||
},
|
||||
};
|
||||
use std::{collections::HashMap, path::Path, sync::Arc};
|
||||
@ -473,10 +473,9 @@ impl ControllerInner {
|
||||
)
|
||||
.map_err(Into::into);
|
||||
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::CreateCommit),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::CreateCommit))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -524,10 +523,9 @@ impl ControllerInner {
|
||||
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let branch_id = super::create_virtual_branch(project_repository, create)?.id;
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::CreateBranch),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::CreateBranch))?;
|
||||
Ok(branch_id)
|
||||
})
|
||||
}
|
||||
@ -556,10 +554,9 @@ impl ControllerInner {
|
||||
signing_key.as_ref(),
|
||||
user,
|
||||
)?;
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::CreateBranch),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::CreateBranch))?;
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
@ -592,10 +589,9 @@ impl ControllerInner {
|
||||
let project = self.projects.get(project_id)?;
|
||||
let project_repository = project_repository::Repository::open(&project)?;
|
||||
let result = super::set_base_branch(&project_repository, target_branch)?;
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::SetBaseBranch),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::SetBaseBranch))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@ -625,10 +621,9 @@ impl ControllerInner {
|
||||
user,
|
||||
)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::MergeUpstream),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::MergeUpstream))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -650,10 +645,9 @@ impl ControllerInner {
|
||||
|
||||
let result = super::update_base_branch(project_repository, user, signing_key.as_ref())
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::UpdateWorkspaceBase),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::UpdateWorkspaceBase))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -682,7 +676,7 @@ impl ControllerInner {
|
||||
SnapshotDetails::new(OperationType::GenericBranchUpdate)
|
||||
};
|
||||
super::update_branch(project_repository, branch_update)?;
|
||||
snapshot::create(project_repository.project(), details)?;
|
||||
project_repository.project().create_snapshot(details)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@ -696,10 +690,9 @@ impl ControllerInner {
|
||||
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
super::delete_branch(project_repository, branch_id)?;
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::DeleteBranch),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::DeleteBranch))?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@ -726,10 +719,9 @@ impl ControllerInner {
|
||||
let result =
|
||||
super::apply_branch(project_repository, branch_id, signing_key.as_ref(), user)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::ApplyBranch),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::ApplyBranch))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -744,10 +736,9 @@ impl ControllerInner {
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result =
|
||||
super::unapply_ownership(project_repository, ownership).map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::DiscardHunk),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::DiscardHunk))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -761,10 +752,9 @@ impl ControllerInner {
|
||||
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result = super::reset_files(project_repository, ownership).map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::DiscardFile),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::DiscardFile))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -781,10 +771,9 @@ impl ControllerInner {
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result = super::amend(project_repository, branch_id, commit_oid, ownership)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::AmendCommit),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::AmendCommit))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -808,10 +797,9 @@ impl ControllerInner {
|
||||
ownership,
|
||||
)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::MoveCommitFile),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::MoveCommitFile))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -827,10 +815,9 @@ impl ControllerInner {
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result =
|
||||
super::undo_commit(project_repository, branch_id, commit_oid).map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::UndoCommit),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::UndoCommit))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -848,10 +835,9 @@ impl ControllerInner {
|
||||
let result =
|
||||
super::insert_blank_commit(project_repository, branch_id, commit_oid, user, offset)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::InsertBlankCommit),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::InsertBlankCommit))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -868,10 +854,9 @@ impl ControllerInner {
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result = super::reorder_commit(project_repository, branch_id, commit_oid, offset)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::ReorderCommit),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::ReorderCommit))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -887,10 +872,9 @@ impl ControllerInner {
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result = super::reset_branch(project_repository, branch_id, target_commit_oid)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::UndoCommit),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::UndoCommit))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -906,10 +890,9 @@ impl ControllerInner {
|
||||
let result = super::unapply_branch(project_repository, branch_id)
|
||||
.map(|_| ())
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::UnapplyBranch),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::UnapplyBranch))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -949,10 +932,9 @@ impl ControllerInner {
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result =
|
||||
super::cherry_pick(project_repository, branch_id, commit_oid).map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::CherryPick),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::CherryPick))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -987,10 +969,9 @@ impl ControllerInner {
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result =
|
||||
super::squash(project_repository, branch_id, commit_oid).map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::SquashCommit),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::SquashCommit))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -1007,10 +988,9 @@ impl ControllerInner {
|
||||
let result =
|
||||
super::update_commit_message(project_repository, branch_id, commit_oid, message)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::UpdateCommitMessage),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::UpdateCommitMessage))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
@ -1089,10 +1069,9 @@ impl ControllerInner {
|
||||
signing_key.as_ref(),
|
||||
)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::MoveCommit),
|
||||
)?;
|
||||
project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationType::MoveCommit))?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
use crate::error::Error;
|
||||
use anyhow::Context;
|
||||
use gitbutler_core::{
|
||||
projects, projects::ProjectId, snapshots::entry::Snapshot, snapshots::snapshot,
|
||||
projects::{self, ProjectId},
|
||||
snapshots::{entry::Snapshot, snapshot::Oplog},
|
||||
};
|
||||
use tauri::Manager;
|
||||
use tracing::instrument;
|
||||
@ -17,7 +18,7 @@ pub async fn list_snapshots(
|
||||
.state::<projects::Controller>()
|
||||
.get(&project_id)
|
||||
.context("failed to get project")?;
|
||||
let snapshots = snapshot::list(&project, limit)?;
|
||||
let snapshots = project.list_snapshots(limit)?;
|
||||
Ok(snapshots)
|
||||
}
|
||||
|
||||
@ -32,6 +33,6 @@ pub async fn restore_snapshot(
|
||||
.state::<projects::Controller>()
|
||||
.get(&project_id)
|
||||
.context("failed to get project")?;
|
||||
snapshot::restore(&project, sha)?;
|
||||
project.restore_snapshot(sha)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use anyhow::{bail, Context, Result};
|
||||
use gitbutler_core::projects::ProjectId;
|
||||
use gitbutler_core::sessions::SessionId;
|
||||
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::{
|
||||
assets, deltas, gb_repository, git, project_repository, projects, reader, sessions, users,
|
||||
@ -305,7 +305,7 @@ impl Handler {
|
||||
.projects
|
||||
.get(&project_id)
|
||||
.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 {
|
||||
let details = SnapshotDetails {
|
||||
version: Default::default(),
|
||||
@ -317,7 +317,7 @@ impl Handler {
|
||||
value: paths,
|
||||
}],
|
||||
};
|
||||
snapshot::create(&project, details)?;
|
||||
project.create_snapshot(details)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user