From a2682e25357faeb42e32a4473c7dd21dc1112070 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 10:37:24 +0200 Subject: [PATCH 01/15] implement undo stack snapshotting --- crates/gitbutler-core/src/lib.rs | 1 + crates/gitbutler-core/src/snapshots/mod.rs | 3 + .../gitbutler-core/src/snapshots/snapshot.rs | 145 ++++++++++++++++++ crates/gitbutler-core/src/snapshots/state.rs | 81 ++++++++++ crates/gitbutler-tauri/src/lib.rs | 1 + crates/gitbutler-tauri/src/main.rs | 7 +- crates/gitbutler-tauri/src/snapshots.rs | 52 +++++++ 7 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 crates/gitbutler-core/src/snapshots/mod.rs create mode 100644 crates/gitbutler-core/src/snapshots/snapshot.rs create mode 100644 crates/gitbutler-core/src/snapshots/state.rs create mode 100644 crates/gitbutler-tauri/src/snapshots.rs diff --git a/crates/gitbutler-core/src/lib.rs b/crates/gitbutler-core/src/lib.rs index 4f86606f6..83161034e 100644 --- a/crates/gitbutler-core/src/lib.rs +++ b/crates/gitbutler-core/src/lib.rs @@ -30,6 +30,7 @@ pub mod project_repository; pub mod projects; pub mod reader; pub mod sessions; +pub mod snapshots; pub mod ssh; pub mod storage; pub mod types; diff --git a/crates/gitbutler-core/src/snapshots/mod.rs b/crates/gitbutler-core/src/snapshots/mod.rs new file mode 100644 index 000000000..bea2d953b --- /dev/null +++ b/crates/gitbutler-core/src/snapshots/mod.rs @@ -0,0 +1,3 @@ +pub mod reflog; +pub mod snapshot; +mod state; diff --git a/crates/gitbutler-core/src/snapshots/snapshot.rs b/crates/gitbutler-core/src/snapshots/snapshot.rs new file mode 100644 index 000000000..ba3e20966 --- /dev/null +++ b/crates/gitbutler-core/src/snapshots/snapshot.rs @@ -0,0 +1,145 @@ +use anyhow::Result; +use serde::Serialize; + +use crate::{projects::Project, virtual_branches::VirtualBranchesHandle}; + +use super::state::OplogHandle; + +#[derive(Debug, PartialEq, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SnapshotEntry { + pub sha: String, + pub label: String, + pub created_at: i64, // milliseconds since epoch +} + +pub fn create(project: Project, label: String) -> Result { + let repo_path = project.path.as_path(); + let repo = git2::Repository::init(repo_path)?; + + let oplog_state = OplogHandle::new(&project.gb_dir()); + let oplog_head_commit = match oplog_state.get_oplog_head()? { + Some(head_sha) => repo.find_commit(git2::Oid::from_str(&head_sha)?)?, + // This is the first snapshot - use the default target as starting point + None => { + let vb_state = VirtualBranchesHandle::new(&project.gb_dir()); + repo.find_commit(vb_state.get_default_target()?.sha.into())? + } + }; + + // Copy virtual_branches.rs to the project root so that we snapshot it + std::fs::copy( + repo_path.join(".git/gitbutler/virtual_branches.toml"), + repo_path.join("virtual_branches.toml"), + )?; + + // 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 tree = repo.find_tree(tree_id)?; + + // Construct a new commit + let signature = repo.signature()?; + let new_commit_oid = repo.commit( + None, + &signature, + &signature, + &label, + &tree, + &[&oplog_head_commit], + )?; + + // Remove the copied virtual_branches.rs + std::fs::remove_file(project.path.join("virtual_branches.toml"))?; + + // 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())?; + // TODO: update .git/logs/gitbutler/target + + Ok(new_commit_oid.to_string()) +} + +pub fn list(project: Project, limit: usize) -> Result> { + 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(); + + // TODO: how do we know when to stop? + for commit_id in revwalk { + let commit_id = commit_id?; + let commit = repo.find_commit(commit_id)?; + + if commit.parent_count() > 1 { + break; + } + snapshots.push(SnapshotEntry { + sha: commit_id.to_string(), + label: commit.summary().unwrap_or_default().to_string(), + created_at: commit.time().seconds() * 1000, + }); + + if snapshots.len() >= limit { + break; + } + } + + Ok(snapshots) +} + +pub fn restore(project: Project, sha: String) -> Result { + let repo_path = project.path.as_path(); + let repo = git2::Repository::init(repo_path)?; + + let commit = repo.find_commit(git2::Oid::from_str(&sha)?)?; + let tree = commit.tree()?; + + // Define the checkout builder + let mut checkout_builder = git2::build::CheckoutBuilder::new(); + checkout_builder.force(); + // Checkout the tree + repo.checkout_tree(tree.as_object(), Some(&mut checkout_builder))?; + + // mv virtual_branches.toml from project root to .git/gitbutler + std::fs::rename( + repo_path.join("virtual_branches.toml"), + repo_path.join(".git/gitbutler/virtual_branches.toml"), + )?; + + // create new snapshot + let label = format!( + "Restored from snapshot {}", + commit.message().unwrap_or(&sha) + ); + let new_sha = create(project, label)?; + + Ok(new_sha) +} diff --git a/crates/gitbutler-core/src/snapshots/state.rs b/crates/gitbutler-core/src/snapshots/state.rs new file mode 100644 index 000000000..90b83de51 --- /dev/null +++ b/crates/gitbutler-core/src/snapshots/state.rs @@ -0,0 +1,81 @@ +use crate::storage; +use anyhow::Result; +use gix::tempfile::{AutoRemove, ContainingDirectory}; +use std::{ + fs::File, + io::{Read, Write}, + path::{Path, PathBuf}, +}; + +use serde::{Deserialize, Serialize}; + +/// This tracks the head of the oplog, persisted in oplog.toml. +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Oplog { + /// This is the sha of the last oplog commit + pub head_sha: Option, +} + +pub struct OplogHandle { + /// The path to the file containing the oplog head state. + file_path: PathBuf, +} + +impl OplogHandle { + /// Creates a new concurrency-safe handle to the state of the oplog. + pub fn new(base_path: &Path) -> Self { + let file_path = base_path.join("oplog.toml"); + Self { file_path } + } + + /// Persists the oplog head for the given repository. + /// + /// Errors if the file cannot be read or written. + pub fn set_oplog_head(&self, sha: String) -> Result<()> { + let mut oplog = self.read_file()?; + oplog.head_sha = Some(sha); + self.write_file(&oplog)?; + Ok(()) + } + + /// Gets the oplog head sha for the given repository. + /// + /// Errors if the file cannot be read or written. + pub fn get_oplog_head(&self) -> anyhow::Result> { + let oplog = self.read_file()?; + Ok(oplog.head_sha) + } + + /// Reads and parses the state file. + /// + /// If the file does not exist, it will be created. + fn read_file(&self) -> Result { + if !self.file_path.exists() { + return Ok(Oplog::default()); + } + let mut file: File = File::open(self.file_path.as_path())?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let oplog: Oplog = + toml::from_str(&contents).map_err(|e| crate::reader::Error::ParseError { + path: self.file_path.clone(), + source: e, + })?; + Ok(oplog) + } + + fn write_file(&self, oplog: &Oplog) -> anyhow::Result<()> { + write(self.file_path.as_path(), oplog) + } +} + +fn write>(file_path: P, oplog: &Oplog) -> anyhow::Result<()> { + let contents = toml::to_string(&oplog)?; + let mut temp_file = gix::tempfile::new( + file_path.as_ref().parent().unwrap(), + ContainingDirectory::Exists, + AutoRemove::Tempfile, + )?; + temp_file.write_all(contents.as_bytes())?; + Ok(storage::persist_tempfile(temp_file, file_path)?) +} diff --git a/crates/gitbutler-tauri/src/lib.rs b/crates/gitbutler-tauri/src/lib.rs index f00172b11..5436cc6a4 100644 --- a/crates/gitbutler-tauri/src/lib.rs +++ b/crates/gitbutler-tauri/src/lib.rs @@ -28,6 +28,7 @@ pub mod keys; pub mod projects; pub mod sentry; pub mod sessions; +pub mod snapshots; pub mod users; pub mod virtual_branches; pub mod zip; diff --git a/crates/gitbutler-tauri/src/main.rs b/crates/gitbutler-tauri/src/main.rs index bffb0f094..df82705f4 100644 --- a/crates/gitbutler-tauri/src/main.rs +++ b/crates/gitbutler-tauri/src/main.rs @@ -18,8 +18,8 @@ use std::path::PathBuf; use anyhow::Context; use gitbutler_core::{assets, database, git, storage}; use gitbutler_tauri::{ - app, askpass, commands, deltas, github, keys, logs, menu, projects, sentry, sessions, users, - virtual_branches, watcher, zip, + app, askpass, commands, deltas, github, keys, logs, menu, projects, sentry, sessions, + snapshots, users, virtual_branches, watcher, zip, }; use tauri::{generate_context, Manager, Wry}; use tauri_plugin_log::LogTarget; @@ -260,6 +260,9 @@ fn main() { virtual_branches::commands::squash_branch_commit, virtual_branches::commands::fetch_from_target, virtual_branches::commands::move_commit, + snapshots::create_snapshot, + snapshots::list_snapshots, + snapshots::restore_snapshot, menu::menu_item_set_enabled, keys::commands::get_public_key, github::commands::init_device_oauth, diff --git a/crates/gitbutler-tauri/src/snapshots.rs b/crates/gitbutler-tauri/src/snapshots.rs new file mode 100644 index 000000000..b900d7339 --- /dev/null +++ b/crates/gitbutler-tauri/src/snapshots.rs @@ -0,0 +1,52 @@ +use crate::error::Error; +use anyhow::Context; +use gitbutler_core::{ + projects, projects::ProjectId, snapshots::snapshot, snapshots::snapshot::SnapshotEntry, +}; +use tauri::Manager; +use tracing::instrument; + +#[tauri::command(async)] +#[instrument(skip(handle), err(Debug))] +pub async fn create_snapshot( + handle: tauri::AppHandle, + project_id: ProjectId, + label: String, +) -> Result { + let project = handle + .state::() + .get(&project_id) + .context("failed to get project")?; + let snapshot_id = snapshot::create(project, label)?; + Ok(snapshot_id) +} + +#[tauri::command(async)] +#[instrument(skip(handle), err(Debug))] +pub async fn list_snapshots( + handle: tauri::AppHandle, + project_id: ProjectId, + limit: usize, +) -> Result, Error> { + let project = handle + .state::() + .get(&project_id) + .context("failed to get project")?; + let snapshots = snapshot::list(project, limit)?; + Ok(snapshots) +} + +#[tauri::command(async)] +#[instrument(skip(handle), err(Debug))] +pub async fn restore_snapshot( + handle: tauri::AppHandle, + project_id: ProjectId, + sha: String, +) -> Result { + let project = handle + .state::() + .get(&project_id) + .context("failed to get project")?; + let snapshot_id = snapshot::restore(project, sha)?; + Ok(snapshot_id) +} From ca4f79f7fff597a2eb8c2e0ef46e1440f7fe0bf4 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 13:50:27 +0200 Subject: [PATCH 02/15] reflog entry for the snapshots --- crates/gitbutler-core/src/snapshots/reflog.rs | 85 +++++++++++++++++++ .../gitbutler-core/src/snapshots/snapshot.rs | 14 +-- 2 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 crates/gitbutler-core/src/snapshots/reflog.rs diff --git a/crates/gitbutler-core/src/snapshots/reflog.rs b/crates/gitbutler-core/src/snapshots/reflog.rs new file mode 100644 index 000000000..15fa314f9 --- /dev/null +++ b/crates/gitbutler-core/src/snapshots/reflog.rs @@ -0,0 +1,85 @@ +use crate::storage; +use anyhow::Result; +use gix::tempfile::{AutoRemove, ContainingDirectory}; +use itertools::Itertools; +use std::{io::Write, path::PathBuf}; + +use crate::projects::Project; + +pub struct SnapshotsReference { + file_path: PathBuf, +} + +impl SnapshotsReference { + pub fn new(project: Project, target_sha: &str) -> Result { + let repo_path = project.path.as_path(); + let reflog_file_path = repo_path + .join(".git") + .join("logs") + .join("refs") + .join("heads") + .join("gitbutler") + .join("target"); + + if !reflog_file_path.exists() { + let repo = git2::Repository::init(repo_path)?; + let commit = repo.find_commit(git2::Oid::from_str(target_sha)?)?; + repo.branch("gitbutler/target", &commit, false)?; + } + + if !reflog_file_path.exists() { + return Err(anyhow::anyhow!( + "Could not create gitbutler/target which is needed for undo snapshotting" + )); + } + + Ok(Self { + file_path: reflog_file_path, + }) + } + + pub fn set_target_ref(&self, sha: &str) -> Result<()> { + // 0000000000000000000000000000000000000000 82873b54925ab268e9949557f28d070d388e7774 Kiril Videlov 1714037434 +0200 branch: Created from 82873b54925ab268e9949557f28d070d388e7774 + let content = std::fs::read_to_string(&self.file_path)?; + let mut lines = content.lines().collect::>(); + let mut first_line = lines.remove(0).split_whitespace().collect_vec(); + let len = first_line.len(); + first_line[1] = sha; + first_line[len - 1] = sha; + let binding = first_line.join(" "); + lines[0] = &binding; + let content = format!("{}\n", lines.join("\n")); + write(&self.file_path, &content) + } + + pub fn set_oplog_ref(&self, sha: &str) -> Result<()> { + // 82873b54925ab268e9949557f28d070d388e7774 7e8eab472636a26611214bebea7d6b79c971fb8b Kiril Videlov 1714044124 +0200 reset: moving to 7e8eab472636a26611214bebea7d6b79c971fb8b + let content = std::fs::read_to_string(&self.file_path)?; + let first_line = content.lines().collect::>().remove(0); + + let target_ref = first_line.split_whitespace().collect_vec()[1]; + let the_rest = first_line.split_whitespace().collect_vec()[2..].join(" "); + let the_rest = the_rest.replace("branch", " reset"); + let mut the_rest_split = the_rest.split(':').collect_vec(); + let new_msg = format!(" moving to {}", sha); + the_rest_split[1] = &new_msg; + let the_rest = the_rest_split.join(":"); + + let second_line = [target_ref, sha, &the_rest].join(" "); + + let content = format!("{}\n", [first_line, &second_line].join("\n")); + write(&self.file_path, &content) + } +} + +fn write(file_path: &PathBuf, content: &str) -> Result<()> { + let mut temp_file = gix::tempfile::new( + file_path.parent().unwrap(), + ContainingDirectory::Exists, + AutoRemove::Tempfile, + )?; + temp_file.write_all(content.as_bytes())?; + storage::persist_tempfile(temp_file, file_path)?; + + Ok(()) +} diff --git a/crates/gitbutler-core/src/snapshots/snapshot.rs b/crates/gitbutler-core/src/snapshots/snapshot.rs index ba3e20966..4da3d6028 100644 --- a/crates/gitbutler-core/src/snapshots/snapshot.rs +++ b/crates/gitbutler-core/src/snapshots/snapshot.rs @@ -3,7 +3,7 @@ use serde::Serialize; use crate::{projects::Project, virtual_branches::VirtualBranchesHandle}; -use super::state::OplogHandle; +use super::{reflog::SnapshotsReference, state::OplogHandle}; #[derive(Debug, PartialEq, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -17,14 +17,14 @@ pub fn create(project: Project, label: String) -> Result { 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) => repo.find_commit(git2::Oid::from_str(&head_sha)?)?, // This is the first snapshot - use the default target as starting point - None => { - let vb_state = VirtualBranchesHandle::new(&project.gb_dir()); - repo.find_commit(vb_state.get_default_target()?.sha.into())? - } + None => repo.find_commit(default_target_sha.into())?, }; // Copy virtual_branches.rs to the project root so that we snapshot it @@ -69,7 +69,9 @@ pub fn create(project: Project, label: String) -> Result { )?; oplog_state.set_oplog_head(new_commit_oid.to_string())?; - // TODO: update .git/logs/gitbutler/target + + let reflog_hack = SnapshotsReference::new(project, &default_target_sha.to_string())?; + reflog_hack.set_oplog_ref(&new_commit_oid.to_string())?; Ok(new_commit_oid.to_string()) } From 0911212a9c9586ae41a6ed421a309c25c22551e1 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 14:21:31 +0200 Subject: [PATCH 03/15] handle commit not found --- crates/gitbutler-core/src/snapshots/snapshot.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/gitbutler-core/src/snapshots/snapshot.rs b/crates/gitbutler-core/src/snapshots/snapshot.rs index 4da3d6028..1175f6ddf 100644 --- a/crates/gitbutler-core/src/snapshots/snapshot.rs +++ b/crates/gitbutler-core/src/snapshots/snapshot.rs @@ -22,7 +22,10 @@ pub fn create(project: Project, label: String) -> Result { let oplog_state = OplogHandle::new(&project.gb_dir()); let oplog_head_commit = match oplog_state.get_oplog_head()? { - Some(head_sha) => repo.find_commit(git2::Oid::from_str(&head_sha)?)?, + 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())?, }; From 3d04c7fc5b08385492b8e867c46f14cc18326198 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 14:22:31 +0200 Subject: [PATCH 04/15] set target ref on snapshot --- crates/gitbutler-core/src/snapshots/snapshot.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/gitbutler-core/src/snapshots/snapshot.rs b/crates/gitbutler-core/src/snapshots/snapshot.rs index 1175f6ddf..e78ca0db5 100644 --- a/crates/gitbutler-core/src/snapshots/snapshot.rs +++ b/crates/gitbutler-core/src/snapshots/snapshot.rs @@ -74,6 +74,7 @@ pub fn create(project: Project, label: String) -> Result { oplog_state.set_oplog_head(new_commit_oid.to_string())?; let reflog_hack = SnapshotsReference::new(project, &default_target_sha.to_string())?; + reflog_hack.set_target_ref(&default_target_sha.to_string())?; reflog_hack.set_oplog_ref(&new_commit_oid.to_string())?; Ok(new_commit_oid.to_string()) From 5e395a370041f43f0f9b8be3a60f9e1a2d56acce Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 14:23:06 +0200 Subject: [PATCH 05/15] snapshot reflog doesnt need to be exposed --- crates/gitbutler-core/src/snapshots/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gitbutler-core/src/snapshots/mod.rs b/crates/gitbutler-core/src/snapshots/mod.rs index bea2d953b..9a3bc151c 100644 --- a/crates/gitbutler-core/src/snapshots/mod.rs +++ b/crates/gitbutler-core/src/snapshots/mod.rs @@ -1,3 +1,3 @@ -pub mod reflog; +mod reflog; pub mod snapshot; mod state; From 753953a7d5aa78487eee42eaa23a746701032fc1 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 14:36:01 +0200 Subject: [PATCH 06/15] remove tauri endpoint for creating snapshots --- .../gitbutler-core/src/snapshots/snapshot.rs | 10 ++++----- crates/gitbutler-tauri/src/main.rs | 1 - crates/gitbutler-tauri/src/snapshots.rs | 21 +++---------------- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/crates/gitbutler-core/src/snapshots/snapshot.rs b/crates/gitbutler-core/src/snapshots/snapshot.rs index e78ca0db5..ec278c046 100644 --- a/crates/gitbutler-core/src/snapshots/snapshot.rs +++ b/crates/gitbutler-core/src/snapshots/snapshot.rs @@ -13,7 +13,7 @@ pub struct SnapshotEntry { pub created_at: i64, // milliseconds since epoch } -pub fn create(project: Project, label: String) -> Result { +pub fn create(project: Project, label: String) -> Result<()> { let repo_path = project.path.as_path(); let repo = git2::Repository::init(repo_path)?; @@ -77,7 +77,7 @@ pub fn create(project: Project, label: String) -> Result { reflog_hack.set_target_ref(&default_target_sha.to_string())?; reflog_hack.set_oplog_ref(&new_commit_oid.to_string())?; - Ok(new_commit_oid.to_string()) + Ok(()) } pub fn list(project: Project, limit: usize) -> Result> { @@ -121,7 +121,7 @@ pub fn list(project: Project, limit: usize) -> Result> { Ok(snapshots) } -pub fn restore(project: Project, sha: String) -> Result { +pub fn restore(project: Project, sha: String) -> Result<()> { let repo_path = project.path.as_path(); let repo = git2::Repository::init(repo_path)?; @@ -145,7 +145,7 @@ pub fn restore(project: Project, sha: String) -> Result { "Restored from snapshot {}", commit.message().unwrap_or(&sha) ); - let new_sha = create(project, label)?; + create(project, label)?; - Ok(new_sha) + Ok(()) } diff --git a/crates/gitbutler-tauri/src/main.rs b/crates/gitbutler-tauri/src/main.rs index df82705f4..66ddaf9cd 100644 --- a/crates/gitbutler-tauri/src/main.rs +++ b/crates/gitbutler-tauri/src/main.rs @@ -260,7 +260,6 @@ fn main() { virtual_branches::commands::squash_branch_commit, virtual_branches::commands::fetch_from_target, virtual_branches::commands::move_commit, - snapshots::create_snapshot, snapshots::list_snapshots, snapshots::restore_snapshot, menu::menu_item_set_enabled, diff --git a/crates/gitbutler-tauri/src/snapshots.rs b/crates/gitbutler-tauri/src/snapshots.rs index b900d7339..3251c95c6 100644 --- a/crates/gitbutler-tauri/src/snapshots.rs +++ b/crates/gitbutler-tauri/src/snapshots.rs @@ -6,21 +6,6 @@ use gitbutler_core::{ use tauri::Manager; use tracing::instrument; -#[tauri::command(async)] -#[instrument(skip(handle), err(Debug))] -pub async fn create_snapshot( - handle: tauri::AppHandle, - project_id: ProjectId, - label: String, -) -> Result { - let project = handle - .state::() - .get(&project_id) - .context("failed to get project")?; - let snapshot_id = snapshot::create(project, label)?; - Ok(snapshot_id) -} - #[tauri::command(async)] #[instrument(skip(handle), err(Debug))] pub async fn list_snapshots( @@ -42,11 +27,11 @@ pub async fn restore_snapshot( handle: tauri::AppHandle, project_id: ProjectId, sha: String, -) -> Result { +) -> Result<(), Error> { let project = handle .state::() .get(&project_id) .context("failed to get project")?; - let snapshot_id = snapshot::restore(project, sha)?; - Ok(snapshot_id) + snapshot::restore(project, sha)?; + Ok(()) } From 432aeeaf2a626aaf7525509b9db8a9f6fb12f50e Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 14:44:45 +0200 Subject: [PATCH 07/15] feature flag for snapshot creation --- crates/gitbutler-core/src/projects/project.rs | 2 ++ crates/gitbutler-core/src/snapshots/snapshot.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/crates/gitbutler-core/src/projects/project.rs b/crates/gitbutler-core/src/projects/project.rs index 460640f61..f085dc5a0 100644 --- a/crates/gitbutler-core/src/projects/project.rs +++ b/crates/gitbutler-core/src/projects/project.rs @@ -82,6 +82,8 @@ pub struct Project { pub project_data_last_fetch: Option, #[serde(default)] pub omit_certificate_check: Option, + #[serde(default)] + pub enable_snapshots: Option, } impl AsRef for Project { diff --git a/crates/gitbutler-core/src/snapshots/snapshot.rs b/crates/gitbutler-core/src/snapshots/snapshot.rs index ec278c046..875a96c3b 100644 --- a/crates/gitbutler-core/src/snapshots/snapshot.rs +++ b/crates/gitbutler-core/src/snapshots/snapshot.rs @@ -14,6 +14,10 @@ pub struct SnapshotEntry { } pub fn create(project: Project, label: String) -> Result<()> { + if let Some(false) = project.enable_snapshots { + return Ok(()); + } + let repo_path = project.path.as_path(); let repo = git2::Repository::init(repo_path)?; From b3c05b794861a957322241b365cfde8b0737e6dc Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 14:56:03 +0200 Subject: [PATCH 08/15] fix signatures --- crates/gitbutler-core/src/snapshots/reflog.rs | 2 +- crates/gitbutler-core/src/snapshots/snapshot.rs | 8 ++++---- crates/gitbutler-tauri/src/snapshots.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/gitbutler-core/src/snapshots/reflog.rs b/crates/gitbutler-core/src/snapshots/reflog.rs index 15fa314f9..024e66504 100644 --- a/crates/gitbutler-core/src/snapshots/reflog.rs +++ b/crates/gitbutler-core/src/snapshots/reflog.rs @@ -11,7 +11,7 @@ pub struct SnapshotsReference { } impl SnapshotsReference { - pub fn new(project: Project, target_sha: &str) -> Result { + pub fn new(project: &Project, target_sha: &str) -> Result { let repo_path = project.path.as_path(); let reflog_file_path = repo_path .join(".git") diff --git a/crates/gitbutler-core/src/snapshots/snapshot.rs b/crates/gitbutler-core/src/snapshots/snapshot.rs index 875a96c3b..9f1514cda 100644 --- a/crates/gitbutler-core/src/snapshots/snapshot.rs +++ b/crates/gitbutler-core/src/snapshots/snapshot.rs @@ -13,7 +13,7 @@ pub struct SnapshotEntry { pub created_at: i64, // milliseconds since epoch } -pub fn create(project: Project, label: String) -> Result<()> { +pub fn create(project: &Project, label: &str) -> Result<()> { if let Some(false) = project.enable_snapshots { return Ok(()); } @@ -55,7 +55,7 @@ pub fn create(project: Project, label: String) -> Result<()> { None, &signature, &signature, - &label, + label, &tree, &[&oplog_head_commit], )?; @@ -125,7 +125,7 @@ pub fn list(project: Project, limit: usize) -> Result> { Ok(snapshots) } -pub fn restore(project: Project, sha: String) -> Result<()> { +pub fn restore(project: &Project, sha: String) -> Result<()> { let repo_path = project.path.as_path(); let repo = git2::Repository::init(repo_path)?; @@ -149,7 +149,7 @@ pub fn restore(project: Project, sha: String) -> Result<()> { "Restored from snapshot {}", commit.message().unwrap_or(&sha) ); - create(project, label)?; + create(project, &label)?; Ok(()) } diff --git a/crates/gitbutler-tauri/src/snapshots.rs b/crates/gitbutler-tauri/src/snapshots.rs index 3251c95c6..8cc0977fe 100644 --- a/crates/gitbutler-tauri/src/snapshots.rs +++ b/crates/gitbutler-tauri/src/snapshots.rs @@ -32,6 +32,6 @@ pub async fn restore_snapshot( .state::() .get(&project_id) .context("failed to get project")?; - snapshot::restore(project, sha)?; + snapshot::restore(&project, sha)?; Ok(()) } From 9a07f502445930a942111109983b78e5f308de0b Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 15:31:34 +0200 Subject: [PATCH 09/15] fix: correctly handle the case of the feature flag being absent --- crates/gitbutler-core/src/snapshots/snapshot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gitbutler-core/src/snapshots/snapshot.rs b/crates/gitbutler-core/src/snapshots/snapshot.rs index 9f1514cda..f326e694c 100644 --- a/crates/gitbutler-core/src/snapshots/snapshot.rs +++ b/crates/gitbutler-core/src/snapshots/snapshot.rs @@ -14,7 +14,7 @@ pub struct SnapshotEntry { } pub fn create(project: &Project, label: &str) -> Result<()> { - if let Some(false) = project.enable_snapshots { + if project.enable_snapshots.is_none() || project.enable_snapshots == Some(false) { return Ok(()); } From 4c92caafd4b59d7be482dc9f607196c4c87c75d7 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 15:31:51 +0200 Subject: [PATCH 10/15] create simple snapshots on branch operations --- .../src/virtual_branches/controller.rs | 90 +++++++++++++------ 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/crates/gitbutler-core/src/virtual_branches/controller.rs b/crates/gitbutler-core/src/virtual_branches/controller.rs index 4f2e9660f..935f7a20b 100644 --- a/crates/gitbutler-core/src/virtual_branches/controller.rs +++ b/crates/gitbutler-core/src/virtual_branches/controller.rs @@ -1,4 +1,4 @@ -use crate::error::Error; +use crate::{error::Error, snapshots::snapshot}; use std::{collections::HashMap, path::Path, sync::Arc}; use anyhow::Context; @@ -397,7 +397,7 @@ impl ControllerInner { }) .transpose()?; - super::commit( + let result = super::commit( project_repository, branch_id, message, @@ -406,7 +406,9 @@ impl ControllerInner { user, run_hooks, ) - .map_err(Into::into) + .map_err(Into::into); + snapshot::create(project_repository.project(), "created commit")?; + result }) } @@ -453,6 +455,7 @@ 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(), "created branch")?; Ok(branch_id) }) } @@ -475,13 +478,14 @@ impl ControllerInner { .context("failed to get private key") }) .transpose()?; - - Ok(super::create_virtual_branch_from_branch( + let result = super::create_virtual_branch_from_branch( project_repository, branch, signing_key.as_ref(), user, - )?) + )?; + snapshot::create(project_repository.project(), "created branch")?; + Ok(result) }) } @@ -512,8 +516,9 @@ impl ControllerInner { ) -> Result { let project = self.projects.get(project_id)?; let project_repository = project_repository::Repository::open(&project)?; - - Ok(super::set_base_branch(&project_repository, target_branch)?) + let result = super::set_base_branch(&project_repository, target_branch)?; + snapshot::create(project_repository.project(), "set base branch")?; + Ok(result) } pub async fn merge_virtual_branch_upstream( @@ -535,13 +540,15 @@ impl ControllerInner { }) .transpose()?; - super::merge_virtual_branch_upstream( + let result = super::merge_virtual_branch_upstream( project_repository, branch_id, signing_key.as_ref(), user, ) - .map_err(Into::into) + .map_err(Into::into); + snapshot::create(project_repository.project(), "merged upstream")?; + result }) } @@ -560,8 +567,10 @@ impl ControllerInner { }) .transpose()?; - super::update_base_branch(project_repository, user, signing_key.as_ref()) - .map_err(Into::into) + let result = super::update_base_branch(project_repository, user, signing_key.as_ref()) + .map_err(Into::into); + snapshot::create(project_repository.project(), "updated base branch")?; + result }) } @@ -574,6 +583,7 @@ impl ControllerInner { self.with_verify_branch(project_id, |project_repository, _| { super::update_branch(project_repository, branch_update)?; + snapshot::create(project_repository.project(), "updated branch")?; Ok(()) }) } @@ -587,6 +597,7 @@ impl ControllerInner { self.with_verify_branch(project_id, |project_repository, _| { super::delete_branch(project_repository, branch_id)?; + snapshot::create(project_repository.project(), "deleted branch")?; Ok(()) }) } @@ -610,8 +621,11 @@ impl ControllerInner { }) .transpose()?; - super::apply_branch(project_repository, branch_id, signing_key.as_ref(), user) - .map_err(Into::into) + let result = + super::apply_branch(project_repository, branch_id, signing_key.as_ref(), user) + .map_err(Into::into); + snapshot::create(project_repository.project(), "applied branch")?; + result }) } @@ -623,7 +637,10 @@ impl ControllerInner { let _permit = self.semaphore.acquire().await; self.with_verify_branch(project_id, |project_repository, _| { - super::unapply_ownership(project_repository, ownership).map_err(Into::into) + let result = + super::unapply_ownership(project_repository, ownership).map_err(Into::into); + snapshot::create(project_repository.project(), "unapplied ownership")?; + result }) } @@ -635,7 +652,9 @@ impl ControllerInner { let _permit = self.semaphore.acquire().await; self.with_verify_branch(project_id, |project_repository, _| { - super::reset_files(project_repository, ownership).map_err(Into::into) + let result = super::reset_files(project_repository, ownership).map_err(Into::into); + snapshot::create(project_repository.project(), "reset files")?; + result }) } @@ -648,7 +667,9 @@ impl ControllerInner { let _permit = self.semaphore.acquire().await; self.with_verify_branch(project_id, |project_repository, _| { - super::amend(project_repository, branch_id, ownership).map_err(Into::into) + let result = super::amend(project_repository, branch_id, ownership).map_err(Into::into); + snapshot::create(project_repository.project(), "amended commit")?; + result }) } @@ -661,8 +682,10 @@ impl ControllerInner { let _permit = self.semaphore.acquire().await; self.with_verify_branch(project_id, |project_repository, _| { - super::reset_branch(project_repository, branch_id, target_commit_oid) - .map_err(Into::into) + let result = super::reset_branch(project_repository, branch_id, target_commit_oid) + .map_err(Into::into); + snapshot::create(project_repository.project(), "reset branch")?; + result }) } @@ -674,9 +697,11 @@ impl ControllerInner { let _permit = self.semaphore.acquire().await; self.with_verify_branch(project_id, |project_repository, _| { - super::unapply_branch(project_repository, branch_id) + let result = super::unapply_branch(project_repository, branch_id) .map(|_| ()) - .map_err(Into::into) + .map_err(Into::into); + snapshot::create(project_repository.project(), "unapplied branch")?; + result }) } @@ -713,7 +738,10 @@ impl ControllerInner { let _permit = self.semaphore.acquire().await; self.with_verify_branch(project_id, |project_repository, _| { - super::cherry_pick(project_repository, branch_id, commit_oid).map_err(Into::into) + let result = + super::cherry_pick(project_repository, branch_id, commit_oid).map_err(Into::into); + snapshot::create(project_repository.project(), "cherry picked")?; + result }) } @@ -745,7 +773,10 @@ impl ControllerInner { let _permit = self.semaphore.acquire().await; self.with_verify_branch(project_id, |project_repository, _| { - super::squash(project_repository, branch_id, commit_oid).map_err(Into::into) + let result = + super::squash(project_repository, branch_id, commit_oid).map_err(Into::into); + snapshot::create(project_repository.project(), "squashed commit")?; + result }) } @@ -758,8 +789,11 @@ impl ControllerInner { ) -> Result<(), Error> { let _permit = self.semaphore.acquire().await; self.with_verify_branch(project_id, |project_repository, _| { - super::update_commit_message(project_repository, branch_id, commit_oid, message) - .map_err(Into::into) + let result = + super::update_commit_message(project_repository, branch_id, commit_oid, message) + .map_err(Into::into); + snapshot::create(project_repository.project(), "updated commit message")?; + result }) } @@ -829,14 +863,16 @@ impl ControllerInner { .context("failed to get private key") }) .transpose()?; - super::move_commit( + let result = super::move_commit( project_repository, target_branch_id, commit_oid, user, signing_key.as_ref(), ) - .map_err(Into::into) + .map_err(Into::into); + snapshot::create(project_repository.project(), "moved commit")?; + result }) } } From 9a499575c4d0f38dc31e00be74a857c29fb9772b Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 17:41:13 +0200 Subject: [PATCH 11/15] fixes a bug with the reflog editing --- crates/gitbutler-core/src/snapshots/reflog.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gitbutler-core/src/snapshots/reflog.rs b/crates/gitbutler-core/src/snapshots/reflog.rs index 024e66504..2366dd4d4 100644 --- a/crates/gitbutler-core/src/snapshots/reflog.rs +++ b/crates/gitbutler-core/src/snapshots/reflog.rs @@ -42,7 +42,7 @@ impl SnapshotsReference { // 0000000000000000000000000000000000000000 82873b54925ab268e9949557f28d070d388e7774 Kiril Videlov 1714037434 +0200 branch: Created from 82873b54925ab268e9949557f28d070d388e7774 let content = std::fs::read_to_string(&self.file_path)?; let mut lines = content.lines().collect::>(); - let mut first_line = lines.remove(0).split_whitespace().collect_vec(); + let mut first_line = lines[0].split_whitespace().collect_vec(); let len = first_line.len(); first_line[1] = sha; first_line[len - 1] = sha; From 24a357e3c7fed51bd08b023ca22150186ffadb4a Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 18:33:56 +0200 Subject: [PATCH 12/15] add some basic UI for the undo activated with Cmd+shift+H --- app/src/lib/components/History.svelte | 79 +++++++++++++++++++++++ app/src/lib/settings/userSettings.ts | 4 +- app/src/routes/+layout.svelte | 6 ++ app/src/routes/[projectId]/+layout.svelte | 7 ++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 app/src/lib/components/History.svelte diff --git a/app/src/lib/components/History.svelte b/app/src/lib/components/History.svelte new file mode 100644 index 000000000..95dad4c75 --- /dev/null +++ b/app/src/lib/components/History.svelte @@ -0,0 +1,79 @@ + + +
+ {#each snapshots as entry, idx} +
+
+
+ {entry.label} +
+
+ + {toHumanReadableTime(entry.createdAt)} + + {#if idx != 0} + + {/if} +
+
+
+ {/each} +
+ + diff --git a/app/src/lib/settings/userSettings.ts b/app/src/lib/settings/userSettings.ts index 436f7da9a..2a0b355a6 100644 --- a/app/src/lib/settings/userSettings.ts +++ b/app/src/lib/settings/userSettings.ts @@ -17,6 +17,7 @@ export interface Settings { zoom: number; scrollbarVisabilityOnHover: boolean; tabSize: number; + showHistoryView: boolean; } const defaults: Settings = { @@ -31,7 +32,8 @@ const defaults: Settings = { stashedBranchesHeight: 150, zoom: 1, scrollbarVisabilityOnHover: false, - tabSize: 4 + tabSize: 4, + showHistoryView: false }; export function loadUserSettings(): Writable { diff --git a/app/src/routes/+layout.svelte b/app/src/routes/+layout.svelte index a918419cd..ba89d6f41 100644 --- a/app/src/routes/+layout.svelte +++ b/app/src/routes/+layout.svelte @@ -67,6 +67,12 @@ hotkeys.on('Backspace', (e) => { // This prevent backspace from navigating back e.preventDefault(); + }), + hotkeys.on('$mod+Shift+H', () => { + userSettings.update((s) => ({ + ...s, + showHistoryView: !$userSettings.showHistoryView + })); }) ); }); diff --git a/app/src/routes/[projectId]/+layout.svelte b/app/src/routes/[projectId]/+layout.svelte index 59fc24f4f..5b909a2c4 100644 --- a/app/src/routes/[projectId]/+layout.svelte +++ b/app/src/routes/[projectId]/+layout.svelte @@ -2,11 +2,14 @@ import { Project } from '$lib/backend/projects'; import { syncToCloud } from '$lib/backend/sync'; import { BranchService } from '$lib/branches/service'; + import History from '$lib/components/History.svelte'; import Navigation from '$lib/components/Navigation.svelte'; import NoBaseBranch from '$lib/components/NoBaseBranch.svelte'; import NotOnGitButlerBranch from '$lib/components/NotOnGitButlerBranch.svelte'; import ProblemLoadingRepo from '$lib/components/ProblemLoadingRepo.svelte'; import ProjectSettingsMenuAction from '$lib/components/ProjectSettingsMenuAction.svelte'; + import { SETTINGS, type Settings } from '$lib/settings/userSettings'; + import { getContextStoreBySymbol } from '$lib/utils/context'; import * as hotkeys from '$lib/utils/hotkeys'; import { unsubscribe } from '$lib/utils/unsubscribe'; import { BaseBranchService, NoDefaultTarget } from '$lib/vbranches/baseBranch'; @@ -33,6 +36,7 @@ $: baseBranch = baseBranchService.base; $: baseError = baseBranchService.error; $: projectError = projectService.error; + const userSettings = getContextStoreBySymbol(SETTINGS); $: setContext(VirtualBranchService, vbranchService); $: setContext(BranchController, branchController); @@ -90,6 +94,9 @@
+ {#if $userSettings.showHistoryView} + + {/if}
{/if} {/key} From bedc918b84f06fbe07f5c01c565436782cc12753 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 20:51:24 +0200 Subject: [PATCH 13/15] simplify reflog reference setting --- crates/gitbutler-core/src/snapshots/reflog.rs | 130 ++++++++++-------- .../gitbutler-core/src/snapshots/snapshot.rs | 10 +- 2 files changed, 78 insertions(+), 62 deletions(-) diff --git a/crates/gitbutler-core/src/snapshots/reflog.rs b/crates/gitbutler-core/src/snapshots/reflog.rs index 2366dd4d4..ddab17cbd 100644 --- a/crates/gitbutler-core/src/snapshots/reflog.rs +++ b/crates/gitbutler-core/src/snapshots/reflog.rs @@ -6,70 +6,84 @@ use std::{io::Write, path::PathBuf}; use crate::projects::Project; -pub struct SnapshotsReference { - file_path: PathBuf, +/// Sets a reference to the oplog head commit such that snapshots are reachable and will not be garbage collected. +/// We want to achieve 2 things: +/// - The oplog must not be visible in `git log --all` as branch +/// - The oplog tree must not be garbage collected (i.e. it must be reachable) +/// +/// This needs to be invoked whenever the target head or the oplog head change. +/// +/// How it works: +/// First a reference gitbutler/target is created, pointing to the head of the target (trunk) branch. This is a fake branch that we don't need to care about. If it doesn't exist, it is created. +/// Then in the reflog entry logs/refs/heads/gitbutler/target we pretend that the the ref originally pointed to the oplog head commit like so: +/// +/// 0000000000000000000000000000000000000000 +/// +/// +/// The reflog entry is continuously updated to refer to the current target and oplog head commits. +pub fn set_reference_to_oplog( + project: &Project, + target_head_sha: &str, + oplog_head_sha: &str, +) -> Result<()> { + let repo_path = project.path.as_path(); + let reflog_file_path = repo_path + .join(".git") + .join("logs") + .join("refs") + .join("heads") + .join("gitbutler") + .join("target"); + + if !reflog_file_path.exists() { + let repo = git2::Repository::init(repo_path)?; + let commit = repo.find_commit(git2::Oid::from_str(target_head_sha)?)?; + repo.branch("gitbutler/target", &commit, false)?; + } + + if !reflog_file_path.exists() { + return Err(anyhow::anyhow!( + "Could not create gitbutler/target which is needed for undo snapshotting" + )); + } + + set_target_ref(&reflog_file_path, target_head_sha)?; + set_oplog_ref(&reflog_file_path, oplog_head_sha)?; + + Ok(()) } -impl SnapshotsReference { - pub fn new(project: &Project, target_sha: &str) -> Result { - let repo_path = project.path.as_path(); - let reflog_file_path = repo_path - .join(".git") - .join("logs") - .join("refs") - .join("heads") - .join("gitbutler") - .join("target"); +fn set_target_ref(file_path: &PathBuf, sha: &str) -> Result<()> { + // 0000000000000000000000000000000000000000 82873b54925ab268e9949557f28d070d388e7774 Kiril Videlov 1714037434 +0200 branch: Created from 82873b54925ab268e9949557f28d070d388e7774 + let content = std::fs::read_to_string(file_path)?; + let mut lines = content.lines().collect::>(); + let mut first_line = lines[0].split_whitespace().collect_vec(); + let len = first_line.len(); + first_line[1] = sha; + first_line[len - 1] = sha; + let binding = first_line.join(" "); + lines[0] = &binding; + let content = format!("{}\n", lines.join("\n")); + write(file_path, &content) +} - if !reflog_file_path.exists() { - let repo = git2::Repository::init(repo_path)?; - let commit = repo.find_commit(git2::Oid::from_str(target_sha)?)?; - repo.branch("gitbutler/target", &commit, false)?; - } +fn set_oplog_ref(file_path: &PathBuf, sha: &str) -> Result<()> { + // 82873b54925ab268e9949557f28d070d388e7774 7e8eab472636a26611214bebea7d6b79c971fb8b Kiril Videlov 1714044124 +0200 reset: moving to 7e8eab472636a26611214bebea7d6b79c971fb8b + let content = std::fs::read_to_string(file_path)?; + let first_line = content.lines().collect::>().remove(0); - if !reflog_file_path.exists() { - return Err(anyhow::anyhow!( - "Could not create gitbutler/target which is needed for undo snapshotting" - )); - } + let target_ref = first_line.split_whitespace().collect_vec()[1]; + let the_rest = first_line.split_whitespace().collect_vec()[2..].join(" "); + let the_rest = the_rest.replace("branch", " reset"); + let mut the_rest_split = the_rest.split(':').collect_vec(); + let new_msg = format!(" moving to {}", sha); + the_rest_split[1] = &new_msg; + let the_rest = the_rest_split.join(":"); - Ok(Self { - file_path: reflog_file_path, - }) - } + let second_line = [target_ref, sha, &the_rest].join(" "); - pub fn set_target_ref(&self, sha: &str) -> Result<()> { - // 0000000000000000000000000000000000000000 82873b54925ab268e9949557f28d070d388e7774 Kiril Videlov 1714037434 +0200 branch: Created from 82873b54925ab268e9949557f28d070d388e7774 - let content = std::fs::read_to_string(&self.file_path)?; - let mut lines = content.lines().collect::>(); - let mut first_line = lines[0].split_whitespace().collect_vec(); - let len = first_line.len(); - first_line[1] = sha; - first_line[len - 1] = sha; - let binding = first_line.join(" "); - lines[0] = &binding; - let content = format!("{}\n", lines.join("\n")); - write(&self.file_path, &content) - } - - pub fn set_oplog_ref(&self, sha: &str) -> Result<()> { - // 82873b54925ab268e9949557f28d070d388e7774 7e8eab472636a26611214bebea7d6b79c971fb8b Kiril Videlov 1714044124 +0200 reset: moving to 7e8eab472636a26611214bebea7d6b79c971fb8b - let content = std::fs::read_to_string(&self.file_path)?; - let first_line = content.lines().collect::>().remove(0); - - let target_ref = first_line.split_whitespace().collect_vec()[1]; - let the_rest = first_line.split_whitespace().collect_vec()[2..].join(" "); - let the_rest = the_rest.replace("branch", " reset"); - let mut the_rest_split = the_rest.split(':').collect_vec(); - let new_msg = format!(" moving to {}", sha); - the_rest_split[1] = &new_msg; - let the_rest = the_rest_split.join(":"); - - let second_line = [target_ref, sha, &the_rest].join(" "); - - let content = format!("{}\n", [first_line, &second_line].join("\n")); - write(&self.file_path, &content) - } + let content = format!("{}\n", [first_line, &second_line].join("\n")); + write(file_path, &content) } fn write(file_path: &PathBuf, content: &str) -> Result<()> { diff --git a/crates/gitbutler-core/src/snapshots/snapshot.rs b/crates/gitbutler-core/src/snapshots/snapshot.rs index f326e694c..2629b198c 100644 --- a/crates/gitbutler-core/src/snapshots/snapshot.rs +++ b/crates/gitbutler-core/src/snapshots/snapshot.rs @@ -3,7 +3,7 @@ use serde::Serialize; use crate::{projects::Project, virtual_branches::VirtualBranchesHandle}; -use super::{reflog::SnapshotsReference, state::OplogHandle}; +use super::{reflog::set_reference_to_oplog, state::OplogHandle}; #[derive(Debug, PartialEq, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -77,9 +77,11 @@ pub fn create(project: &Project, label: &str) -> Result<()> { oplog_state.set_oplog_head(new_commit_oid.to_string())?; - let reflog_hack = SnapshotsReference::new(project, &default_target_sha.to_string())?; - reflog_hack.set_target_ref(&default_target_sha.to_string())?; - reflog_hack.set_oplog_ref(&new_commit_oid.to_string())?; + set_reference_to_oplog( + project, + &default_target_sha.to_string(), + &new_commit_oid.to_string(), + )?; Ok(()) } From 33859c89ef7c0dffd30f9d3197d88332fb13a4ae Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 21:09:02 +0200 Subject: [PATCH 14/15] adds some documentation --- .../gitbutler-core/src/snapshots/snapshot.rs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/gitbutler-core/src/snapshots/snapshot.rs b/crates/gitbutler-core/src/snapshots/snapshot.rs index 2629b198c..1c1b3c458 100644 --- a/crates/gitbutler-core/src/snapshots/snapshot.rs +++ b/crates/gitbutler-core/src/snapshots/snapshot.rs @@ -5,14 +5,26 @@ use crate::{projects::Project, virtual_branches::VirtualBranchesHandle}; use super::{reflog::set_reference_to_oplog, state::OplogHandle}; +/// A snapshot of the repository and virtual branches state that GitButler can restore to. +/// It captures the state of the working directory, virtual branches and commits. #[derive(Debug, PartialEq, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct SnapshotEntry { + /// The sha of the commit that represents the snapshot. pub sha: String, + /// Textual description of the snapshot. pub label: String, - pub created_at: i64, // milliseconds since epoch + /// The time the snapshot was created at in milliseconds since epoch. + pub created_at: i64, } +/// 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 state of virtual branches `.git/gitbutler/virtual_branches.toml` is copied to the project root so that it is snapshotted. pub fn create(project: &Project, label: &str) -> Result<()> { if project.enable_snapshots.is_none() || project.enable_snapshots == Some(false) { return Ok(()); @@ -86,6 +98,10 @@ pub fn create(project: &Project, label: &str) -> Result<()> { Ok(()) } +/// 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 ` available in `.git/gitbutler/oplog.toml`. +/// +/// If there are no snapshots, an empty list is returned. pub fn list(project: Project, limit: usize) -> Result> { let repo_path = project.path.as_path(); let repo = git2::Repository::init(repo_path)?; @@ -105,7 +121,6 @@ pub fn list(project: Project, limit: usize) -> Result> { let mut snapshots = Vec::new(); - // TODO: how do we know when to stop? for commit_id in revwalk { let commit_id = commit_id?; let commit = repo.find_commit(commit_id)?; @@ -127,6 +142,11 @@ pub fn list(project: Project, limit: usize) -> Result> { 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. +/// +/// The state of virtual branches `.git/gitbutler/virtual_branches.toml` is restored from the snapshot. pub fn restore(project: &Project, sha: String) -> Result<()> { let repo_path = project.path.as_path(); let repo = git2::Repository::init(repo_path)?; From fd1ac5d65b35e9b262fed0682197c86337967cfa Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Thu, 25 Apr 2024 21:44:30 +0200 Subject: [PATCH 15/15] improved snapshot labels --- .../gitbutler-core/src/snapshots/snapshot.rs | 5 +- .../src/virtual_branches/controller.rs | 47 ++++++++++++------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/crates/gitbutler-core/src/snapshots/snapshot.rs b/crates/gitbutler-core/src/snapshots/snapshot.rs index 1c1b3c458..02d1f3a31 100644 --- a/crates/gitbutler-core/src/snapshots/snapshot.rs +++ b/crates/gitbutler-core/src/snapshots/snapshot.rs @@ -167,10 +167,7 @@ pub fn restore(project: &Project, sha: String) -> Result<()> { )?; // create new snapshot - let label = format!( - "Restored from snapshot {}", - commit.message().unwrap_or(&sha) - ); + let label = format!("Restored from {}", &sha); create(project, &label)?; Ok(()) diff --git a/crates/gitbutler-core/src/virtual_branches/controller.rs b/crates/gitbutler-core/src/virtual_branches/controller.rs index 935f7a20b..fd37f843a 100644 --- a/crates/gitbutler-core/src/virtual_branches/controller.rs +++ b/crates/gitbutler-core/src/virtual_branches/controller.rs @@ -407,7 +407,7 @@ impl ControllerInner { run_hooks, ) .map_err(Into::into); - snapshot::create(project_repository.project(), "created commit")?; + snapshot::create(project_repository.project(), "create commit")?; result }) } @@ -455,7 +455,7 @@ 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(), "created branch")?; + snapshot::create(project_repository.project(), "create branch")?; Ok(branch_id) }) } @@ -484,7 +484,7 @@ impl ControllerInner { signing_key.as_ref(), user, )?; - snapshot::create(project_repository.project(), "created branch")?; + snapshot::create(project_repository.project(), "create branch")?; Ok(result) }) } @@ -547,7 +547,7 @@ impl ControllerInner { user, ) .map_err(Into::into); - snapshot::create(project_repository.project(), "merged upstream")?; + snapshot::create(project_repository.project(), "merge upstream")?; result }) } @@ -569,7 +569,7 @@ impl ControllerInner { let result = super::update_base_branch(project_repository, user, signing_key.as_ref()) .map_err(Into::into); - snapshot::create(project_repository.project(), "updated base branch")?; + snapshot::create(project_repository.project(), "update workspace base")?; result }) } @@ -582,8 +582,23 @@ impl ControllerInner { let _permit = self.semaphore.acquire().await; self.with_verify_branch(project_id, |project_repository, _| { + let label = if branch_update.ownership.is_some() { + "move hunk" + } else if branch_update.name.is_some() { + "update branch name" + } else if branch_update.notes.is_some() { + "update branch notes" + } else if branch_update.order.is_some() { + "reorder branches" + } else if branch_update.selected_for_changes.is_some() { + "select default branch" + } else if branch_update.upstream.is_some() { + "update remote branch name" + } else { + "update branch" + }; super::update_branch(project_repository, branch_update)?; - snapshot::create(project_repository.project(), "updated branch")?; + snapshot::create(project_repository.project(), label)?; Ok(()) }) } @@ -597,7 +612,7 @@ impl ControllerInner { self.with_verify_branch(project_id, |project_repository, _| { super::delete_branch(project_repository, branch_id)?; - snapshot::create(project_repository.project(), "deleted branch")?; + snapshot::create(project_repository.project(), "delete branch")?; Ok(()) }) } @@ -624,7 +639,7 @@ 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(), "applied branch")?; + snapshot::create(project_repository.project(), "apply branch")?; result }) } @@ -639,7 +654,7 @@ 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(), "unapplied ownership")?; + snapshot::create(project_repository.project(), "discard hunk")?; result }) } @@ -653,7 +668,7 @@ 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(), "reset files")?; + snapshot::create(project_repository.project(), "discard file")?; result }) } @@ -668,7 +683,7 @@ impl ControllerInner { self.with_verify_branch(project_id, |project_repository, _| { let result = super::amend(project_repository, branch_id, ownership).map_err(Into::into); - snapshot::create(project_repository.project(), "amended commit")?; + snapshot::create(project_repository.project(), "amend commit")?; result }) } @@ -684,7 +699,7 @@ 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(), "reset branch")?; + snapshot::create(project_repository.project(), "undo commit")?; result }) } @@ -700,7 +715,7 @@ impl ControllerInner { let result = super::unapply_branch(project_repository, branch_id) .map(|_| ()) .map_err(Into::into); - snapshot::create(project_repository.project(), "unapplied branch")?; + snapshot::create(project_repository.project(), "unapply branch")?; result }) } @@ -740,7 +755,7 @@ 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(), "cherry picked")?; + snapshot::create(project_repository.project(), "cherry pick")?; result }) } @@ -775,7 +790,7 @@ 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(), "squashed commit")?; + snapshot::create(project_repository.project(), "squash commit")?; result }) } @@ -792,7 +807,7 @@ impl ControllerInner { let result = super::update_commit_message(project_repository, branch_id, commit_oid, message) .map_err(Into::into); - snapshot::create(project_repository.project(), "updated commit message")?; + snapshot::create(project_repository.project(), "update commit message")?; result }) }