2024-05-24 13:09:15 +03:00
use anyhow ::anyhow ;
2024-05-26 16:48:36 +03:00
use git2 ::FileMode ;
2024-04-29 21:28:27 +03:00
use itertools ::Itertools ;
2024-05-09 21:24:42 +03:00
use std ::collections ::HashMap ;
2024-04-27 00:20:31 +03:00
use std ::str ::FromStr ;
2024-05-24 13:09:15 +03:00
use std ::time ::Duration ;
2024-05-09 21:24:42 +03:00
use std ::{ fs , path ::PathBuf } ;
2024-04-27 00:20:31 +03:00
2024-04-25 11:37:24 +03:00
use anyhow ::Result ;
2024-05-26 10:52:43 +03:00
use tracing ::instrument ;
2024-04-25 11:37:24 +03:00
2024-05-09 21:24:42 +03:00
use crate ::git ::diff ::FileDiff ;
2024-05-23 23:37:10 +03:00
use crate ::virtual_branches ::Branch ;
2024-05-26 16:48:36 +03:00
use crate ::{ git , git ::diff ::hunks_by_filepath , projects ::Project } ;
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-05 23:28:12 +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:
/// .
/// ├── workdir/
/// ├── virtual_branches
/// │ └── [branch-id]
/// │ ├── commit-message.txt
/// │ └── tree (subtree)
/// │ └── [branch-id]
/// │ ├── commit-message.txt
/// │ └── tree (subtree)
/// └── virtual_branches.toml
2024-05-26 15:34:45 +03:00
impl Project {
2024-05-24 01:16:40 +03:00
/// Prepares a snapshot of the current state of the working directory as well as GitButler data.
/// Returns a tree sha of the snapshot. The snapshot is not discoverable until it is comitted with `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-05-26 16:48:36 +03:00
pub ( crate ) fn prepare_snapshot ( & self ) -> Result < git ::Oid > {
2024-05-05 23:28:12 +03:00
let repo_path = self . path . as_path ( ) ;
let repo = git2 ::Repository ::init ( repo_path ) ? ;
2024-04-25 11:37:24 +03:00
2024-05-06 21:02:40 +03:00
let vb_state = self . virtual_branches ( ) ;
2024-05-16 11:51:31 +03:00
// grab the target tree sha
2024-05-05 23:28:12 +03:00
let default_target_sha = vb_state . get_default_target ( ) ? . sha ;
2024-05-16 11:51:31 +03:00
let default_target = repo . find_commit ( default_target_sha . into ( ) ) ? ;
let target_tree_oid = default_target . tree_id ( ) ;
2024-04-25 14:50:27 +03:00
2024-05-05 23:28:12 +03:00
// Create a blob out of `.git/gitbutler/virtual_branches.toml`
let vb_path = repo_path
. join ( " .git " )
. join ( " gitbutler " )
. join ( " virtual_branches.toml " ) ;
let vb_content = fs ::read ( vb_path ) ? ;
let vb_blob = repo . blob ( & vb_content ) ? ;
2024-04-25 11:37:24 +03:00
2024-05-05 23:28:12 +03:00
// Create a tree out of the conflicts state if present
let conflicts_tree = write_conflicts_tree ( repo_path , & repo ) ? ;
2024-05-05 18:31:29 +03:00
2024-05-16 11:51:31 +03:00
// write out the index as a tree to store
2024-05-05 23:28:12 +03:00
let mut index = repo . index ( ) ? ;
2024-05-16 11:51:31 +03:00
let index_tree_oid = index . write_tree ( ) ? ;
2024-05-03 12:13:55 +03:00
2024-05-16 11:51:31 +03:00
// start building our snapshot tree
2024-05-05 23:28:12 +03:00
let mut tree_builder = repo . treebuilder ( None ) ? ;
2024-05-16 11:51:31 +03:00
tree_builder . insert ( " index " , index_tree_oid , FileMode ::Tree . into ( ) ) ? ;
tree_builder . insert ( " target_tree " , target_tree_oid , FileMode ::Tree . into ( ) ) ? ;
2024-05-26 16:48:36 +03:00
tree_builder . insert ( " conflicts " , conflicts_tree . into ( ) , FileMode ::Tree . into ( ) ) ? ;
2024-05-16 11:51:31 +03:00
tree_builder . insert ( " virtual_branches.toml " , vb_blob , 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_trees = Vec ::new ( ) ;
for branch in vb_state . list_branches ( ) ? {
if branch . applied {
head_trees . 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 . into ( ) , FileMode ::Tree . into ( ) ) ? ;
// lets get all the commits between the branch head and the target
let mut revwalk = repo . revwalk ( ) ? ;
revwalk . push ( branch . head . into ( ) ) ? ;
revwalk . hide ( default_target . 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 ) ? ;
// get the raw commit data
let commit_header = commit . raw_header_bytes ( ) ;
let commit_message = commit . message_raw_bytes ( ) ;
let commit_data = [ commit_header , b " \n " , commit_message ] . concat ( ) ;
// convert that data into a blob
let commit_data_blob = repo . blob ( & commit_data ) ? ;
commit_tree_builder . insert ( " commit " , commit_data_blob , FileMode ::Blob . into ( ) ) ? ;
commit_tree_builder . insert ( " tree " , commit_tree . id ( ) , FileMode ::Tree . into ( ) ) ? ;
let commit_tree_id = commit_tree_builder . write ( ) ? ;
2024-05-17 16:16:04 +03:00
commits_tree_builder . insert (
& commit_id . to_string ( ) ,
commit_tree_id ,
FileMode ::Tree . into ( ) ,
) ? ;
2024-05-16 11:51:31 +03:00
}
2024-04-25 11:37:24 +03:00
2024-05-16 11:51:31 +03:00
let commits_tree_id = commits_tree_builder . write ( ) ? ;
2024-05-17 16:16:04 +03:00
branch_tree_builder . insert ( " commits " , commits_tree_id , FileMode ::Tree . into ( ) ) ? ;
2024-05-16 11:51:31 +03:00
let branch_tree_id = branch_tree_builder . write ( ) ? ;
2024-05-17 16:16:04 +03:00
branches_tree_builder . insert (
& branch . id . to_string ( ) ,
branch_tree_id ,
FileMode ::Tree . into ( ) ,
) ? ;
2024-05-16 11:51:31 +03:00
}
2024-05-18 14:50:22 +03:00
// also add the gitbutler/integration commit to the branches tree
let head = repo . head ( ) ? ;
if head . is_branch ( ) & & head . name ( ) . unwrap ( ) = = " refs/heads/gitbutler/integration " {
let commit = head . peel_to_commit ( ) ? ;
let commit_tree = commit . tree ( ) ? ;
let mut commit_tree_builder = repo . treebuilder ( None ) ? ;
// get the raw commit data
let commit_header = commit . raw_header_bytes ( ) ;
let commit_message = commit . message_raw_bytes ( ) ;
let commit_data = [ commit_header , b " \n " , commit_message ] . concat ( ) ;
// convert that data into a blob
let commit_data_blob = repo . blob ( & commit_data ) ? ;
commit_tree_builder . insert ( " commit " , commit_data_blob , FileMode ::Blob . into ( ) ) ? ;
commit_tree_builder . insert ( " tree " , commit_tree . id ( ) , FileMode ::Tree . into ( ) ) ? ;
2024-05-19 08:24:15 +03:00
2024-05-18 14:50:22 +03:00
let commit_tree_id = commit_tree_builder . write ( ) ? ;
// gotta make a subtree to match
let mut commits_tree_builder = repo . treebuilder ( None ) ? ;
2024-05-19 08:24:15 +03:00
commits_tree_builder . insert (
commit . id ( ) . to_string ( ) ,
commit_tree_id ,
FileMode ::Tree . into ( ) ,
) ? ;
2024-05-18 14:50:22 +03:00
let commits_tree_id = commits_tree_builder . write ( ) ? ;
let mut branch_tree_builder = repo . treebuilder ( None ) ? ;
branch_tree_builder . insert ( " tree " , commit_tree . id ( ) , FileMode ::Tree . into ( ) ) ? ;
branch_tree_builder . insert ( " commits " , commits_tree_id , FileMode ::Tree . into ( ) ) ? ;
let branch_tree_id = branch_tree_builder . write ( ) ? ;
2024-05-19 08:24:15 +03:00
branches_tree_builder . insert ( " integration " , branch_tree_id , FileMode ::Tree . into ( ) ) ? ;
2024-05-18 14:50:22 +03:00
}
2024-05-16 11:51:31 +03:00
let branch_tree_id = branches_tree_builder . write ( ) ? ;
tree_builder . insert ( " virtual_branches " , branch_tree_id , FileMode ::Tree . into ( ) ) ? ;
// merge all the branch trees together, this should be our worktree
// TODO: when we implement sub-hunk splitting, this merge logic will need to incorporate that
2024-05-17 16:16:04 +03:00
if head_trees . is_empty ( ) {
// if there are no applied branches, then it's just the target tree
2024-05-16 11:51:31 +03:00
tree_builder . insert ( " workdir " , target_tree_oid , FileMode ::Tree . into ( ) ) ? ;
2024-05-17 16:16:04 +03:00
} else if head_trees . len ( ) = = 1 {
// if there is just one applied branch, then it's just that branch tree
2024-05-16 11:51:31 +03:00
tree_builder . insert ( " workdir " , head_trees [ 0 ] . into ( ) , FileMode ::Tree . into ( ) ) ? ;
} else {
// otherwise merge one branch tree at a time with target_tree_oid as the base
let mut workdir_tree_oid = target_tree_oid ;
let base_tree = repo . find_tree ( target_tree_oid ) ? ;
let mut current_ours = base_tree . clone ( ) ;
let head_trees_iter = head_trees . iter ( ) ;
// iterate through all head trees
for head_tree in head_trees_iter {
2024-05-26 16:48:36 +03:00
let current_theirs = repo . find_tree ( ( * head_tree ) . into ( ) ) ? ;
2024-05-17 16:16:04 +03:00
let mut workdir_temp_index =
repo . merge_trees ( & base_tree , & current_ours , & current_theirs , None ) ? ;
2024-05-16 11:51:31 +03:00
workdir_tree_oid = workdir_temp_index . write_tree_to ( & repo ) ? ;
current_ours = current_theirs ;
}
tree_builder . insert ( " workdir " , workdir_tree_oid , FileMode ::Tree . into ( ) ) ? ;
}
2024-05-17 16:16:04 +03:00
2024-05-16 11:51:31 +03:00
// ok, write out the final oplog tree
2024-05-05 23:28:12 +03:00
let tree_id = tree_builder . write ( ) ? ;
2024-05-26 16:48:36 +03:00
Ok ( tree_id . into ( ) )
2024-05-24 01:16:40 +03:00
}
2024-05-26 15:34:45 +03:00
/// Commits the snapshot tree that is created with the `prepare_snapshot` method.
/// Committing it makes the snapshot discoverable in `list_snapshots` as well as restorable with `restore_snapshot`.
/// Returns the sha of the created snapshot commit or None if snapshots are disabled.
pub ( crate ) fn commit_snapshot (
& self ,
2024-05-26 16:48:36 +03:00
tree_id : git ::Oid ,
2024-05-26 15:34:45 +03:00
details : SnapshotDetails ,
2024-05-26 16:48:36 +03:00
) -> Result < Option < git ::Oid > > {
2024-05-24 01:16:40 +03:00
let repo_path = self . path . as_path ( ) ;
let repo = git2 ::Repository ::init ( repo_path ) ? ;
2024-05-26 16:48:36 +03:00
let tree = repo . find_tree ( tree_id . into ( ) ) ? ;
2024-05-24 01:16:40 +03:00
let oplog_state = OplogHandle ::new ( & self . gb_dir ( ) ) ;
let oplog_head_commit = match oplog_state . get_oplog_head ( ) ? {
2024-05-26 16:48:36 +03:00
Some ( head_sha ) = > match repo . find_commit ( head_sha . into ( ) ) {
2024-05-24 01:16:40 +03:00
Ok ( commit ) = > Some ( commit ) ,
Err ( _ ) = > None , // cant find the old one, start over
} ,
// This is the first snapshot - no parents
None = > None ,
} ;
2024-04-25 11:37:24 +03:00
2024-05-08 04:12:17 +03:00
// Check if there is a difference between the tree and the parent tree, and if not, return so that we dont create noop snapshots
2024-05-16 11:51:31 +03:00
if let Some ( ref head_commit ) = oplog_head_commit {
let parent_tree = head_commit . tree ( ) ? ;
let diff = repo . diff_tree_to_tree ( Some ( & parent_tree ) , Some ( & tree ) , None ) ? ;
if diff . deltas ( ) . count ( ) = = 0 {
return Ok ( None ) ;
}
2024-05-08 04:12:17 +03:00
}
2024-05-05 23:28:12 +03:00
// Construct a new commit
let name = " GitButler " ;
let email = " gitbutler@gitbutler.com " ;
let signature = git2 ::Signature ::now ( name , email ) . unwrap ( ) ;
2024-05-16 11:51:31 +03:00
let parents = if let Some ( ref oplog_head_commit ) = oplog_head_commit {
vec! [ oplog_head_commit ]
} else {
vec! [ ]
} ;
2024-05-05 23:28:12 +03:00
let new_commit_oid = repo . commit (
None ,
& signature ,
& signature ,
& details . to_string ( ) ,
& tree ,
2024-05-16 11:51:31 +03:00
parents . as_slice ( ) ,
2024-05-05 23:28:12 +03:00
) ? ;
2024-05-26 16:48:36 +03:00
oplog_state . set_oplog_head ( new_commit_oid . into ( ) ) ? ;
2024-04-25 11:37:24 +03:00
2024-05-24 01:16:40 +03:00
let vb_state = self . virtual_branches ( ) ;
// grab the target tree sha
let default_target_sha = vb_state . get_default_target ( ) ? . sha ;
2024-05-26 16:48:36 +03:00
set_reference_to_oplog ( self , default_target_sha , new_commit_oid . into ( ) ) ? ;
2024-04-25 11:37:24 +03:00
2024-05-26 16:48:36 +03:00
Ok ( Some ( new_commit_oid . into ( ) ) )
2024-05-05 23:28:12 +03:00
}
2024-04-25 11:37:24 +03:00
2024-05-26 15:34:45 +03:00
/// Creates a snapshot of the current state of the working directory as well as GitButler data.
/// This is a convinience method that combines `prepare_snapshot` and `commit_snapshot`.
///
2024-05-26 10:52:43 +03:00
/// Note that errors in snapshot creation is typically ignored, so we want to learn about them.
#[ instrument(skip(details), err(Debug)) ]
2024-05-26 16:48:36 +03:00
pub fn create_snapshot ( & self , details : SnapshotDetails ) -> Result < Option < git ::Oid > > {
2024-05-24 01:16:40 +03:00
let tree_id = self . prepare_snapshot ( ) ? ;
self . commit_snapshot ( tree_id , details )
}
2024-05-26 15:34:45 +03:00
/// Lists the snapshots that have been created for the given repository, up to the given limit.
/// An alternative way of retrieving the snapshots would be to manually the oplog head `git log <oplog_head>` available in `.git/gitbutler/operations-log.toml`.
///
/// If there are no snapshots, an empty list is returned.
2024-05-26 16:48:36 +03:00
pub fn list_snapshots ( & self , limit : usize , sha : Option < git ::Oid > ) -> Result < Vec < Snapshot > > {
2024-05-05 23:28:12 +03:00
let repo_path = self . path . as_path ( ) ;
let repo = git2 ::Repository ::init ( repo_path ) ? ;
2024-04-25 11:37:24 +03:00
2024-05-07 01:43:45 +03:00
let head_sha = match sha {
Some ( sha ) = > sha ,
None = > {
let oplog_state = OplogHandle ::new ( & self . gb_dir ( ) ) ;
if let Some ( sha ) = oplog_state . get_oplog_head ( ) ? {
sha
} else {
// there are no snapshots so return an empty list
return Ok ( vec! [ ] ) ;
}
}
} ;
2024-05-05 23:28:12 +03:00
2024-05-26 16:48:36 +03:00
let oplog_head_commit = repo . find_commit ( head_sha . into ( ) ) ? ;
2024-05-05 23:28:12 +03:00
let mut revwalk = repo . revwalk ( ) ? ;
revwalk . push ( oplog_head_commit . id ( ) ) ? ;
let mut snapshots = Vec ::new ( ) ;
for commit_id in revwalk {
let commit_id = commit_id ? ;
let commit = repo . find_commit ( commit_id ) ? ;
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-05-17 16:16:04 +03:00
let wd_tree_entry = tree . get_name ( " workdir " ) ;
2024-05-06 16:26:44 +03:00
let tree = if let Some ( wd_tree_entry ) = wd_tree_entry {
repo . find_tree ( wd_tree_entry . id ( ) ) ?
} else {
// We reached a tree that is not a snapshot
continue ;
} ;
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 ) {
let parent_tree = parent . tree ( ) ? ;
let parent_tree_entry = parent_tree . get_name ( " workdir " ) ;
let parent_tree = parent_tree_entry
. map ( | entry | repo . find_tree ( entry . id ( ) ) )
. transpose ( ) ? ;
let diff = repo . diff_tree_to_tree ( parent_tree . as_ref ( ) , Some ( & tree ) , None ) ? ;
let stats = diff . stats ( ) ? ;
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
} ) ? ;
let lines_added = stats . insertions ( ) ;
let lines_removed = stats . deletions ( ) ;
snapshots . push ( Snapshot {
2024-05-26 16:48:36 +03:00
id : commit_id . into ( ) ,
2024-05-16 12:19:38 +03:00
details ,
lines_added ,
lines_removed ,
files_changed ,
2024-05-26 10:52:43 +03:00
created_at : commit . time ( ) ,
2024-05-16 12:19:38 +03:00
} ) ;
if snapshots . len ( ) > = limit {
break ;
}
} else {
// this is the very first snapshot
snapshots . push ( Snapshot {
2024-05-26 16:48:36 +03:00
id : commit_id . into ( ) ,
2024-05-16 12:19:38 +03:00
details ,
lines_added : 0 ,
lines_removed : 0 ,
files_changed : Vec ::new ( ) , // Fix: Change 0 to an empty vector
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-05-26 15:34:45 +03:00
/// Reverts to a previous state of the working directory, virtual branches and commits.
/// The provided sha must refer to a valid snapshot commit.
/// Upon success, a new snapshot is created.
///
/// This will restore the following:
/// - The state of the working directory is checked out from the subtree `workdir` in the snapshot.
/// - The state of virtual branches is restored from the blob `virtual_branches.toml` in the snapshot.
/// - The state of conflicts (.git/base_merge_parent and .git/conflicts) is restored from the subtree `conflicts` in the snapshot (if not present, existing files are deleted).
///
/// If there are files that are untracked and larger than `SNAPSHOT_FILE_LIMIT_BYTES`, they are excluded from snapshot creation and restoring.
/// Returns the sha of the created revert snapshot commit or None if snapshots are disabled.
2024-05-26 16:48:36 +03:00
pub fn restore_snapshot ( & self , sha : git ::Oid ) -> Result < Option < git ::Oid > > {
2024-05-05 23:28:12 +03:00
let repo_path = self . path . as_path ( ) ;
let repo = git2 ::Repository ::init ( repo_path ) ? ;
2024-05-24 01:23:34 +03:00
// prepare snapshot
let snapshot_tree = self . prepare_snapshot ( ) ;
2024-05-26 16:48:36 +03:00
let commit = repo . find_commit ( sha . into ( ) ) ? ;
2024-05-05 23:28:12 +03:00
// Top tree
2024-05-16 11:51:31 +03:00
let top_tree = commit . tree ( ) ? ;
let vb_tree_entry = top_tree
2024-05-05 23:28:12 +03:00
. get_name ( " virtual_branches.toml " )
2024-05-16 11:51:31 +03:00
. ok_or ( anyhow! ( " failed to get virtual_branches.toml blob " ) ) ? ;
2024-05-05 23:28:12 +03:00
// virtual_branches.toml blob
let vb_blob = vb_tree_entry
. to_object ( & repo ) ?
. into_blob ( )
. map_err ( | _ | anyhow! ( " failed to convert virtual_branches tree entry to blob " ) ) ? ;
// Restore the state of .git/base_merge_parent and .git/conflicts from the snapshot
// Will remove those files if they are not present in the snapshot
2024-05-16 11:51:31 +03:00
_ = restore_conflicts_tree ( & top_tree , & repo , repo_path ) ;
let wd_tree_entry = top_tree
2024-05-05 23:28:12 +03:00
. get_name ( " workdir " )
. ok_or ( anyhow! ( " failed to get workdir tree entry " ) ) ? ;
2024-05-16 11:51:31 +03:00
// make sure we reconstitute any commits that were in the snapshot that are not here for some reason
2024-05-17 16:16:04 +03:00
// for every entry in the virtual_branches subtree, reconsitute the commits
2024-05-16 11:51:31 +03:00
let vb_tree_entry = top_tree
. get_name ( " virtual_branches " )
2024-05-17 16:16:04 +03:00
. ok_or ( anyhow! ( " failed to get virtual_branches tree entry " ) ) ? ;
let vb_tree = vb_tree_entry
. to_object ( & repo ) ?
. into_tree ( )
. map_err ( | _ | anyhow! ( " failed to convert virtual_branches tree entry to tree " ) ) ? ;
2024-05-16 11:51:31 +03:00
// walk through all the entries (branches)
let walker = vb_tree . iter ( ) ;
for branch_entry in walker {
2024-05-17 16:16:04 +03:00
let branch_tree = branch_entry
. to_object ( & repo ) ?
. into_tree ( )
. map_err ( | _ | anyhow! ( " failed to convert virtual_branches tree entry to tree " ) ) ? ;
2024-05-18 14:50:22 +03:00
let branch_name = branch_entry . name ( ) ;
2024-05-17 16:16:04 +03:00
let commits_tree_entry = branch_tree
. get_name ( " commits " )
. ok_or ( anyhow! ( " failed to get commits tree entry " ) ) ? ;
let commits_tree = commits_tree_entry
. to_object ( & repo ) ?
. into_tree ( )
. map_err ( | _ | anyhow! ( " failed to convert commits tree entry to tree " ) ) ? ;
2024-05-16 11:51:31 +03:00
// walk through all the commits in the branch
let commit_walker = commits_tree . iter ( ) ;
for commit_entry in commit_walker {
// 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
2024-05-26 16:48:36 +03:00
let commit_oid = git ::Oid ::from_str ( commit_id ) ? ;
if repo . find_commit ( commit_oid . into ( ) ) . is_err ( ) {
2024-05-18 14:50:22 +03:00
// commit is not in the repo, let's build it from our data
// we get the data from the blob entry and create a commit object from it, which should match the oid of the entry
let commit_tree = commit_entry
. to_object ( & repo ) ?
. into_tree ( )
. map_err ( | _ | anyhow! ( " failed to convert commit tree entry to tree " ) ) ? ;
let commit_blob_entry = commit_tree
. get_name ( " commit " )
. ok_or ( anyhow! ( " failed to get workdir tree entry " ) ) ? ;
let commit_blob = commit_blob_entry
. to_object ( & repo ) ?
. into_blob ( )
. map_err ( | _ | anyhow! ( " failed to convert commit tree entry to blob " ) ) ? ;
let new_commit_oid = repo
. odb ( ) ?
. write ( git2 ::ObjectType ::Commit , commit_blob . content ( ) ) ? ;
2024-05-26 16:48:36 +03:00
if new_commit_oid ! = commit_oid . into ( ) {
2024-05-18 14:50:22 +03:00
return Err ( anyhow! ( " commit oid mismatch " ) ) ;
}
2024-05-16 11:51:31 +03:00
}
2024-05-18 14:50:22 +03:00
// if branch_name is 'integration', we need to create or update the gitbutler/integration branch
if let Some ( branch_name ) = branch_name {
if branch_name = = " integration " {
2024-05-26 16:48:36 +03:00
let integration_commit = repo . find_commit ( commit_oid . into ( ) ) ? ;
2024-05-18 14:50:22 +03:00
// reset the branch if it's there
2024-05-19 08:24:15 +03:00
let branch =
repo . find_branch ( " gitbutler/integration " , git2 ::BranchType ::Local ) ;
2024-05-18 14:50:22 +03:00
if let Ok ( mut branch ) = branch {
// need to detatch the head for just a minuto
2024-05-26 16:48:36 +03:00
repo . set_head_detached ( commit_oid . into ( ) ) ? ;
2024-05-18 14:50:22 +03:00
branch . delete ( ) ? ;
}
// ok, now we set the branch to what it was and update HEAD
repo . branch ( " gitbutler/integration " , & integration_commit , true ) ? ;
// make sure head is gitbutler/integration
repo . set_head ( " refs/heads/gitbutler/integration " ) ? ;
}
2024-05-16 11:51:31 +03:00
}
}
}
}
2024-05-05 23:28:12 +03:00
// workdir tree
2024-05-16 11:51:31 +03:00
let work_tree = repo . find_tree ( wd_tree_entry . id ( ) ) ? ;
2024-05-05 23:28:12 +03:00
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
let files_to_exclude = get_exclude_list ( & repo ) ? ;
// In-memory, libgit2 internal ignore rule
repo . add_ignore_rule ( & files_to_exclude ) ? ;
// Define the checkout builder
let mut checkout_builder = git2 ::build ::CheckoutBuilder ::new ( ) ;
checkout_builder . remove_untracked ( true ) ;
checkout_builder . force ( ) ;
// Checkout the tree
2024-05-16 11:51:31 +03:00
repo . checkout_tree ( work_tree . as_object ( ) , Some ( & mut checkout_builder ) ) ? ;
2024-05-05 23:28:12 +03:00
// Update virtual_branches.toml with the state from the snapshot
fs ::write (
repo_path
. join ( " .git " )
. join ( " gitbutler " )
. join ( " virtual_branches.toml " ) ,
vb_blob . content ( ) ,
) ? ;
2024-04-25 11:37:24 +03:00
2024-05-16 11:51:31 +03:00
// reset the repo index to our index tree
let index_tree_entry = top_tree
. get_name ( " index " )
. ok_or ( anyhow! ( " failed to get virtual_branches.toml blob " ) ) ? ;
let index_tree = index_tree_entry
. to_object ( & repo ) ?
. into_tree ( )
. map_err ( | _ | anyhow! ( " failed to convert index tree entry to tree " ) ) ? ;
let mut index = repo . index ( ) ? ;
index . read_tree ( & index_tree ) ? ;
2024-05-14 17:04:02 +03:00
let restored_operation = commit
. message ( )
. and_then ( | msg | SnapshotDetails ::from_str ( msg ) . ok ( ) )
. map ( | d | d . operation . to_string ( ) )
. unwrap_or_default ( ) ;
let restored_date = commit . time ( ) . seconds ( ) * 1000 ;
2024-05-05 23:28:12 +03:00
// create new snapshot
let details = SnapshotDetails {
version : Default ::default ( ) ,
2024-05-26 10:52:43 +03:00
operation : OperationKind ::RestoreFromSnapshot ,
2024-05-05 23:28:12 +03:00
title : " Restored from snapshot " . to_string ( ) ,
body : None ,
2024-05-14 17:04:02 +03:00
trailers : vec ! [
Trailer {
key : " restored_from " . to_string ( ) ,
2024-05-26 16:48:36 +03:00
value : sha . to_string ( ) ,
2024-05-14 17:04:02 +03:00
} ,
Trailer {
key : " restored_operation " . to_string ( ) ,
value : restored_operation ,
} ,
Trailer {
key : " restored_date " . to_string ( ) ,
value : restored_date . to_string ( ) ,
} ,
] ,
2024-05-05 23:28:12 +03:00
} ;
2024-05-24 01:23:34 +03:00
snapshot_tree . and_then ( | snapshot_tree | self . commit_snapshot ( snapshot_tree , details ) )
2024-05-05 23:28:12 +03:00
}
2024-04-25 11:37:24 +03:00
2024-05-26 15:34:45 +03:00
/// 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
///
/// This implementation works as follows:
/// - 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.
pub fn should_auto_snapshot ( & self ) -> Result < bool > {
2024-05-05 23:28:12 +03:00
let oplog_state = OplogHandle ::new ( & self . gb_dir ( ) ) ;
2024-05-24 13:09:15 +03:00
let last_snapshot_time = oplog_state . get_modified_at ( ) ? ;
if last_snapshot_time . elapsed ( ) ? > Duration ::from_secs ( 300 ) {
2024-05-23 23:17:38 +03:00
let changed_lines = lines_since_snapshot ( self ) ? ;
if changed_lines > self . snapshot_lines_threshold ( ) {
return Ok ( true ) ;
}
2024-05-24 13:09:15 +03:00
} else {
return Ok ( false ) ;
2024-05-23 03:46:40 +03:00
}
2024-05-23 23:17:38 +03:00
Ok ( false )
2024-05-05 23:28:12 +03:00
}
2024-05-09 21:24:42 +03:00
2024-05-26 15:34:45 +03:00
/// 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
2024-05-26 16:48:36 +03:00
pub fn snapshot_diff ( & self , sha : git ::Oid ) -> Result < HashMap < PathBuf , FileDiff > > {
2024-05-09 21:24:42 +03:00
let repo_path = self . path . as_path ( ) ;
let repo = git2 ::Repository ::init ( repo_path ) ? ;
2024-05-26 16:48:36 +03:00
let commit = repo . find_commit ( sha . into ( ) ) ? ;
2024-05-09 21:24:42 +03:00
// Top tree
let tree = commit . tree ( ) ? ;
let old_tree = commit . parent ( 0 ) ? . tree ( ) ? ;
let wd_tree_entry = tree
. get_name ( " workdir " )
. ok_or ( anyhow! ( " failed to get workdir tree entry " ) ) ? ;
let old_wd_tree_entry = old_tree
. get_name ( " workdir " )
. ok_or ( anyhow! ( " failed to get old workdir tree entry " ) ) ? ;
// workdir tree
let wd_tree = repo . find_tree ( wd_tree_entry . id ( ) ) ? ;
let old_wd_tree = repo . find_tree ( old_wd_tree_entry . id ( ) ) ? ;
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
let files_to_exclude = get_exclude_list ( & repo ) ? ;
// In-memory, libgit2 internal ignore rule
repo . add_ignore_rule ( & files_to_exclude ) ? ;
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-05-26 16:48:36 +03:00
pub fn oplog_head ( & self ) -> Result < Option < git ::Oid > > {
2024-05-26 00:07:40 +03:00
let oplog_state = OplogHandle ::new ( & self . gb_dir ( ) ) ;
oplog_state . get_oplog_head ( )
}
2024-05-05 21:08:34 +03:00
}
2024-05-05 18:31:29 +03:00
fn restore_conflicts_tree (
snapshot_tree : & git2 ::Tree ,
repo : & git2 ::Repository ,
repo_path : & std ::path ::Path ,
) -> Result < ( ) > {
let conflicts_tree_entry = snapshot_tree
. get_name ( " conflicts " )
. ok_or ( anyhow! ( " failed to get conflicts tree entry " ) ) ? ;
let tree = repo . find_tree ( conflicts_tree_entry . id ( ) ) ? ;
let base_merge_parent_blob = tree . get_name ( " base_merge_parent " ) ;
2024-05-05 18:55:10 +03:00
let path = repo_path . join ( " .git " ) . join ( " base_merge_parent " ) ;
2024-05-05 18:31:29 +03:00
if let Some ( base_merge_parent_blob ) = base_merge_parent_blob {
let base_merge_parent_blob = base_merge_parent_blob
. to_object ( repo ) ?
. into_blob ( )
. map_err ( | _ | anyhow! ( " failed to convert base_merge_parent tree entry to blob " ) ) ? ;
fs ::write ( path , base_merge_parent_blob . content ( ) ) ? ;
} else if path . exists ( ) {
fs ::remove_file ( path ) ? ;
}
let conflicts_blob = tree . get_name ( " conflicts " ) ;
2024-05-05 18:55:10 +03:00
let path = repo_path . join ( " .git " ) . join ( " conflicts " ) ;
2024-05-05 18:31:29 +03:00
if let Some ( conflicts_blob ) = conflicts_blob {
let conflicts_blob = conflicts_blob
. to_object ( repo ) ?
. into_blob ( )
. map_err ( | _ | anyhow! ( " failed to convert conflicts tree entry to blob " ) ) ? ;
fs ::write ( path , conflicts_blob . content ( ) ) ? ;
} else if path . exists ( ) {
fs ::remove_file ( path ) ? ;
}
Ok ( ( ) )
}
2024-05-26 16:48:36 +03:00
fn write_conflicts_tree ( repo_path : & std ::path ::Path , repo : & git2 ::Repository ) -> Result < git ::Oid > {
2024-05-05 18:55:10 +03:00
let merge_parent_path = repo_path . join ( " .git " ) . 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-05 18:55:10 +03:00
let conflicts_path = repo_path . join ( " .git " ) . 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-05-26 16:48:36 +03:00
Ok ( conflicts_tree . into ( ) )
2024-05-05 18:31:29 +03:00
}
2024-04-29 21:28:27 +03:00
fn get_exclude_list ( repo : & git2 ::Repository ) -> Result < String > {
let repo_path = repo
. path ( )
. parent ( )
. ok_or ( anyhow! ( " failed to get repo path " ) ) ? ;
let statuses = repo . statuses ( None ) ? ;
let mut files_to_exclude = vec! [ ] ;
for entry in statuses . iter ( ) {
if let Some ( path ) = entry . path ( ) {
let path = repo_path . join ( path ) ;
if let Ok ( metadata ) = fs ::metadata ( & path ) {
2024-05-04 17:28:34 +03:00
if metadata . is_file ( )
& & metadata . len ( ) > SNAPSHOT_FILE_LIMIT_BYTES
& & entry . status ( ) . is_wt_new ( )
{
2024-04-29 21:28:27 +03:00
files_to_exclude . push ( path ) ;
}
}
}
}
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
let files_to_exclude = files_to_exclude
. iter ( )
. filter_map ( | f | f . strip_prefix ( repo_path ) . ok ( ) )
. filter_map ( | f | f . to_str ( ) )
. join ( " " ) ;
Ok ( files_to_exclude )
}
2024-05-23 23:17:38 +03:00
/// Returns the number of lines of code (added plus removed) since the last snapshot. Includes untracked files.
///
/// If there are no snapshots, 0 is returned.
fn lines_since_snapshot ( project : & Project ) -> Result < usize > {
// This looks at the diff between the tree of the currenly selected as 'default' branch (where new changes go)
// 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.
let repo_path = project . path . as_path ( ) ;
let repo = git2 ::Repository ::init ( repo_path ) ? ;
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
let files_to_exclude = get_exclude_list ( & repo ) ? ;
// In-memory, libgit2 internal ignore rule
repo . add_ignore_rule ( & files_to_exclude ) ? ;
let oplog_state = OplogHandle ::new ( & project . gb_dir ( ) ) ;
2024-05-26 16:48:36 +03:00
let Some ( head_sha ) = oplog_state . get_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
let vb_state = project . virtual_branches ( ) ;
let binding = vb_state . list_branches ( ) ? ;
2024-05-23 23:37:10 +03:00
let dirty_branches : Vec < & Branch > = binding
2024-05-23 23:17:38 +03:00
. iter ( )
. filter ( | b | b . applied )
2024-05-23 23:37:10 +03:00
. filter ( | b | ! b . ownership . claims . is_empty ( ) )
. collect ( ) ;
let mut lines_changed = 0 ;
for branch in dirty_branches {
2024-05-26 16:48:36 +03:00
lines_changed + = branch_lines_since_snapshot ( branch , & repo , head_sha ) ? ;
2024-05-23 23:17:38 +03:00
}
2024-05-23 23:37:10 +03:00
Ok ( lines_changed )
}
fn branch_lines_since_snapshot (
branch : & Branch ,
repo : & git2 ::Repository ,
2024-05-26 16:48:36 +03:00
head_sha : git ::Oid ,
2024-05-23 23:37:10 +03:00
) -> Result < usize > {
let active_branch_tree = repo . find_tree ( branch . tree . into ( ) ) ? ;
2024-05-23 23:17:38 +03:00
2024-05-26 16:48:36 +03:00
let commit = repo . find_commit ( head_sha . into ( ) ) ? ;
2024-05-23 23:17:38 +03:00
let head_tree = commit . tree ( ) ? ;
let virtual_branches = head_tree
. get_name ( " virtual_branches " )
. ok_or ( anyhow! ( " failed to get virtual_branches tree entry " ) ) ? ;
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-23 23:17:38 +03:00
. ok_or ( anyhow! ( " failed to get active branch from tree entry " ) ) ? ;
let old_active_branch = repo . find_tree ( old_active_branch . id ( ) ) ? ;
let old_active_branch_tree = old_active_branch
. get_name ( " tree " )
. ok_or ( anyhow! ( " failed to get integration tree entry " ) ) ? ;
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 ( ) )
}