mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-26 02:51:57 +03:00
Merge pull request #5141 from gitbutlerapp/handle-undo-commit
Stacking flow/ UI handles already pushed vbranches
This commit is contained in:
commit
dcf645da6c
@ -292,7 +292,7 @@ pub fn list_virtual_branches_cached(
|
|||||||
|
|
||||||
let branches_span =
|
let branches_span =
|
||||||
tracing::debug_span!("handle branches", num_branches = status.branches.len()).entered();
|
tracing::debug_span!("handle branches", num_branches = status.branches.len()).entered();
|
||||||
for (branch, mut files) in status.branches {
|
for (mut branch, mut files) in status.branches {
|
||||||
let repo = ctx.repository();
|
let repo = ctx.repository();
|
||||||
update_conflict_markers(ctx, files.clone())?;
|
update_conflict_markers(ctx, files.clone())?;
|
||||||
|
|
||||||
@ -423,7 +423,7 @@ pub fn list_virtual_branches_cached(
|
|||||||
// TODO: Error out here once this API is stable
|
// TODO: Error out here once this API is stable
|
||||||
let series = match stack_series(
|
let series = match stack_series(
|
||||||
ctx,
|
ctx,
|
||||||
&branch,
|
&mut branch,
|
||||||
&default_target,
|
&default_target,
|
||||||
&check_commit,
|
&check_commit,
|
||||||
remote_commit_data,
|
remote_commit_data,
|
||||||
@ -481,7 +481,7 @@ pub fn list_virtual_branches_cached(
|
|||||||
/// Newest first, oldest last in the list
|
/// Newest first, oldest last in the list
|
||||||
fn stack_series(
|
fn stack_series(
|
||||||
ctx: &CommandContext,
|
ctx: &CommandContext,
|
||||||
branch: &Stack,
|
branch: &mut Stack,
|
||||||
default_target: &Target,
|
default_target: &Target,
|
||||||
check_commit: &IsCommitIntegrated,
|
check_commit: &IsCommitIntegrated,
|
||||||
remote_commit_data: HashMap<CommitData, git2::Oid>,
|
remote_commit_data: HashMap<CommitData, git2::Oid>,
|
||||||
@ -563,6 +563,12 @@ fn stack_series(
|
|||||||
}
|
}
|
||||||
api_series.reverse();
|
api_series.reverse();
|
||||||
|
|
||||||
|
// This is done for compatibility with the legacy flow.
|
||||||
|
// After a couple of weeks we can get rid of this.
|
||||||
|
if let Err(e) = branch.set_legacy_compatible_stack_reference(ctx) {
|
||||||
|
tracing::warn!("failed to set legacy compatible stack reference: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
Ok((api_series, requires_force))
|
Ok((api_series, requires_force))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +152,8 @@ pub trait StackExt {
|
|||||||
from: &Commit<'_>,
|
from: &Commit<'_>,
|
||||||
to: &Commit<'_>,
|
to: &Commit<'_>,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
fn set_legacy_compatible_stack_reference(&mut self, ctx: &CommandContext) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to update a PatchReference.
|
/// Request to update a PatchReference.
|
||||||
@ -233,7 +235,11 @@ impl StackExt for Stack {
|
|||||||
} else {
|
} else {
|
||||||
CommitOrChangeId::CommitId(commit.id().to_string())
|
CommitOrChangeId::CommitId(commit.id().to_string())
|
||||||
},
|
},
|
||||||
name: normalize_branch_name(&self.name)?,
|
name: if let Some(refname) = self.upstream.as_ref() {
|
||||||
|
refname.branch().to_string()
|
||||||
|
} else {
|
||||||
|
normalize_branch_name(&self.name)?
|
||||||
|
},
|
||||||
description: None,
|
description: None,
|
||||||
};
|
};
|
||||||
let state = branch_state(ctx);
|
let state = branch_state(ctx);
|
||||||
@ -256,7 +262,7 @@ impl StackExt for Stack {
|
|||||||
let prefix = rand::random::<u32>().to_string();
|
let prefix = rand::random::<u32>().to_string();
|
||||||
reference.name = format!("{}-{}", &reference.name, prefix);
|
reference.name = format!("{}-{}", &reference.name, prefix);
|
||||||
}
|
}
|
||||||
validate_name(&reference, ctx, &state)?;
|
validate_name(&reference, ctx, &state, self.upstream.clone())?;
|
||||||
self.heads = vec![reference];
|
self.heads = vec![reference];
|
||||||
state.set_branch(self.clone())
|
state.set_branch(self.clone())
|
||||||
}
|
}
|
||||||
@ -279,7 +285,7 @@ impl StackExt for Stack {
|
|||||||
};
|
};
|
||||||
let state = branch_state(ctx);
|
let state = branch_state(ctx);
|
||||||
let patches = stack_patches(ctx, &state, self.head(), true)?;
|
let patches = stack_patches(ctx, &state, self.head(), true)?;
|
||||||
validate_name(&new_head, ctx, &state)?;
|
validate_name(&new_head, ctx, &state, None)?;
|
||||||
validate_target(&new_head, ctx, self.head(), &state)?;
|
validate_target(&new_head, ctx, self.head(), &state)?;
|
||||||
let updated_heads = add_head(self.heads.clone(), new_head, preceding_head, patches)?;
|
let updated_heads = add_head(self.heads.clone(), new_head, preceding_head, patches)?;
|
||||||
self.heads = updated_heads;
|
self.heads = updated_heads;
|
||||||
@ -406,7 +412,7 @@ impl StackExt for Stack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
head.name = name;
|
head.name = name;
|
||||||
validate_name(head, ctx, &state)?;
|
validate_name(head, ctx, &state, self.upstream.clone())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -569,6 +575,41 @@ impl StackExt for Stack {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_legacy_compatible_stack_reference(&mut self, ctx: &CommandContext) -> Result<()> {
|
||||||
|
// self.upstream is only set if this is a branch that was created & manipulated by the legacy flow
|
||||||
|
let legacy_refname = match self.upstream.clone().map(|r| r.branch().to_owned()) {
|
||||||
|
Some(legacy_refname) => legacy_refname,
|
||||||
|
None => return Ok(()), // noop
|
||||||
|
};
|
||||||
|
// update the reference only if there is exactly one series in the stack
|
||||||
|
if self.heads.len() != 1 {
|
||||||
|
return Ok(()); // noop
|
||||||
|
}
|
||||||
|
let head = match self.heads.first() {
|
||||||
|
Some(head) => head,
|
||||||
|
None => return Ok(()), // noop
|
||||||
|
};
|
||||||
|
if legacy_refname == head.name {
|
||||||
|
return Ok(()); // noop
|
||||||
|
}
|
||||||
|
let default_target = branch_state(ctx).get_default_target()?;
|
||||||
|
let update = PatchReferenceUpdate {
|
||||||
|
name: Some(legacy_refname),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if let Some(remote_name) = default_target.push_remote_name.as_ref() {
|
||||||
|
// modify the stack reference only if it has not been pushed yet
|
||||||
|
if !head.pushed(remote_name, ctx).unwrap_or_default() {
|
||||||
|
// set the stack reference to the legacy refname
|
||||||
|
self.update_series(ctx, head.name.clone(), &update)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if there is no push remote, just update the stack reference
|
||||||
|
self.update_series(ctx, head.name.clone(), &update)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates that the commit in the reference target
|
/// Validates that the commit in the reference target
|
||||||
@ -651,7 +692,9 @@ fn validate_name(
|
|||||||
reference: &PatchReference,
|
reference: &PatchReference,
|
||||||
ctx: &CommandContext,
|
ctx: &CommandContext,
|
||||||
state: &VirtualBranchesHandle,
|
state: &VirtualBranchesHandle,
|
||||||
|
legacy_branch_ref: Option<RemoteRefname>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let legacy_branch_ref = legacy_branch_ref.map(|r| r.branch().to_string());
|
||||||
if reference.name.starts_with("refs/heads") {
|
if reference.name.starts_with("refs/heads") {
|
||||||
return Err(anyhow!("Stack head name cannot start with 'refs/heads'"));
|
return Err(anyhow!("Stack head name cannot start with 'refs/heads'"));
|
||||||
}
|
}
|
||||||
@ -659,21 +702,27 @@ fn validate_name(
|
|||||||
name_partial(reference.name.as_str().into()).context("Invalid branch name")?;
|
name_partial(reference.name.as_str().into()).context("Invalid branch name")?;
|
||||||
// assert that there is no local git reference with this name
|
// assert that there is no local git reference with this name
|
||||||
if reference_exists(ctx, &reference.name)? {
|
if reference_exists(ctx, &reference.name)? {
|
||||||
|
// Allow the reference overlap if it is the same as the legacy branch ref
|
||||||
|
if legacy_branch_ref != Some(reference.name.clone()) {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"A git reference with the name {} exists",
|
"A git reference with the name {} exists",
|
||||||
&reference.name
|
&reference.name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let default_target = state.get_default_target()?;
|
let default_target = state.get_default_target()?;
|
||||||
// assert that there is no remote git reference with this name
|
// assert that there is no remote git reference with this name
|
||||||
if let Some(remote_name) = default_target.push_remote_name {
|
if let Some(remote_name) = default_target.push_remote_name {
|
||||||
if reference_exists(ctx, &reference.remote_reference(remote_name.as_str())?)? {
|
if reference_exists(ctx, &reference.remote_reference(remote_name.as_str())?)? {
|
||||||
|
// Allow the reference overlap if it is the same as the legacy branch ref
|
||||||
|
if legacy_branch_ref != Some(reference.name.clone()) {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"A git reference with the name {} exists",
|
"A git reference with the name {} exists",
|
||||||
&reference.name
|
&reference.name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// assert that there are no existing patch references with this name
|
// assert that there are no existing patch references with this name
|
||||||
if patch_reference_exists(state, &reference.name)? {
|
if patch_reference_exists(state, &reference.name)? {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
|||||||
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};
|
||||||
|
use gitbutler_reference::RemoteRefname;
|
||||||
use gitbutler_repo::{LogUntil, RepositoryExt as _};
|
use gitbutler_repo::{LogUntil, RepositoryExt as _};
|
||||||
use gitbutler_stack::VirtualBranchesHandle;
|
use gitbutler_stack::VirtualBranchesHandle;
|
||||||
use gitbutler_stack_api::{PatchReferenceUpdate, StackExt, TargetUpdate};
|
use gitbutler_stack_api::{PatchReferenceUpdate, StackExt, TargetUpdate};
|
||||||
@ -1010,6 +1011,77 @@ fn replace_head_top_of_stack_multiple() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_legacy_refname() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let remote_branch: RemoteRefname = "refs/remotes/origin/my-branch".parse().unwrap();
|
||||||
|
test_ctx.branch.upstream = Some(remote_branch.clone());
|
||||||
|
test_ctx
|
||||||
|
.branch
|
||||||
|
.set_legacy_compatible_stack_reference(&ctx)?;
|
||||||
|
// reference name was updated
|
||||||
|
assert_eq!(test_ctx.branch.heads[0].name, "my-branch");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_legacy_refname_no_upstream_set() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let initial_state = test_ctx.branch.clone();
|
||||||
|
test_ctx
|
||||||
|
.branch
|
||||||
|
.set_legacy_compatible_stack_reference(&ctx)?;
|
||||||
|
// no changes
|
||||||
|
assert_eq!(initial_state, test_ctx.branch);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_legacy_refname_multiple_heads() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let remote_branch: RemoteRefname = "refs/remotes/origin/my-branch".parse().unwrap();
|
||||||
|
test_ctx.branch.upstream = Some(remote_branch.clone());
|
||||||
|
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)?;
|
||||||
|
let initial_state = test_ctx.branch.clone();
|
||||||
|
test_ctx
|
||||||
|
.branch
|
||||||
|
.set_legacy_compatible_stack_reference(&ctx)?;
|
||||||
|
// no changes
|
||||||
|
assert_eq!(initial_state, test_ctx.branch);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_legacy_refname_pushed() -> Result<()> {
|
||||||
|
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
|
||||||
|
let mut test_ctx = test_ctx(&ctx)?;
|
||||||
|
let remote_branch: RemoteRefname = "refs/remotes/origin/my-branch".parse().unwrap();
|
||||||
|
test_ctx.branch.upstream = Some(remote_branch.clone());
|
||||||
|
|
||||||
|
let state = VirtualBranchesHandle::new(ctx.project().gb_dir());
|
||||||
|
let mut target = state.get_default_target()?;
|
||||||
|
target.push_remote_name = Some("origin".into());
|
||||||
|
state.set_default_target(target)?;
|
||||||
|
test_ctx.branch.push_series(&ctx, "virtual".into(), false)?;
|
||||||
|
let initial_state = test_ctx.branch.clone();
|
||||||
|
|
||||||
|
test_ctx
|
||||||
|
.branch
|
||||||
|
.set_legacy_compatible_stack_reference(&ctx)?;
|
||||||
|
// no changes
|
||||||
|
assert_eq!(initial_state, test_ctx.branch);
|
||||||
|
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