mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-11 10:32:54 +03:00
Merge pull request #5112 from gitbutlerapp/stack-update-target-commit
Adds replace_head method on the Stack trait
This commit is contained in:
commit
c5cf1499ba
@ -4,6 +4,7 @@ use anyhow::anyhow;
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use git2::Commit;
|
||||||
use gitbutler_command_context::CommandContext;
|
use gitbutler_command_context::CommandContext;
|
||||||
use gitbutler_commit::commit_ext::CommitExt;
|
use gitbutler_commit::commit_ext::CommitExt;
|
||||||
use gitbutler_patch_reference::{CommitOrChangeId, PatchReference};
|
use gitbutler_patch_reference::{CommitOrChangeId, PatchReference};
|
||||||
@ -134,6 +135,23 @@ pub trait StackExt {
|
|||||||
/// This operation will compute the current list of local and remote commits that belong to each series.
|
/// This operation will compute the current list of local and remote commits that belong to each series.
|
||||||
/// The first entry is the newest in the Stack (i.e the top of the stack).
|
/// The first entry is the newest in the Stack (i.e the top of the stack).
|
||||||
fn list_series(&self, ctx: &CommandContext) -> Result<Vec<Series>>;
|
fn list_series(&self, ctx: &CommandContext) -> Result<Vec<Series>>;
|
||||||
|
|
||||||
|
/// Updates all heads in the stack that point to the `from` commit to point to the `to` commit.
|
||||||
|
/// If there is nothing pointing to the `from` commit, this operation is a no-op.
|
||||||
|
/// If the `from` and `to` commits have the same change_id, this operation is also a no-op.
|
||||||
|
///
|
||||||
|
/// In the case that the `from` commit is the head of the stack, this operation delegates to `set_stack_head`.
|
||||||
|
///
|
||||||
|
/// Every time a commit/patch is moved / removed / updated, this method needs to be invoked to maintain the integrity of the stack.
|
||||||
|
/// Typically in this case the `to` Commit would be `from`'s parent.
|
||||||
|
///
|
||||||
|
/// The `to` commit must be between the Stack head and it's merge base otherwise this operation will error out.
|
||||||
|
fn replace_head(
|
||||||
|
&mut self,
|
||||||
|
ctx: &CommandContext,
|
||||||
|
from: &Commit<'_>,
|
||||||
|
to: &Commit<'_>,
|
||||||
|
) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to update a PatchReference.
|
/// Request to update a PatchReference.
|
||||||
@ -434,7 +452,6 @@ impl StackExt for Stack {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: remote commits are not being populated yet
|
|
||||||
fn list_series(&self, ctx: &CommandContext) -> Result<Vec<Series>> {
|
fn list_series(&self, ctx: &CommandContext) -> Result<Vec<Series>> {
|
||||||
if !self.initialized() {
|
if !self.initialized() {
|
||||||
return Err(anyhow!("Stack has not been initialized"));
|
return Err(anyhow!("Stack has not been initialized"));
|
||||||
@ -491,6 +508,67 @@ impl StackExt for Stack {
|
|||||||
}
|
}
|
||||||
Ok(all_series)
|
Ok(all_series)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replace_head(
|
||||||
|
&mut self,
|
||||||
|
ctx: &CommandContext,
|
||||||
|
from: &Commit<'_>,
|
||||||
|
to: &Commit<'_>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !self.initialized() {
|
||||||
|
return Err(anyhow!("Stack has not been initialized"));
|
||||||
|
}
|
||||||
|
// find all heads matching the 'from' target (there can be multiple heads pointing to the same commit)
|
||||||
|
let matching_heads = self
|
||||||
|
.heads
|
||||||
|
.iter()
|
||||||
|
.filter(|h| match from.change_id() {
|
||||||
|
Some(change_id) => h.target == CommitOrChangeId::ChangeId(change_id.clone()),
|
||||||
|
None => h.target == CommitOrChangeId::CommitId(from.id().to_string()),
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
if from.change_id() == to.change_id() {
|
||||||
|
// there is nothing to do
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = branch_state(ctx);
|
||||||
|
let mut updated_heads: Vec<PatchReference> = vec![];
|
||||||
|
|
||||||
|
for head in matching_heads {
|
||||||
|
if self.heads.last().cloned() == Some(head.clone()) {
|
||||||
|
// the head is the stack head - update it accordingly
|
||||||
|
self.set_stack_head(ctx, to.id(), None)?;
|
||||||
|
} else {
|
||||||
|
// new head target from the 'to' commit
|
||||||
|
let new_target = match to.change_id() {
|
||||||
|
Some(change_id) => CommitOrChangeId::ChangeId(change_id.to_string()),
|
||||||
|
None => CommitOrChangeId::CommitId(to.id().to_string()),
|
||||||
|
};
|
||||||
|
let mut new_head = head.clone();
|
||||||
|
new_head.target = new_target;
|
||||||
|
// validate the updated head
|
||||||
|
validate_target(&new_head, ctx, self.head(), &state)?;
|
||||||
|
// add it to the list of updated heads
|
||||||
|
updated_heads.push(new_head);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updated_heads.is_empty() {
|
||||||
|
for updated_head in updated_heads {
|
||||||
|
if let Some(head) = self.heads.iter_mut().find(|h| h.name == updated_head.name) {
|
||||||
|
// find set the corresponding head in the mutable self
|
||||||
|
*head = updated_head;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.updated_timestamp_ms = gitbutler_time::time::now_ms();
|
||||||
|
// update the persistent state
|
||||||
|
state.set_branch(self.clone())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates that the commit in the reference target
|
/// Validates that the commit in the reference target
|
||||||
|
@ -752,6 +752,264 @@ fn set_stack_head() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_head_single() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let top_of_stack = test_ctx.branch.heads.last().unwrap().target.clone();
|
||||||
|
let from_head = PatchReference {
|
||||||
|
name: "from_head".into(),
|
||||||
|
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
|
||||||
|
description: None,
|
||||||
|
};
|
||||||
|
test_ctx.branch.add_series(&ctx, from_head, None)?;
|
||||||
|
// replace with previous head
|
||||||
|
let result = test_ctx
|
||||||
|
.branch
|
||||||
|
.replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[0]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// the heads is update to point to the new commit
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch.heads[0].target,
|
||||||
|
CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap())
|
||||||
|
);
|
||||||
|
// the top of the stack is not changed
|
||||||
|
assert_eq!(test_ctx.branch.heads.last().unwrap().target, top_of_stack);
|
||||||
|
// the state was persisted
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch,
|
||||||
|
test_ctx.handle.get_branch(test_ctx.branch.id)?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_head_single_with_mergebase() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let top_of_stack = test_ctx.branch.heads.last().unwrap().target.clone();
|
||||||
|
let from_head = PatchReference {
|
||||||
|
name: "from_head".into(),
|
||||||
|
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
|
||||||
|
description: None,
|
||||||
|
};
|
||||||
|
test_ctx.branch.add_series(&ctx, from_head, None)?;
|
||||||
|
// replace with merge base
|
||||||
|
let merge_base = ctx.repository().find_commit(
|
||||||
|
ctx.repository()
|
||||||
|
.merge_base(test_ctx.branch.head(), test_ctx.default_target.sha)?,
|
||||||
|
)?;
|
||||||
|
let result = test_ctx
|
||||||
|
.branch
|
||||||
|
.replace_head(&ctx, &test_ctx.commits[1], &merge_base);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// the heads is update to point to the new commit
|
||||||
|
// this time its a commit id
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch.heads[0].target,
|
||||||
|
CommitOrChangeId::CommitId(merge_base.id().to_string())
|
||||||
|
);
|
||||||
|
// the top of the stack is not changed
|
||||||
|
assert_eq!(test_ctx.branch.heads.last().unwrap().target, top_of_stack);
|
||||||
|
// the state was persisted
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch,
|
||||||
|
test_ctx.handle.get_branch(test_ctx.branch.id)?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_head_with_inavlid_commit_error() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let from_head = PatchReference {
|
||||||
|
name: "from_head".into(),
|
||||||
|
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
|
||||||
|
description: None,
|
||||||
|
};
|
||||||
|
test_ctx.branch.add_series(&ctx, from_head, None)?;
|
||||||
|
let stack = test_ctx.branch.clone();
|
||||||
|
let result =
|
||||||
|
test_ctx
|
||||||
|
.branch
|
||||||
|
.replace_head(&ctx, &test_ctx.commits[1], &test_ctx.other_commits[0]); //in another stack
|
||||||
|
assert!(result.is_err());
|
||||||
|
// is unmodified
|
||||||
|
assert_eq!(stack, test_ctx.branch);
|
||||||
|
// same in persistence
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch,
|
||||||
|
test_ctx.handle.get_branch(test_ctx.branch.id)?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_head_with_same_noop() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let from_head = PatchReference {
|
||||||
|
name: "from_head".into(),
|
||||||
|
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
|
||||||
|
description: None,
|
||||||
|
};
|
||||||
|
test_ctx.branch.add_series(&ctx, from_head, None)?;
|
||||||
|
let stack = test_ctx.branch.clone();
|
||||||
|
let result = test_ctx
|
||||||
|
.branch
|
||||||
|
.replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[1]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// is unmodified
|
||||||
|
assert_eq!(stack, test_ctx.branch);
|
||||||
|
// same in persistence
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch,
|
||||||
|
test_ctx.handle.get_branch(test_ctx.branch.id)?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_no_head_noop() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let stack = test_ctx.branch.clone();
|
||||||
|
let result = test_ctx
|
||||||
|
.branch
|
||||||
|
.replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[0]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// is unmodified
|
||||||
|
assert_eq!(stack, test_ctx.branch);
|
||||||
|
// same in persistence
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch,
|
||||||
|
test_ctx.handle.get_branch(test_ctx.branch.id)?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_non_member_commit_noop_noerror() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let stack = test_ctx.branch.clone();
|
||||||
|
let result =
|
||||||
|
test_ctx
|
||||||
|
.branch
|
||||||
|
.replace_head(&ctx, &test_ctx.other_commits[0], &test_ctx.commits[0]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// is unmodified
|
||||||
|
assert_eq!(stack, test_ctx.branch);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_top_of_stack_single() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let initial_head = ctx.repository().find_commit(test_ctx.branch.head())?;
|
||||||
|
|
||||||
|
let result = test_ctx
|
||||||
|
.branch
|
||||||
|
.replace_head(&ctx, &initial_head, &test_ctx.commits[1]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// the heads is update to point to the new commit
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch.heads[0].target,
|
||||||
|
CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap())
|
||||||
|
);
|
||||||
|
assert_eq!(test_ctx.branch.head(), test_ctx.commits[1].id());
|
||||||
|
assert_eq!(test_ctx.branch.heads.len(), 1);
|
||||||
|
// the state was persisted
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch,
|
||||||
|
test_ctx.handle.get_branch(test_ctx.branch.id)?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_head_multiple() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let top_of_stack = test_ctx.branch.heads.last().unwrap().target.clone();
|
||||||
|
let from_head_1 = PatchReference {
|
||||||
|
name: "from_head_1".into(),
|
||||||
|
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
|
||||||
|
description: None,
|
||||||
|
};
|
||||||
|
let from_head_2 = PatchReference {
|
||||||
|
name: "from_head_2".into(),
|
||||||
|
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
|
||||||
|
description: None,
|
||||||
|
};
|
||||||
|
// both references point to the same commit
|
||||||
|
test_ctx.branch.add_series(&ctx, from_head_1, None)?;
|
||||||
|
test_ctx
|
||||||
|
.branch
|
||||||
|
.add_series(&ctx, from_head_2, Some("from_head_1".into()))?;
|
||||||
|
// replace the commit
|
||||||
|
let result = test_ctx
|
||||||
|
.branch
|
||||||
|
.replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[0]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// both heads are updated to point to the new commit
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch.heads[0].target,
|
||||||
|
CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch.heads[1].target,
|
||||||
|
CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap())
|
||||||
|
);
|
||||||
|
// the top of the stack is not changed
|
||||||
|
assert_eq!(test_ctx.branch.heads.last().unwrap().target, top_of_stack);
|
||||||
|
// the state was persisted
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch,
|
||||||
|
test_ctx.handle.get_branch(test_ctx.branch.id)?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_head_top_of_stack_multiple() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let initial_head = ctx.repository().find_commit(test_ctx.branch.head())?;
|
||||||
|
let extra_head = PatchReference {
|
||||||
|
name: "extra_head".into(),
|
||||||
|
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
|
||||||
|
description: None,
|
||||||
|
};
|
||||||
|
// an extra head just beneath the top of the stack
|
||||||
|
test_ctx.branch.add_series(&ctx, extra_head, None)?;
|
||||||
|
// replace top of stack the commit
|
||||||
|
let result = test_ctx
|
||||||
|
.branch
|
||||||
|
.replace_head(&ctx, &initial_head, &test_ctx.commits[1]);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// both heads are updated to point to the new commit
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch.heads[0].target,
|
||||||
|
CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch.heads[1].target,
|
||||||
|
CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap())
|
||||||
|
);
|
||||||
|
assert_eq!(test_ctx.branch.head(), test_ctx.commits[1].id());
|
||||||
|
// order is the same
|
||||||
|
assert_eq!(test_ctx.branch.heads[0].name, "extra_head");
|
||||||
|
// the state was persisted
|
||||||
|
assert_eq!(
|
||||||
|
test_ctx.branch,
|
||||||
|
test_ctx.handle.get_branch(test_ctx.branch.id)?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn command_ctx(name: &str) -> Result<(CommandContext, TempDir)> {
|
fn command_ctx(name: &str) -> Result<(CommandContext, TempDir)> {
|
||||||
gitbutler_testsupport::writable::fixture("stacking.sh", name)
|
gitbutler_testsupport::writable::fixture("stacking.sh", name)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user