diff --git a/crates/gitbutler-core/src/ops/oplog.rs b/crates/gitbutler-core/src/ops/oplog.rs index 8ee95caee..e617dc1ad 100644 --- a/crates/gitbutler-core/src/ops/oplog.rs +++ b/crates/gitbutler-core/src/ops/oplog.rs @@ -24,7 +24,7 @@ pub trait Oplog { /// Creates a snapshot of the current state of the repository and virtual branches using the given label. /// /// If this is the first shapshot created, supporting structures are initialized: - /// - The current oplog head is persisted in `.git/gitbutler/oplog.toml`. + /// - The current oplog head is persisted in `.git/gitbutler/operations-log.toml`. /// - A fake branch `gitbutler/target` is created and maintained in order to keep the oplog head reachable. /// /// The snapshot tree contains: @@ -36,7 +36,7 @@ pub trait Oplog { /// Returns the sha of the created snapshot commit or None if snapshots are disabled. fn create_snapshot(&self, details: SnapshotDetails) -> Result>; /// 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`. + /// An alternative way of retrieving the snapshots would be to manually the oplog head `git log ` available in `.git/gitbutler/operations-log.toml`. /// /// If there are no snapshots, an empty list is returned. fn list_snapshots(&self, limit: usize, sha: Option) -> Result>; @@ -169,6 +169,43 @@ impl Oplog for Project { )?; } + // 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())?; + + let commit_tree_id = commit_tree_builder.write()?; + + // gotta make a subtree to match + let mut commits_tree_builder = repo.treebuilder(None)?; + commits_tree_builder.insert( + commit.id().to_string(), + 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", 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()?; + + 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())?; @@ -374,6 +411,8 @@ impl Oplog for Project { .to_object(&repo)? .into_tree() .map_err(|_| anyhow!("failed to convert virtual_branches tree entry to tree"))?; + let branch_name = branch_entry.name(); + let commits_tree_entry = branch_tree .get_name("commits") .ok_or(anyhow!("failed to get commits tree entry"))?; @@ -389,28 +428,45 @@ impl Oplog for Project { 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_ok() { - continue; // commit is here, so keep going + if repo.find_commit(commit_oid).is_err() { + // 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())?; + if new_commit_oid != commit_oid { + return Err(anyhow!("commit oid mismatch")); + } } - // 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())?; - if new_commit_oid != commit_oid { - return Err(anyhow!("commit oid mismatch")); + // 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" { + let integration_commit = repo.find_commit(commit_oid)?; + // reset the branch if it's there + let branch = + repo.find_branch("gitbutler/integration", git2::BranchType::Local); + if let Ok(mut branch) = branch { + // need to detatch the head for just a minuto + repo.set_head_detached(commit_oid)?; + 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")?; + } } } } diff --git a/crates/gitbutler-core/src/ops/state.rs b/crates/gitbutler-core/src/ops/state.rs index c4ec2e388..947d94f51 100644 --- a/crates/gitbutler-core/src/ops/state.rs +++ b/crates/gitbutler-core/src/ops/state.rs @@ -7,7 +7,7 @@ use std::{ use serde::{Deserialize, Serialize}; -/// This tracks the head of the oplog, persisted in oplog.toml. +/// This tracks the head of the oplog, persisted in operations-log.toml. #[derive(Serialize, Deserialize, Debug, Default)] pub struct Oplog { /// This is the sha of the last oplog commit diff --git a/crates/gitbutler-core/src/virtual_branches/base.rs b/crates/gitbutler-core/src/virtual_branches/base.rs index 398f72efa..f4d7534d2 100644 --- a/crates/gitbutler-core/src/virtual_branches/base.rs +++ b/crates/gitbutler-core/src/virtual_branches/base.rs @@ -282,8 +282,6 @@ pub fn set_target_push_remote( ) -> Result<(), errors::SetBaseBranchError> { let repo = &project_repository.git_repository; - dbg!(push_remote_name); - let remote = repo .find_remote(push_remote_name) .context(format!("failed to find remote {}", push_remote_name))?; diff --git a/crates/gitbutler-core/tests/suite/virtual_branches/oplog.rs b/crates/gitbutler-core/tests/suite/virtual_branches/oplog.rs index 17fbe88a6..812ce3ede 100644 --- a/crates/gitbutler-core/tests/suite/virtual_branches/oplog.rs +++ b/crates/gitbutler-core/tests/suite/virtual_branches/oplog.rs @@ -156,8 +156,68 @@ async fn test_basic_oplog() { assert_eq!(file_lines, "content"); } -// test oplog.toml head is not a commit +#[tokio::test] +async fn test_oplog_restores_gitbutler_integration() { + let Test { + repository, + project_id, + controller, + project, + .. + } = &Test::default(); + controller + .set_base_branch(project_id, &"refs/remotes/origin/master".parse().unwrap()) + .await + .unwrap(); + + let branch_id = controller + .create_virtual_branch(project_id, &branch::BranchCreateRequest::default()) + .await + .unwrap(); + + // create commit + fs::write(repository.path().join("file.txt"), "content").unwrap(); + let _commit1_id = controller + .create_commit(project_id, &branch_id, "commit one", None, false) + .await + .unwrap(); + + let repo = git2::Repository::open(&project.path).unwrap(); + + // check the integration commit + let head = repo.head(); + let commit = &head.as_ref().unwrap().peel_to_commit().unwrap(); + let commit_oid = commit.id(); + let message = commit.summary().unwrap(); + assert_eq!(message, "GitButler Integration Commit"); + + // create second commit + fs::write(repository.path().join("file.txt"), "content").unwrap(); + let _commit2_id = controller + .create_commit(project_id, &branch_id, "commit one", None, false) + .await + .unwrap(); + + // check the integration commit changed + let head = repo.head(); + let commit = &head.as_ref().unwrap().peel_to_commit().unwrap(); + let commit2_oid = commit.id(); + let message = commit.summary().unwrap(); + assert_eq!(message, "GitButler Integration Commit"); + assert_ne!(commit_oid, commit2_oid); + + // restore the first + let snapshots = project.list_snapshots(10, None).unwrap(); + project.restore_snapshot(snapshots[1].clone().id).unwrap(); + + let head = repo.head(); + let commit = &head.as_ref().unwrap().peel_to_commit().unwrap(); + let commit_restore_oid = commit.id(); + assert_eq!(commit_oid, commit_restore_oid); +} + +// test operations-log.toml head is not a commit #[tokio::test] async fn test_oplog_head_corrupt() { let Test { @@ -177,7 +237,11 @@ async fn test_oplog_head_corrupt() { assert_eq!(snapshots.len(), 1); // overwrite oplog head with a non-commit sha - let file_path = repository.path().join(".git").join("operations-log.toml"); + let file_path = repository + .path() + .join(".git") + .join("gitbutler") + .join("operations-log.toml"); fs::write( file_path, "head_sha = \"758d54f587227fba3da3b61fbb54a99c17903d59\"", diff --git a/crates/gitbutler-tauri/src/virtual_branches.rs b/crates/gitbutler-tauri/src/virtual_branches.rs index cb203d30b..bb1ce5ab1 100644 --- a/crates/gitbutler-tauri/src/virtual_branches.rs +++ b/crates/gitbutler-tauri/src/virtual_branches.rs @@ -126,7 +126,6 @@ pub mod commands { branch: &str, push_remote: Option<&str>, // optional different name of a remote to push to (defaults to same as the branch) ) -> Result { - dbg!(&project_id, &branch, &push_remote); let branch_name = format!("refs/remotes/{}", branch) .parse() .context("Invalid branch name")?;