2024-07-28 21:48:13 +03:00
use std ::{
collections ::{ hash_map ::Entry , HashMap } ,
fs ,
2024-08-28 22:52:48 +03:00
path ::PathBuf ,
2024-07-28 21:48:13 +03:00
str ::{ from_utf8 , FromStr } ,
time ::Duration ,
} ;
use anyhow ::{ anyhow , bail , Context , Result } ;
2024-06-25 14:21:51 +03:00
use git2 ::{ DiffOptions , FileMode } ;
2024-07-30 16:32:38 +03:00
use gitbutler_branch ::{ Branch , SignaturePurpose , VirtualBranchesHandle , VirtualBranchesState } ;
2024-08-28 22:52:48 +03:00
use gitbutler_command_context ::RepositoryExtLite ;
2024-07-17 21:16:48 +03:00
use gitbutler_diff ::{ hunks_by_filepath , FileDiff } ;
2024-07-28 21:48:13 +03:00
use gitbutler_project ::{
access ::{ WorktreeReadPermission , WorktreeWritePermission } ,
Project ,
} ;
2024-07-08 13:21:01 +03:00
use gitbutler_repo ::RepositoryExt ;
2024-05-26 10:52:43 +03:00
use tracing ::instrument ;
2024-04-25 11:37:24 +03:00
2024-04-27 00:20:31 +03:00
use super ::{
2024-05-26 10:52:43 +03:00
entry ::{ OperationKind , Snapshot , SnapshotDetails , Trailer } ,
2024-04-27 00:20:31 +03:00
reflog ::set_reference_to_oplog ,
state ::OplogHandle ,
} ;
2024-04-25 11:37:24 +03:00
2024-04-29 21:28:27 +03:00
const SNAPSHOT_FILE_LIMIT_BYTES : u64 = 32 * 1024 * 1024 ;
2024-05-26 15:34:45 +03:00
/// The Oplog allows for crating snapshots of the current state of the project as well as restoring to a previous snapshot.
2024-05-27 16:23:04 +03:00
/// Snapshots include the state of the working directory as well as all additional GitButler state (e.g. virtual branches, conflict state).
2024-05-24 01:16:40 +03:00
/// The data is stored as git trees in the following shape:
2024-05-27 16:23:04 +03:00
///
/// ```text
2024-05-24 01:16:40 +03:00
/// .
2024-05-27 16:23:04 +03:00
/// ├── conflicts/…
/// ├── index/
/// ├── target_tree/…
2024-05-24 01:16:40 +03:00
/// ├── virtual_branches
/// │ └── [branch-id]
/// │ ├── commit-message.txt
/// │ └── tree (subtree)
/// │ └── [branch-id]
/// │ ├── commit-message.txt
/// │ └── tree (subtree)
/// └── virtual_branches.toml
2024-05-27 16:23:04 +03:00
/// ```
2024-07-13 12:08:24 +03:00
pub trait OplogExt {
2024-05-24 01:16:40 +03:00
/// Prepares a snapshot of the current state of the working directory as well as GitButler data.
2024-05-27 16:23:04 +03:00
/// Returns a tree hash of the snapshot. The snapshot is not discoverable until it is committed with [`commit_snapshot`](Self::commit_snapshot())
2024-05-26 15:34:45 +03:00
/// If there are files that are untracked and larger than `SNAPSHOT_FILE_LIMIT_BYTES`, they are excluded from snapshot creation and restoring.
2024-07-15 15:22:27 +03:00
fn prepare_snapshot ( & self , perm : & WorktreeReadPermission ) -> Result < git2 ::Oid > ;
2024-07-07 21:00:01 +03:00
/// Commits the snapshot tree that is created with the [`prepare_snapshot`](Self::prepare_snapshot) method,
/// which yielded the `snapshot_tree_id` for the entire snapshot state.
/// Use `details` to provide metadata about the snapshot.
///
/// Committing it makes the snapshot discoverable in [`list_snapshots`](Self::list_snapshots) as well as
/// restorable with [`restore_snapshot`](Self::restore_snapshot).
///
/// Returns `Some(snapshot_commit_id)` if it was created or `None` if nothing changed between the previous oplog
/// commit and the current one (after comparing trees).
fn commit_snapshot (
& self ,
snapshot_tree_id : git2 ::Oid ,
details : SnapshotDetails ,
2024-07-15 15:22:27 +03:00
perm : & mut WorktreeWritePermission ,
2024-07-07 21:00:01 +03:00
) -> Result < Option < git2 ::Oid > > ;
/// Creates a snapshot of the current state of the working directory as well as GitButler data.
/// This is a convenience method that combines [`prepare_snapshot`](Self::prepare_snapshot) and
/// [`commit_snapshot`](Self::commit_snapshot).
///
/// Returns `Some(snapshot_commit_id)` if it was created or `None` if nothing changed between the previous oplog
/// commit and the current one (after comparing trees).
///
/// Note that errors in snapshot creation is typically ignored, so we want to learn about them.
2024-07-15 15:22:27 +03:00
fn create_snapshot (
& self ,
details : SnapshotDetails ,
perm : & mut WorktreeWritePermission ,
) -> Result < Option < git2 ::Oid > > ;
2024-07-07 21:00:01 +03:00
/// Lists the snapshots that have been created for the given repository, up to the given limit,
/// and with the most recent snapshot first, and at the end of the vec.
///
/// Use `oplog_commit_id` if the traversal root for snapshot discovery should be the specified commit, which
/// is usually obtained from a previous iteration. Useful along with `limit` to allow starting where the iteration
/// left off. Note that the `oplog_commit_id` is always returned as first item in the result vec.
///
/// An alternative way of retrieving the snapshots would be to manually the oplog head `git log <oplog_head>` available in `.git/gitbutler/operations-log.toml`.
///
/// If there are no snapshots, an empty list is returned.
fn list_snapshots (
& self ,
limit : usize ,
oplog_commit_id : Option < git2 ::Oid > ,
) -> Result < Vec < Snapshot > > ;
/// Reverts to a previous state of the working directory, virtual branches and commits.
/// The provided `snapshot_commit_id` must refer to a valid snapshot commit, as returned by [`create_snapshot`](Self::create_snapshot).
/// Upon success, a new snapshot is created representing the state right before this call.
///
/// 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 , snapshot_commit_id : git2 ::Oid ) -> Result < Option < git2 ::Oid > > ;
/// Determines if a new snapshot should be created due to file changes being created since the last snapshot.
/// The needs for the automatic snapshotting are:
/// - It needs to facilitate backup of work in progress code
/// - The snapshots should not be too frequent or small - both for UX and performance reasons
/// - Checking if an automatic snapshot is needed should be fast and efficient since it is called on filesystem events
///
/// Use `check_if_last_snapshot_older_than` as a way to control if the check should be performed at all, i.e.
/// if this is 10s but the last snapshot was done 9s ago, no check if performed and the return value is `false`.
///
/// This implementation returns `true` on the following conditions:
/// - Head is pointing to the integration branch.
/// - If it's been more than 5 minutes since the last snapshot,
/// check the sum of added and removed lines since the last snapshot, otherwise return `false`.
/// * If the sum of added and removed lines is greater than a configured threshold, return `true`, otherwise return `false`.
fn should_auto_snapshot ( & self , check_if_last_snapshot_older_than : Duration ) -> Result < bool > ;
/// Returns the diff of the snapshot and it's parent. It only includes the workdir changes.
///
/// This is useful to show what has changed in this particular snapshot
fn snapshot_diff ( & self , sha : git2 ::Oid ) -> Result < HashMap < PathBuf , FileDiff > > ;
/// Gets the sha of the last snapshot commit if present.
fn oplog_head ( & self ) -> Result < Option < git2 ::Oid > > ;
}
2024-07-13 12:08:24 +03:00
impl OplogExt for Project {
2024-07-15 15:22:27 +03:00
fn prepare_snapshot ( & self , perm : & WorktreeReadPermission ) -> Result < git2 ::Oid > {
prepare_snapshot ( self , perm )
2024-05-24 01:16:40 +03:00
}
2024-07-07 21:00:01 +03:00
fn commit_snapshot (
2024-05-26 15:34:45 +03:00
& self ,
2024-06-05 23:56:03 +03:00
snapshot_tree_id : git2 ::Oid ,
2024-05-26 15:34:45 +03:00
details : SnapshotDetails ,
2024-07-15 15:22:27 +03:00
perm : & mut WorktreeWritePermission ,
2024-06-05 23:56:03 +03:00
) -> Result < Option < git2 ::Oid > > {
2024-07-15 15:22:27 +03:00
commit_snapshot ( self , snapshot_tree_id , details , perm )
2024-05-05 23:28:12 +03:00
}
2024-04-25 11:37:24 +03:00
2024-08-29 15:46:58 +03:00
#[ instrument(skip(self, details, perm), err(Debug)) ]
2024-07-15 15:22:27 +03:00
fn create_snapshot (
& self ,
details : SnapshotDetails ,
perm : & mut WorktreeWritePermission ,
) -> Result < Option < git2 ::Oid > > {
let tree_id = prepare_snapshot ( self , perm . read_permission ( ) ) ? ;
commit_snapshot ( self , tree_id , details , perm )
2024-05-24 01:16:40 +03:00
}
2024-08-29 15:46:58 +03:00
#[ instrument(skip(self), err(Debug)) ]
2024-07-07 21:00:01 +03:00
fn list_snapshots (
2024-05-27 16:23:04 +03:00
& self ,
limit : usize ,
2024-06-05 23:56:03 +03:00
oplog_commit_id : Option < git2 ::Oid > ,
2024-05-27 16:23:04 +03:00
) -> Result < Vec < Snapshot > > {
2024-05-05 23:28:12 +03:00
let repo_path = self . path . as_path ( ) ;
2024-05-27 16:23:04 +03:00
let repo = git2 ::Repository ::open ( repo_path ) ? ;
2024-04-25 11:37:24 +03:00
2024-05-27 16:23:04 +03:00
let traversal_root_id = match oplog_commit_id {
2024-05-28 19:54:50 +03:00
Some ( id ) = > id ,
2024-05-07 01:43:45 +03:00
None = > {
let oplog_state = OplogHandle ::new ( & self . gb_dir ( ) ) ;
2024-05-27 16:23:04 +03:00
if let Some ( id ) = oplog_state . oplog_head ( ) ? {
id
2024-05-07 01:43:45 +03:00
} else {
return Ok ( vec! [ ] ) ;
}
}
} ;
2024-05-05 23:28:12 +03:00
2024-06-05 23:56:03 +03:00
let oplog_head_commit = repo . find_commit ( traversal_root_id ) ? ;
2024-05-05 23:28:12 +03:00
let mut revwalk = repo . revwalk ( ) ? ;
revwalk . push ( oplog_head_commit . id ( ) ) ? ;
let mut snapshots = Vec ::new ( ) ;
2024-06-11 21:05:13 +03:00
let mut wd_trees_cache : HashMap < git2 ::Oid , git2 ::Oid > = HashMap ::new ( ) ;
2024-05-05 23:28:12 +03:00
for commit_id in revwalk {
2024-05-27 16:23:04 +03:00
if snapshots . len ( ) = = limit {
break ;
}
2024-05-05 23:28:12 +03:00
let commit_id = commit_id ? ;
let commit = repo . find_commit ( commit_id ) ? ;
2024-04-27 00:20:31 +03:00
2024-05-05 23:28:12 +03:00
if commit . parent_count ( ) > 1 {
break ;
}
2024-05-06 16:26:44 +03:00
let tree = commit . tree ( ) ? ;
2024-06-11 20:09:45 +03:00
if tree . get_name ( " virtual_branches.toml " ) . is_none ( ) {
2024-05-06 16:26:44 +03:00
// We reached a tree that is not a snapshot
2024-05-28 19:54:50 +03:00
tracing ::warn! ( " Commit {commit_id} didn't seem to be an oplog commit - skipping " ) ;
2024-05-06 16:26:44 +03:00
continue ;
2024-06-11 20:09:45 +03:00
}
2024-06-11 21:05:13 +03:00
// Get tree id from cache or calculate it
2024-07-20 15:31:43 +03:00
let wd_tree = get_workdir_tree ( & mut wd_trees_cache , commit_id , & repo ) ? ;
2024-05-06 16:26:44 +03:00
2024-05-05 23:28:12 +03:00
let details = commit
. message ( )
. and_then ( | msg | SnapshotDetails ::from_str ( msg ) . ok ( ) ) ;
2024-04-27 00:20:31 +03:00
2024-05-16 12:19:38 +03:00
if let Ok ( parent ) = commit . parent ( 0 ) {
2024-06-11 21:05:13 +03:00
// Get tree id from cache or calculate it
2024-07-20 15:31:43 +03:00
let parent_tree = get_workdir_tree ( & mut wd_trees_cache , parent . id ( ) , & repo ) ? ;
2024-05-16 12:19:38 +03:00
2024-06-25 14:21:51 +03:00
let mut opts = DiffOptions ::new ( ) ;
opts . include_untracked ( true ) ;
opts . ignore_submodules ( true ) ;
let diff =
repo . diff_tree_to_tree ( Some ( & parent_tree ) , Some ( & wd_tree ) , Some ( & mut opts ) ) ? ;
2024-04-25 11:37:24 +03:00
2024-05-16 12:19:38 +03:00
let mut files_changed = Vec ::new ( ) ;
diff . print ( git2 ::DiffFormat ::NameOnly , | delta , _ , _ | {
if let Some ( path ) = delta . new_file ( ) . path ( ) {
files_changed . push ( path . to_path_buf ( ) ) ;
}
true
} ) ? ;
2024-05-27 16:23:04 +03:00
let stats = diff . stats ( ) ? ;
2024-05-16 12:19:38 +03:00
snapshots . push ( Snapshot {
2024-06-05 23:56:03 +03:00
commit_id ,
2024-05-16 12:19:38 +03:00
details ,
2024-05-27 16:23:04 +03:00
lines_added : stats . insertions ( ) ,
lines_removed : stats . deletions ( ) ,
2024-05-16 12:19:38 +03:00
files_changed ,
2024-05-26 10:52:43 +03:00
created_at : commit . time ( ) ,
2024-05-16 12:19:38 +03:00
} ) ;
} else {
// this is the very first snapshot
snapshots . push ( Snapshot {
2024-06-05 23:56:03 +03:00
commit_id ,
2024-05-16 12:19:38 +03:00
details ,
lines_added : 0 ,
lines_removed : 0 ,
2024-05-27 16:23:04 +03:00
files_changed : Vec ::new ( ) ,
2024-05-26 10:52:43 +03:00
created_at : commit . time ( ) ,
2024-05-16 12:19:38 +03:00
} ) ;
2024-05-05 23:28:12 +03:00
break ;
}
2024-04-25 11:37:24 +03:00
}
2024-05-05 23:28:12 +03:00
Ok ( snapshots )
2024-04-25 11:37:24 +03:00
}
2024-07-07 21:00:01 +03:00
fn restore_snapshot ( & self , snapshot_commit_id : git2 ::Oid ) -> Result < Option < git2 ::Oid > > {
2024-07-14 22:51:28 +03:00
let mut guard = self . exclusive_worktree_access ( ) ;
restore_snapshot ( self , snapshot_commit_id , guard . write_permission ( ) )
2024-05-05 23:28:12 +03:00
}
2024-04-25 11:37:24 +03:00
2024-08-28 16:32:28 +03:00
#[ instrument(level = tracing::Level::DEBUG, skip(self), err(Debug)) ]
2024-07-07 21:00:01 +03:00
fn should_auto_snapshot ( & self , check_if_last_snapshot_older_than : Duration ) -> Result < bool > {
2024-05-28 19:54:50 +03:00
let last_snapshot_time = OplogHandle ::new ( & self . gb_dir ( ) ) . modified_at ( ) ? ;
if last_snapshot_time . elapsed ( ) ? < = check_if_last_snapshot_older_than {
return Ok ( false ) ;
}
let repo = git2 ::Repository ::open ( & self . path ) ? ;
if repo . integration_ref_from_head ( ) . is_err ( ) {
return Ok ( false ) ;
}
Ok ( lines_since_snapshot ( self , & repo ) ? > self . snapshot_lines_threshold ( ) )
2024-05-05 23:28:12 +03:00
}
2024-05-09 21:24:42 +03:00
2024-07-07 21:00:01 +03:00
fn snapshot_diff ( & self , sha : git2 ::Oid ) -> Result < HashMap < PathBuf , FileDiff > > {
2024-05-28 19:54:50 +03:00
let worktree_dir = self . path . as_path ( ) ;
let repo = git2 ::Repository ::init ( worktree_dir ) ? ;
2024-05-09 21:24:42 +03:00
2024-06-05 23:56:03 +03:00
let commit = repo . find_commit ( sha ) ? ;
2024-06-11 20:51:36 +03:00
let wd_tree_id = tree_from_applied_vbranches ( & repo , commit . id ( ) ) ? ;
let wd_tree = repo . find_tree ( wd_tree_id ) ? ;
let old_wd_tree_id = tree_from_applied_vbranches ( & repo , commit . parent ( 0 ) ? . id ( ) ) ? ;
let old_wd_tree = repo . find_tree ( old_wd_tree_id ) ? ;
2024-05-09 21:24:42 +03:00
2024-08-28 22:52:48 +03:00
repo . ignore_large_files_in_diffs ( SNAPSHOT_FILE_LIMIT_BYTES ) ? ;
2024-05-09 21:24:42 +03:00
let mut diff_opts = git2 ::DiffOptions ::new ( ) ;
diff_opts
. recurse_untracked_dirs ( true )
. include_untracked ( true )
. show_binary ( true )
. ignore_submodules ( true )
. show_untracked_content ( true ) ;
let diff =
repo . diff_tree_to_tree ( Some ( & old_wd_tree ) , Some ( & wd_tree ) , Some ( & mut diff_opts ) ) ? ;
let hunks = hunks_by_filepath ( None , & diff ) ? ;
Ok ( hunks )
}
2024-05-26 15:34:45 +03:00
/// Gets the sha of the last snapshot commit if present.
2024-07-07 21:00:01 +03:00
fn oplog_head ( & self ) -> Result < Option < git2 ::Oid > > {
2024-05-26 00:07:40 +03:00
let oplog_state = OplogHandle ::new ( & self . gb_dir ( ) ) ;
2024-05-27 16:23:04 +03:00
oplog_state . oplog_head ( )
2024-05-26 00:07:40 +03:00
}
2024-05-05 21:08:34 +03:00
}
2024-07-20 15:31:43 +03:00
/// Get a tree of the working dir (applied branches merged)
fn get_workdir_tree < ' a > (
wd_trees_cache : & mut HashMap < git2 ::Oid , git2 ::Oid > ,
commit_id : git2 ::Oid ,
repo : & ' a git2 ::Repository ,
) -> Result < git2 ::Tree < ' a > , anyhow ::Error > {
if let Entry ::Vacant ( e ) = wd_trees_cache . entry ( commit_id ) {
if let Ok ( wd_tree_id ) = tree_from_applied_vbranches ( repo , commit_id ) {
e . insert ( wd_tree_id ) ;
}
}
let wd_tree_id = wd_trees_cache . get ( & commit_id ) . ok_or ( anyhow! (
" Could not get a tree of all applied virtual branches merged "
) ) ? ;
let wd_tree = repo . find_tree ( wd_tree_id . to_owned ( ) ) ? ;
Ok ( wd_tree )
}
2024-07-14 22:51:28 +03:00
fn prepare_snapshot ( ctx : & Project , _shared_access : & WorktreeReadPermission ) -> Result < git2 ::Oid > {
let worktree_dir = ctx . path . as_path ( ) ;
let repo = git2 ::Repository ::open ( worktree_dir ) ? ;
let vb_state = VirtualBranchesHandle ::new ( ctx . gb_dir ( ) ) ;
// grab the target commit
let default_target_commit = repo . find_commit ( vb_state . get_default_target ( ) ? . sha ) ? ;
let target_tree_id = default_target_commit . tree_id ( ) ;
// Create a blob out of `.git/gitbutler/virtual_branches.toml`
let vb_path = repo . path ( ) . join ( " gitbutler " ) . join ( " virtual_branches.toml " ) ;
let vb_content = fs ::read ( vb_path ) ? ;
let vb_blob_id = repo . blob ( & vb_content ) ? ;
// Create a tree out of the conflicts state if present
let conflicts_tree_id = write_conflicts_tree ( worktree_dir , & repo ) ? ;
// write out the index as a tree to store
let mut index = repo . index ( ) ? ;
let index_tree_oid = index . write_tree ( ) ? ;
// start building our snapshot tree
let mut tree_builder = repo . treebuilder ( None ) ? ;
tree_builder . insert ( " index " , index_tree_oid , FileMode ::Tree . into ( ) ) ? ;
tree_builder . insert ( " target_tree " , target_tree_id , FileMode ::Tree . into ( ) ) ? ;
tree_builder . insert ( " conflicts " , conflicts_tree_id , FileMode ::Tree . into ( ) ) ? ;
tree_builder . insert ( " virtual_branches.toml " , vb_blob_id , FileMode ::Blob . into ( ) ) ? ;
// go through all virtual branches and create a subtree for each with the tree and any commits encoded
let mut branches_tree_builder = repo . treebuilder ( None ) ? ;
let mut head_tree_ids = Vec ::new ( ) ;
for branch in vb_state . list_branches_in_workspace ( ) ? {
head_tree_ids . push ( branch . tree ) ;
// commits in virtual branches (tree and commit data)
// calculate all the commits between branch.head and the target and codify them
let mut branch_tree_builder = repo . treebuilder ( None ) ? ;
branch_tree_builder . insert ( " tree " , branch . tree , FileMode ::Tree . into ( ) ) ? ;
// let's get all the commits between the branch head and the target
let mut revwalk = repo . revwalk ( ) ? ;
revwalk . push ( branch . head ) ? ;
revwalk . hide ( default_target_commit . id ( ) ) ? ;
let mut commits_tree_builder = repo . treebuilder ( None ) ? ;
for commit_id in revwalk {
let commit_id = commit_id ? ;
let commit = repo . find_commit ( commit_id ) ? ;
let commit_tree = commit . tree ( ) ? ;
let mut commit_tree_builder = repo . treebuilder ( None ) ? ;
let commit_data_blob_id = repo . blob ( & serialize_commit ( & commit ) ) ? ;
commit_tree_builder . insert ( " commit " , commit_data_blob_id , FileMode ::Blob . into ( ) ) ? ;
commit_tree_builder . insert ( " tree " , commit_tree . id ( ) , FileMode ::Tree . into ( ) ) ? ;
let commit_tree_id = commit_tree_builder . write ( ) ? ;
commits_tree_builder . insert (
commit_id . to_string ( ) ,
commit_tree_id ,
FileMode ::Tree . into ( ) ,
) ? ;
}
let commits_tree_id = commits_tree_builder . write ( ) ? ;
branch_tree_builder . insert ( " commits " , commits_tree_id , FileMode ::Tree . into ( ) ) ? ;
let branch_tree_id = branch_tree_builder . write ( ) ? ;
branches_tree_builder . insert (
branch . id . to_string ( ) ,
branch_tree_id ,
FileMode ::Tree . into ( ) ,
) ? ;
}
// also add the gitbutler/integration commit to the branches tree
let head = repo . head ( ) ? ;
if head . name ( ) = = Some ( " refs/heads/gitbutler/integration " ) {
let head_commit = head . peel_to_commit ( ) ? ;
let head_tree = head_commit . tree ( ) ? ;
let mut head_commit_tree_builder = repo . treebuilder ( None ) ? ;
// convert that data into a blob
let commit_data_blob = repo . blob ( & serialize_commit ( & head_commit ) ) ? ;
head_commit_tree_builder . insert ( " commit " , commit_data_blob , FileMode ::Blob . into ( ) ) ? ;
head_commit_tree_builder . insert ( " tree " , head_tree . id ( ) , FileMode ::Tree . into ( ) ) ? ;
let head_commit_tree_id = head_commit_tree_builder . write ( ) ? ;
// have to make a subtree to match
let mut commits_tree_builder = repo . treebuilder ( None ) ? ;
commits_tree_builder . insert (
head_commit . id ( ) . to_string ( ) ,
head_commit_tree_id ,
FileMode ::Tree . into ( ) ,
) ? ;
let commits_tree_id = commits_tree_builder . write ( ) ? ;
let mut branch_tree_builder = repo . treebuilder ( None ) ? ;
branch_tree_builder . insert ( " tree " , head_tree . id ( ) , FileMode ::Tree . into ( ) ) ? ;
branch_tree_builder . insert ( " commits " , commits_tree_id , FileMode ::Tree . into ( ) ) ? ;
let branch_tree_id = branch_tree_builder . write ( ) ? ;
branches_tree_builder . insert ( " integration " , branch_tree_id , FileMode ::Tree . into ( ) ) ? ;
}
let branch_tree_id = branches_tree_builder . write ( ) ? ;
tree_builder . insert ( " virtual_branches " , branch_tree_id , FileMode ::Tree . into ( ) ) ? ;
let tree_id = tree_builder . write ( ) ? ;
Ok ( tree_id )
}
fn commit_snapshot (
ctx : & Project ,
snapshot_tree_id : git2 ::Oid ,
details : SnapshotDetails ,
_exclusive_access : & mut WorktreeWritePermission ,
) -> Result < Option < git2 ::Oid > > {
let repo = git2 ::Repository ::open ( ctx . path . as_path ( ) ) ? ;
let snapshot_tree = repo . find_tree ( snapshot_tree_id ) ? ;
let oplog_state = OplogHandle ::new ( & ctx . gb_dir ( ) ) ;
let oplog_head_commit = oplog_state
. oplog_head ( ) ?
. and_then ( | head_id | repo . find_commit ( head_id ) . ok ( ) ) ;
// Construct a new commit
2024-07-30 16:32:38 +03:00
let committer = gitbutler_branch ::signature ( SignaturePurpose ::Committer ) ? ;
let author = gitbutler_branch ::signature ( SignaturePurpose ::Author ) ? ;
2024-07-14 22:51:28 +03:00
let parents = oplog_head_commit
. as_ref ( )
. map ( | head | vec! [ head ] )
. unwrap_or_default ( ) ;
let snapshot_commit_id = repo . commit (
None ,
2024-07-30 16:32:38 +03:00
& author ,
& committer ,
2024-07-14 22:51:28 +03:00
& details . to_string ( ) ,
& snapshot_tree ,
parents . as_slice ( ) ,
) ? ;
oplog_state . set_oplog_head ( snapshot_commit_id ) ? ;
let vb_state = VirtualBranchesHandle ::new ( ctx . gb_dir ( ) ) ;
let target_commit_id = vb_state . get_default_target ( ) ? . sha ;
set_reference_to_oplog ( & ctx . path , target_commit_id , snapshot_commit_id ) ? ;
Ok ( Some ( snapshot_commit_id ) )
}
fn restore_snapshot (
ctx : & Project ,
snapshot_commit_id : git2 ::Oid ,
exclusive_access : & mut WorktreeWritePermission ,
) -> Result < Option < git2 ::Oid > > {
let worktree_dir = ctx . path . as_path ( ) ;
let repo = git2 ::Repository ::open ( worktree_dir ) ? ;
let before_restore_snapshot_result = prepare_snapshot ( ctx , exclusive_access . read_permission ( ) ) ;
let snapshot_commit = repo . find_commit ( snapshot_commit_id ) ? ;
let snapshot_tree = snapshot_commit . tree ( ) ? ;
let vb_toml_entry = snapshot_tree
. get_name ( " virtual_branches.toml " )
. context ( " failed to get virtual_branches.toml blob " ) ? ;
// virtual_branches.toml blob
let vb_toml_blob = repo
. find_blob ( vb_toml_entry . id ( ) )
. context ( " failed to convert virtual_branches tree entry to blob " ) ? ;
if let Err ( err ) = restore_conflicts_tree ( & snapshot_tree , & repo ) {
tracing ::warn! ( " failed to restore conflicts tree - ignoring: {err} " )
}
// make sure we reconstitute any commits that were in the snapshot that are not here for some reason
// for every entry in the virtual_branches subtree, reconsitute the commits
let vb_tree_entry = snapshot_tree
. get_name ( " virtual_branches " )
. context ( " failed to get virtual_branches tree entry " ) ? ;
let vb_tree = repo
. find_tree ( vb_tree_entry . id ( ) )
. context ( " failed to convert virtual_branches tree entry to tree " ) ? ;
// walk through all the entries (branches by id)
let walker = vb_tree . iter ( ) ;
for branch_entry in walker {
let branch_tree = repo
. find_tree ( branch_entry . id ( ) )
. context ( " failed to convert virtual_branches tree entry to tree " ) ? ;
let branch_name = branch_entry . name ( ) ;
let commits_tree_entry = branch_tree
. get_name ( " commits " )
. context ( " failed to get commits tree entry " ) ? ;
let commits_tree = repo
. find_tree ( commits_tree_entry . id ( ) )
. context ( " failed to convert commits tree entry to tree " ) ? ;
// walk through all the commits in the branch
for commit_entry in commits_tree . iter ( ) {
// for each commit, recreate the commit from the commit data if it doesn't exist
if let Some ( commit_id ) = commit_entry . name ( ) {
// check for the oid in the repo
let commit_oid = git2 ::Oid ::from_str ( commit_id ) ? ;
if repo . find_commit ( commit_oid ) . is_err ( ) {
// commit is not in the repo, let's build it from our data
let new_commit_oid = deserialize_commit ( & repo , & commit_entry ) ? ;
if new_commit_oid ! = commit_oid {
bail! ( " commit id mismatch: failed to recreate a commit from its parts " ) ;
}
}
// if branch_name is 'integration', we need to create or update the gitbutler/integration branch
if branch_name = = Some ( " integration " ) {
// TODO(ST): with `gitoxide`, just update the branch without this dance,
// similar to `git update-ref`.
// Then a missing integration branch also doesn't have to be
// fatal, but we wouldn't want to `set_head()` if we are
// not already on the integration branch.
let mut integration_ref = repo . integration_ref_from_head ( ) ? ;
// reset the branch if it's there, otherwise bail as we don't meddle with other branches
// need to detach the head for just a moment.
repo . set_head_detached ( commit_oid ) ? ;
integration_ref . delete ( ) ? ;
// ok, now we set the branch to what it was and update HEAD
let integration_commit = repo . find_commit ( commit_oid ) ? ;
repo . branch ( " gitbutler/integration " , & integration_commit , true ) ? ;
// make sure head is gitbutler/integration
repo . set_head ( " refs/heads/gitbutler/integration " ) ? ;
}
}
}
}
repo . integration_ref_from_head ( ) . context (
" We will not change a worktree which for some reason isn't on the integration branch " ,
) ? ;
let workdir_tree_id = tree_from_applied_vbranches ( & repo , snapshot_commit_id ) ? ;
let workdir_tree = repo . find_tree ( workdir_tree_id ) ? ;
2024-08-28 22:52:48 +03:00
repo . ignore_large_files_in_diffs ( SNAPSHOT_FILE_LIMIT_BYTES ) ? ;
2024-07-14 22:51:28 +03:00
// Define the checkout builder
let mut checkout_builder = git2 ::build ::CheckoutBuilder ::new ( ) ;
checkout_builder . remove_untracked ( true ) ;
checkout_builder . force ( ) ;
// Checkout the tree
repo . checkout_tree ( workdir_tree . as_object ( ) , Some ( & mut checkout_builder ) ) ? ;
// Update virtual_branches.toml with the state from the snapshot
fs ::write (
repo . path ( ) . join ( " gitbutler " ) . join ( " virtual_branches.toml " ) ,
vb_toml_blob . content ( ) ,
) ? ;
// reset the repo index to our index tree
let index_tree_entry = snapshot_tree
. get_name ( " index " )
. context ( " failed to get virtual_branches.toml blob " ) ? ;
let index_tree = repo
. find_tree ( index_tree_entry . id ( ) )
. context ( " failed to convert index tree entry to tree " ) ? ;
let mut index = repo . index ( ) ? ;
index . read_tree ( & index_tree ) ? ;
let restored_operation = snapshot_commit
. message ( )
. and_then ( | msg | SnapshotDetails ::from_str ( msg ) . ok ( ) )
. map ( | d | d . operation . to_string ( ) )
. unwrap_or_default ( ) ;
// create new snapshot
let before_restore_snapshot_tree_id = before_restore_snapshot_result ? ;
let restored_date_ms = snapshot_commit . time ( ) . seconds ( ) * 1000 ;
let details = SnapshotDetails {
version : Default ::default ( ) ,
operation : OperationKind ::RestoreFromSnapshot ,
title : " Restored from snapshot " . to_string ( ) ,
body : None ,
trailers : vec ! [
Trailer {
key : " restored_from " . to_string ( ) ,
value : snapshot_commit_id . to_string ( ) ,
} ,
Trailer {
key : " restored_operation " . to_string ( ) ,
value : restored_operation ,
} ,
Trailer {
key : " restored_date " . to_string ( ) ,
value : restored_date_ms . to_string ( ) ,
} ,
] ,
} ;
commit_snapshot (
ctx ,
before_restore_snapshot_tree_id ,
details ,
exclusive_access ,
)
}
2024-05-05 21:08:34 +03:00
2024-05-28 19:54:50 +03:00
/// 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
2024-05-27 16:23:04 +03:00
fn restore_conflicts_tree ( snapshot_tree : & git2 ::Tree , repo : & git2 ::Repository ) -> Result < ( ) > {
2024-05-05 18:31:29 +03:00
let conflicts_tree_entry = snapshot_tree
. get_name ( " conflicts " )
2024-05-27 16:23:04 +03:00
. context ( " failed to get conflicts tree entry " ) ? ;
let conflicts_tree = repo . find_tree ( conflicts_tree_entry . id ( ) ) ? ;
let base_merge_parent_entry = conflicts_tree . get_name ( " base_merge_parent " ) ;
let base_merge_parent_path = repo . path ( ) . join ( " base_merge_parent " ) ;
if let Some ( base_merge_parent_blob ) = base_merge_parent_entry {
let base_merge_parent_blob = repo
. find_blob ( base_merge_parent_blob . id ( ) )
. context ( " failed to convert base_merge_parent tree entry to blob " ) ? ;
fs ::write ( base_merge_parent_path , base_merge_parent_blob . content ( ) ) ? ;
} else if base_merge_parent_path . exists ( ) {
fs ::remove_file ( base_merge_parent_path ) ? ;
2024-05-05 18:31:29 +03:00
}
2024-05-27 16:23:04 +03:00
let conflicts_entry = conflicts_tree . get_name ( " conflicts " ) ;
let conflicts_path = repo . path ( ) . join ( " conflicts " ) ;
if let Some ( conflicts_entry ) = conflicts_entry {
let conflicts_blob = repo
. find_blob ( conflicts_entry . id ( ) )
. context ( " failed to convert conflicts tree entry to blob " ) ? ;
fs ::write ( conflicts_path , conflicts_blob . content ( ) ) ? ;
} else if conflicts_path . exists ( ) {
fs ::remove_file ( conflicts_path ) ? ;
2024-05-05 18:31:29 +03:00
}
Ok ( ( ) )
}
2024-05-27 16:23:04 +03:00
fn write_conflicts_tree (
worktree_dir : & std ::path ::Path ,
repo : & git2 ::Repository ,
2024-06-05 23:56:03 +03:00
) -> Result < git2 ::Oid > {
2024-05-27 16:23:04 +03:00
let git_dir = worktree_dir . join ( " .git " ) ;
let merge_parent_path = git_dir . join ( " base_merge_parent " ) ;
2024-05-05 18:31:29 +03:00
let merge_parent_blob = if merge_parent_path . exists ( ) {
let merge_parent_content = fs ::read ( merge_parent_path ) ? ;
Some ( repo . blob ( & merge_parent_content ) ? )
} else {
None
} ;
2024-05-27 16:23:04 +03:00
let conflicts_path = git_dir . join ( " conflicts " ) ;
2024-05-05 18:31:29 +03:00
let conflicts_blob = if conflicts_path . exists ( ) {
let conflicts_content = fs ::read ( conflicts_path ) ? ;
Some ( repo . blob ( & conflicts_content ) ? )
} else {
None
} ;
let mut tree_builder = repo . treebuilder ( None ) ? ;
if merge_parent_blob . is_some ( ) {
tree_builder . insert (
" base_merge_parent " ,
merge_parent_blob . unwrap ( ) ,
FileMode ::Blob . into ( ) ,
) ? ;
}
if conflicts_blob . is_some ( ) {
tree_builder . insert ( " conflicts " , conflicts_blob . unwrap ( ) , FileMode ::Blob . into ( ) ) ? ;
}
let conflicts_tree = tree_builder . write ( ) ? ;
2024-06-05 23:56:03 +03:00
Ok ( conflicts_tree )
2024-05-05 18:31:29 +03:00
}
2024-05-28 19:54:50 +03:00
/// Returns the number of lines of code (added + removed) since the last snapshot in `project`.
/// Includes untracked files.
/// `repo` is an already opened project repository.
2024-05-23 23:17:38 +03:00
///
/// If there are no snapshots, 0 is returned.
2024-05-28 19:54:50 +03:00
fn lines_since_snapshot ( project : & Project , repo : & git2 ::Repository ) -> Result < usize > {
// This looks at the diff between the tree of the currently selected as 'default' branch (where new changes go)
2024-05-23 23:17:38 +03:00
// and that same tree in the last snapshot. For some reason, comparing workdir to the workdir subree from
// the snapshot simply does not give us what we need here, so instead using tree to tree comparison.
2024-08-28 22:52:48 +03:00
repo . ignore_large_files_in_diffs ( SNAPSHOT_FILE_LIMIT_BYTES ) ? ;
2024-05-23 23:17:38 +03:00
let oplog_state = OplogHandle ::new ( & project . gb_dir ( ) ) ;
2024-05-28 19:54:50 +03:00
let Some ( oplog_commit_id ) = oplog_state . oplog_head ( ) ? else {
2024-05-23 23:17:38 +03:00
return Ok ( 0 ) ;
2024-05-26 16:48:36 +03:00
} ;
2024-05-23 23:17:38 +03:00
2024-07-10 17:18:34 +03:00
let vbranches = VirtualBranchesHandle ::new ( project . gb_dir ( ) ) . list_branches_in_workspace ( ) ? ;
2024-05-28 19:54:50 +03:00
let mut lines_changed = 0 ;
2024-07-05 16:56:01 +03:00
let dirty_branches = vbranches . iter ( ) . filter ( | b | ! b . ownership . claims . is_empty ( ) ) ;
2024-05-23 23:37:10 +03:00
for branch in dirty_branches {
2024-05-28 19:54:50 +03:00
lines_changed + = branch_lines_since_snapshot ( branch , repo , oplog_commit_id ) ? ;
2024-05-23 23:17:38 +03:00
}
2024-05-23 23:37:10 +03:00
Ok ( lines_changed )
}
2024-08-28 16:32:28 +03:00
#[ instrument(level = tracing::Level::DEBUG, skip(branch, repo), err(Debug)) ]
2024-05-23 23:37:10 +03:00
fn branch_lines_since_snapshot (
branch : & Branch ,
repo : & git2 ::Repository ,
2024-06-05 23:56:03 +03:00
head_sha : git2 ::Oid ,
2024-05-23 23:37:10 +03:00
) -> Result < usize > {
2024-06-05 23:56:03 +03:00
let active_branch_tree = repo . find_tree ( branch . tree ) ? ;
2024-05-23 23:17:38 +03:00
2024-06-05 23:56:03 +03:00
let commit = repo . find_commit ( head_sha ) ? ;
2024-05-23 23:17:38 +03:00
let head_tree = commit . tree ( ) ? ;
let virtual_branches = head_tree
. get_name ( " virtual_branches " )
2024-05-27 16:23:04 +03:00
. ok_or_else ( | | anyhow! ( " failed to get virtual_branches tree entry " ) ) ? ;
2024-05-23 23:17:38 +03:00
let virtual_branches = repo . find_tree ( virtual_branches . id ( ) ) ? ;
let old_active_branch = virtual_branches
2024-05-23 23:37:10 +03:00
. get_name ( branch . id . to_string ( ) . as_str ( ) )
2024-05-27 16:23:04 +03:00
. ok_or_else ( | | anyhow! ( " failed to get active branch from tree entry " ) ) ? ;
2024-05-23 23:17:38 +03:00
let old_active_branch = repo . find_tree ( old_active_branch . id ( ) ) ? ;
let old_active_branch_tree = old_active_branch
. get_name ( " tree " )
2024-05-27 16:23:04 +03:00
. ok_or_else ( | | anyhow! ( " failed to get integration tree entry " ) ) ? ;
2024-05-23 23:17:38 +03:00
let old_active_branch_tree = repo . find_tree ( old_active_branch_tree . id ( ) ) ? ;
let mut opts = git2 ::DiffOptions ::new ( ) ;
opts . include_untracked ( true ) ;
opts . ignore_submodules ( true ) ;
let diff = repo . diff_tree_to_tree (
Some ( & active_branch_tree ) ,
Some ( & old_active_branch_tree ) ,
Some ( & mut opts ) ,
) ;
let stats = diff ? . stats ( ) ? ;
Ok ( stats . deletions ( ) + stats . insertions ( ) )
}
2024-05-27 16:23:04 +03:00
fn serialize_commit ( commit : & git2 ::Commit < '_ > ) -> Vec < u8 > {
let commit_header = commit . raw_header_bytes ( ) ;
let commit_message = commit . message_raw_bytes ( ) ;
[ commit_header , b " \n " , commit_message ] . concat ( )
}
2024-05-28 19:54:50 +03:00
/// we get the data from the blob entry and re-create a commit object from it,
/// whose returned id should match the one we stored.
fn deserialize_commit (
repo : & git2 ::Repository ,
commit_entry : & git2 ::TreeEntry ,
) -> Result < git2 ::Oid > {
let commit_tree = repo
. find_tree ( commit_entry . id ( ) )
. context ( " failed to convert commit tree entry to tree " ) ? ;
let commit_blob_entry = commit_tree
. get_name ( " commit " )
. context ( " failed to get workdir tree entry " ) ? ;
let commit_blob = repo
. find_blob ( commit_blob_entry . id ( ) )
. context ( " failed to convert commit tree entry to blob " ) ? ;
let new_commit_oid = repo
. odb ( ) ?
. write ( git2 ::ObjectType ::Commit , commit_blob . content ( ) ) ? ;
Ok ( new_commit_oid )
}
2024-06-11 18:07:04 +03:00
/// Creates a tree that is the merge of all applied branches from a given snapshot and returns the tree id.
fn tree_from_applied_vbranches (
repo : & git2 ::Repository ,
snapshot_commit_id : git2 ::Oid ,
) -> Result < git2 ::Oid > {
let snapshot_commit = repo . find_commit ( snapshot_commit_id ) ? ;
let snapshot_tree = snapshot_commit . tree ( ) ? ;
let target_tree_entry = snapshot_tree
. get_name ( " target_tree " )
. context ( " failed to get target tree entry " ) ? ;
let target_tree = repo
. find_tree ( target_tree_entry . id ( ) )
. context ( " failed to convert target tree entry to tree " ) ? ;
let vb_toml_entry = snapshot_tree
. get_name ( " virtual_branches.toml " )
. context ( " failed to get virtual_branches.toml blob " ) ? ;
// virtual_branches.toml blob
let vb_toml_blob = repo
. find_blob ( vb_toml_entry . id ( ) )
. context ( " failed to convert virtual_branches tree entry to blob " ) ? ;
2024-07-07 22:26:07 +03:00
let vbs_from_toml : VirtualBranchesState = toml ::from_str ( from_utf8 ( vb_toml_blob . content ( ) ) ? ) ? ;
2024-06-11 18:07:04 +03:00
let applied_branch_trees : Vec < git2 ::Oid > = vbs_from_toml
2024-07-05 16:58:35 +03:00
. list_branches_in_workspace ( ) ?
2024-07-05 16:56:01 +03:00
. iter ( )
2024-06-11 18:07:04 +03:00
. map ( | b | b . tree )
. collect ( ) ;
let mut workdir_tree_id = target_tree . id ( ) ;
let base_tree = target_tree ;
let mut current_ours = base_tree . clone ( ) ;
for branch in applied_branch_trees {
let branch_tree = repo . find_tree ( branch ) ? ;
2024-07-20 16:51:46 +03:00
let mut merge_options : git2 ::MergeOptions = git2 ::MergeOptions ::new ( ) ;
merge_options . fail_on_conflict ( false ) ;
let mut workdir_temp_index = repo . merge_trees (
& base_tree ,
& current_ours ,
& branch_tree ,
Some ( & merge_options ) ,
) ? ;
match workdir_temp_index . write_tree_to ( repo ) {
Ok ( id ) = > {
workdir_tree_id = id ;
current_ours = repo . find_tree ( workdir_tree_id ) ? ;
}
Err ( _err ) = > {
tracing ::warn! ( " Failed to merge tree {branch} - this branch is probably applied at a time when it should not be " ) ;
}
}
2024-06-11 18:07:04 +03:00
}
Ok ( workdir_tree_id )
}