Merge pull request #3787 from gitbutlerapp/save-and-restore-the-gitbutler/integration-branch

save and restore the gitbutler/integration branch
This commit is contained in:
Scott Chacon 2024-05-19 07:36:52 +02:00 committed by GitHub
commit 070a88beb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 145 additions and 28 deletions

View File

@ -24,7 +24,7 @@ pub trait Oplog {
/// Creates a snapshot of the current state of the repository and virtual branches using the given label. /// 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: /// 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. /// - A fake branch `gitbutler/target` is created and maintained in order to keep the oplog head reachable.
/// ///
/// The snapshot tree contains: /// 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. /// Returns the sha of the created snapshot commit or None if snapshots are disabled.
fn create_snapshot(&self, details: SnapshotDetails) -> Result<Option<String>>; fn create_snapshot(&self, details: SnapshotDetails) -> Result<Option<String>>;
/// Lists the snapshots that have been created for the given repository, up to the given limit. /// Lists the snapshots that have been created for the given repository, up to the given limit.
/// An alternative way of retrieving the snapshots would be to manually the oplog head `git log <oplog_head>` available in `.git/gitbutler/oplog.toml`. /// 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. /// If there are no snapshots, an empty list is returned.
fn list_snapshots(&self, limit: usize, sha: Option<String>) -> Result<Vec<Snapshot>>; fn list_snapshots(&self, limit: usize, sha: Option<String>) -> Result<Vec<Snapshot>>;
@ -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()?; let branch_tree_id = branches_tree_builder.write()?;
tree_builder.insert("virtual_branches", branch_tree_id, FileMode::Tree.into())?; tree_builder.insert("virtual_branches", branch_tree_id, FileMode::Tree.into())?;
@ -374,6 +411,8 @@ impl Oplog for Project {
.to_object(&repo)? .to_object(&repo)?
.into_tree() .into_tree()
.map_err(|_| anyhow!("failed to convert virtual_branches tree entry to 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 let commits_tree_entry = branch_tree
.get_name("commits") .get_name("commits")
.ok_or(anyhow!("failed to get commits tree entry"))?; .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() { if let Some(commit_id) = commit_entry.name() {
// check for the oid in the repo // check for the oid in the repo
let commit_oid = git2::Oid::from_str(commit_id)?; let commit_oid = git2::Oid::from_str(commit_id)?;
if repo.find_commit(commit_oid).is_ok() { if repo.find_commit(commit_oid).is_err() {
continue; // commit is here, so keep going // 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 // if branch_name is 'integration', we need to create or update the gitbutler/integration branch
// we get the data from the blob entry and create a commit object from it, which should match the oid of the entry if let Some(branch_name) = branch_name {
let commit_tree = commit_entry if branch_name == "integration" {
.to_object(&repo)? let integration_commit = repo.find_commit(commit_oid)?;
.into_tree() // reset the branch if it's there
.map_err(|_| anyhow!("failed to convert commit tree entry to tree"))?; let branch =
let commit_blob_entry = commit_tree repo.find_branch("gitbutler/integration", git2::BranchType::Local);
.get_name("commit") if let Ok(mut branch) = branch {
.ok_or(anyhow!("failed to get workdir tree entry"))?; // need to detatch the head for just a minuto
let commit_blob = commit_blob_entry repo.set_head_detached(commit_oid)?;
.to_object(&repo)? branch.delete()?;
.into_blob() }
.map_err(|_| anyhow!("failed to convert commit tree entry to blob"))?; // ok, now we set the branch to what it was and update HEAD
let new_commit_oid = repo repo.branch("gitbutler/integration", &integration_commit, true)?;
.odb()? // make sure head is gitbutler/integration
.write(git2::ObjectType::Commit, commit_blob.content())?; repo.set_head("refs/heads/gitbutler/integration")?;
if new_commit_oid != commit_oid { }
return Err(anyhow!("commit oid mismatch"));
} }
} }
} }

View File

@ -7,7 +7,7 @@ use std::{
use serde::{Deserialize, Serialize}; 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)] #[derive(Serialize, Deserialize, Debug, Default)]
pub struct Oplog { pub struct Oplog {
/// This is the sha of the last oplog commit /// This is the sha of the last oplog commit

View File

@ -282,8 +282,6 @@ pub fn set_target_push_remote(
) -> Result<(), errors::SetBaseBranchError> { ) -> Result<(), errors::SetBaseBranchError> {
let repo = &project_repository.git_repository; let repo = &project_repository.git_repository;
dbg!(push_remote_name);
let remote = repo let remote = repo
.find_remote(push_remote_name) .find_remote(push_remote_name)
.context(format!("failed to find remote {}", push_remote_name))?; .context(format!("failed to find remote {}", push_remote_name))?;

View File

@ -156,8 +156,68 @@ async fn test_basic_oplog() {
assert_eq!(file_lines, "content"); 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] #[tokio::test]
async fn test_oplog_head_corrupt() { async fn test_oplog_head_corrupt() {
let Test { let Test {
@ -177,7 +237,11 @@ async fn test_oplog_head_corrupt() {
assert_eq!(snapshots.len(), 1); assert_eq!(snapshots.len(), 1);
// overwrite oplog head with a non-commit sha // 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( fs::write(
file_path, file_path,
"head_sha = \"758d54f587227fba3da3b61fbb54a99c17903d59\"", "head_sha = \"758d54f587227fba3da3b61fbb54a99c17903d59\"",

View File

@ -126,7 +126,6 @@ pub mod commands {
branch: &str, branch: &str,
push_remote: Option<&str>, // optional different name of a remote to push to (defaults to same as the branch) push_remote: Option<&str>, // optional different name of a remote to push to (defaults to same as the branch)
) -> Result<BaseBranch, Error> { ) -> Result<BaseBranch, Error> {
dbg!(&project_id, &branch, &push_remote);
let branch_name = format!("refs/remotes/{}", branch) let branch_name = format!("refs/remotes/{}", branch)
.parse() .parse()
.context("Invalid branch name")?; .context("Invalid branch name")?;