Merge pull request #5240 from gitbutlerapp/implement-reorder-stack

Implement stack commit reordering
This commit is contained in:
Kiril Videlov 2024-10-26 22:10:26 +02:00 committed by GitHub
commit a9c600e1a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 748 additions and 12 deletions

View File

@ -1,6 +1,7 @@
use super::r#virtual as vbranch;
use crate::branch_upstream_integration;
use crate::move_commits;
use crate::reorder::{self, StackOrder};
use crate::reorder_commits;
use crate::upstream_integration::{
self, BaseBranchResolution, BaseBranchResolutionApproach, BranchStatuses, Resolution,
@ -349,6 +350,17 @@ pub fn insert_blank_commit(
vbranch::insert_blank_commit(&ctx, branch_id, commit_oid, offset).map_err(Into::into)
}
pub fn reorder_stack(project: &Project, stack_id: StackId, stack_order: StackOrder) -> Result<()> {
let ctx = open_with_verify(project)?;
assure_open_workspace_mode(&ctx).context("Reordering a commit requires open workspace mode")?;
let mut guard = project.exclusive_worktree_access();
let _ = ctx.project().create_snapshot(
SnapshotDetails::new(OperationKind::ReorderCommit),
guard.write_permission(),
);
reorder::reorder_stack(&ctx, stack_id, stack_order, guard.write_permission())
}
pub fn reorder_commit(
project: &Project,
branch_id: StackId,

View File

@ -8,11 +8,11 @@ pub use actions::{
get_uncommited_files_reusable, insert_blank_commit, integrate_upstream,
integrate_upstream_commits, list_local_branches, list_remote_commit_files,
list_virtual_branches, list_virtual_branches_cached, move_commit, move_commit_file,
push_base_branch, push_virtual_branch, reorder_commit, reset_files, reset_virtual_branch,
resolve_upstream_integration, save_and_unapply_virutal_branch, set_base_branch,
set_target_push_remote, squash, unapply_ownership, unapply_without_saving_virtual_branch,
undo_commit, update_branch_order, update_commit_message, update_virtual_branch,
upstream_integration_statuses,
push_base_branch, push_virtual_branch, reorder_commit, reorder_stack, reset_files,
reset_virtual_branch, resolve_upstream_integration, save_and_unapply_virutal_branch,
set_base_branch, set_target_push_remote, squash, unapply_ownership,
unapply_without_saving_virtual_branch, undo_commit, update_branch_order, update_commit_message,
update_virtual_branch, upstream_integration_statuses,
};
mod r#virtual;
@ -47,6 +47,8 @@ pub mod conflicts;
pub mod branch_trees;
pub mod branch_upstream_integration;
mod move_commits;
pub mod reorder;
pub use reorder::{SeriesOrder, StackOrder};
mod reorder_commits;
mod undo_commit;

View File

@ -0,0 +1,412 @@
use anyhow::{bail, Context, Result};
use git2::Oid;
use gitbutler_command_context::CommandContext;
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_repo::rebase::cherry_rebase_group;
use gitbutler_stack::{Series, StackId};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::{
branch_trees::{
checkout_branch_trees, compute_updated_branch_head_for_commits, BranchHeadAndTree,
},
VirtualBranchesExt,
};
/// This API allows the client to reorder commits in a stack.
/// Commits may be moved within the same series or between different series.
/// Moving of series is not permitted.
///
/// # Errors
/// Errors out upon invalid stack order input. The following conditions are checked:
/// - The number of series in the order must match the number of series in the stack
/// - The series names in the reorder request must match the names in the stack
/// - The series themselves in the reorder request must be the same as the ones in the stack (this API is about moving commits, not series)
/// - The number of commits in the reorder request must match the number of commits in the stack
/// - The commit ids in the reorder request must be in the stack
pub fn reorder_stack(
ctx: &CommandContext,
branch_id: StackId,
new_order: StackOrder,
perm: &mut WorktreeWritePermission,
) -> Result<()> {
let state = ctx.project().virtual_branches();
let repo = ctx.repository();
let mut stack = state.get_branch(branch_id)?;
let all_series = stack.list_series(ctx)?;
let current_order = series_order(&all_series);
new_order.validate(current_order.clone())?;
let default_target = state.get_default_target()?;
let default_target_commit = repo
.find_reference(&default_target.branch.to_string())?
.peel_to_commit()?;
let old_head = repo.find_commit(stack.head())?;
let merge_base = repo.merge_base(default_target_commit.id(), stack.head())?;
let mut update_pairs = vec![];
let mut previous = merge_base;
for series in new_order.series.iter().rev() {
let new_head = series.commit_ids.first();
let current = all_series
.iter()
.find(|s| s.head.name == series.name)
.unwrap();
let old_head = current.local_commits.last().unwrap();
let new_head_oid = if let Some(new_head) = new_head {
*new_head
} else {
previous
};
update_pairs.push((old_head.id(), new_head_oid));
previous = new_head_oid
}
let ids_to_rebase = new_order
.series
.iter()
.flat_map(|s| s.commit_ids.iter())
.cloned()
.collect_vec();
let new_head = cherry_rebase_group(repo, merge_base, &ids_to_rebase)?;
// Calculate the new head and tree
let BranchHeadAndTree {
head: new_head_oid,
tree: new_tree_oid,
} = compute_updated_branch_head_for_commits(repo, old_head.id(), old_head.tree_id(), new_head)?;
// Set the series heads accordingly
for (current_oid, new_oid) in update_pairs {
let from_commit = repo.find_commit(current_oid)?;
let to_commit = repo.find_commit(new_oid)?;
// println!(
// "Replacing {} with {}",
// from_commit.message().unwrap(),
// to_commit.message().unwrap()
// );
stack.replace_head(ctx, &from_commit, &to_commit)?;
}
stack.set_stack_head(ctx, new_head_oid, Some(new_tree_oid))?;
checkout_branch_trees(ctx, perm)?;
crate::integration::update_workspace_commit(&state, ctx)
.context("failed to update gitbutler workspace")?;
Ok(())
}
/// Represents the order of series (branches) and changes (commits) in a stack.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StackOrder {
/// The series are ordered from newest to oldest (most recent stacks go first)
pub series: Vec<SeriesOrder>,
}
/// Represents the order of changes (commits) in a series (branch).
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct SeriesOrder {
/// Unique name of the series (branch). Must already exist in the stack.
pub name: String,
/// This is the desired commit order for the series. Because the commits will be rabased,
/// naturally, the the commit ids will be different afte updating.
/// The changes are ordered from newest to oldest (most recent changes go first)
#[serde(with = "gitbutler_serde::oid_vec")]
pub commit_ids: Vec<Oid>,
}
impl StackOrder {
fn validate(&self, current_order: StackOrder) -> Result<()> {
// Ensure the number of series is the same between the reorder update request and the stack
if self.series.len() != current_order.series.len() {
bail!(
"The number of series in the order ({}) does not match the number of series in the stack ({})",
self.series.len(),
current_order.series.len()
);
}
// Ensure that the names in the reorder update request match the names in the stack
for series_order in &self.series {
if !current_order
.series
.iter()
.any(|s| s.name == series_order.name)
{
bail!("Series '{}' does not exist in the stack", series_order.name);
}
}
// Ensure that the series themselves in the updater request are the same as the ones in the stack (this API is about moving commits, not series)
for (new_order, current_order) in self.series.iter().zip(current_order.series.iter()) {
if new_order.name != current_order.name {
bail!(
"Series '{}' in the order does not match the series '{}' in the stack. Series can't be reordered with this API, it's only for commits",
new_order.name,
current_order.name
);
}
}
let new_order_commit_ids = self
.series
.iter()
.flat_map(|s| s.commit_ids.iter())
.cloned()
.collect_vec();
let current_order_commit_ids = current_order
.series
.iter()
.flat_map(|s| s.commit_ids.iter())
.cloned()
.collect_vec();
// Ensure that the number of commits in the order is the same as the number of commits in the stack
if new_order_commit_ids.len() != current_order_commit_ids.len() {
bail!(
"The number of commits in the request order ({}) does not match the number of commits in the stack ({})",
new_order_commit_ids.len(),
current_order_commit_ids.len()
);
}
// Ensure that every commit in the order is in the stack
for commit_id in &new_order_commit_ids {
if !current_order_commit_ids.contains(commit_id) {
bail!("Commit '{}' does not exist in the stack", commit_id);
}
}
// Ensure the new order is not a noop
if new_order_commit_ids == current_order_commit_ids {
bail!("The new order is the same as the current order");
}
Ok(())
}
}
pub fn series_order(all_series: &[Series<'_>]) -> StackOrder {
let series_order: Vec<SeriesOrder> = all_series
.iter()
.rev()
.map(|series| {
let commit_ids = series
.local_commits
.iter()
.rev()
.map(|commit| commit.id())
.collect();
SeriesOrder {
name: series.head.name.clone(),
commit_ids,
}
})
.collect();
StackOrder {
series: series_order,
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn validation_ok() -> Result<()> {
let new_order = StackOrder {
series: vec![
SeriesOrder {
name: "branch-2".to_string(),
commit_ids: vec![
Oid::from_str("6").unwrap(),
Oid::from_str("5").unwrap(),
Oid::from_str("4").unwrap(),
],
},
SeriesOrder {
name: "branch-1".to_string(),
commit_ids: vec![
Oid::from_str("3").unwrap(),
Oid::from_str("1").unwrap(), // swapped with below
Oid::from_str("2").unwrap(),
],
},
],
};
let result = new_order.validate(existing_order());
assert!(result.is_ok());
Ok(())
}
#[test]
fn noop_errors_out() -> Result<()> {
let result = existing_order().validate(existing_order());
assert_eq!(
result.unwrap_err().to_string(),
"The new order is the same as the current order"
);
Ok(())
}
#[test]
fn non_existing_id_errors_out() -> Result<()> {
let new_order = StackOrder {
series: vec![
SeriesOrder {
name: "branch-2".to_string(),
commit_ids: vec![
Oid::from_str("6").unwrap(),
Oid::from_str("5").unwrap(),
Oid::from_str("4").unwrap(),
],
},
SeriesOrder {
name: "branch-1".to_string(),
commit_ids: vec![
Oid::from_str("3").unwrap(),
Oid::from_str("9").unwrap(), // does not exist
Oid::from_str("1").unwrap(),
],
},
],
};
let result = new_order.validate(existing_order());
assert_eq!(
result.unwrap_err().to_string(),
"Commit '9000000000000000000000000000000000000000' does not exist in the stack"
);
Ok(())
}
#[test]
fn number_of_commits_mismatch_errors_out() -> Result<()> {
let new_order = StackOrder {
series: vec![
SeriesOrder {
name: "branch-2".to_string(),
commit_ids: vec![
Oid::from_str("6").unwrap(),
Oid::from_str("5").unwrap(),
Oid::from_str("4").unwrap(),
],
},
SeriesOrder {
name: "branch-1".to_string(),
commit_ids: vec![
Oid::from_str("3").unwrap(), // missing
Oid::from_str("1").unwrap(),
],
},
],
};
let result = new_order.validate(existing_order());
assert_eq!(
result.unwrap_err().to_string(),
"The number of commits in the request order (5) does not match the number of commits in the stack (6)"
);
Ok(())
}
#[test]
fn series_out_of_order_errors_out() -> Result<()> {
let new_order = StackOrder {
series: vec![
SeriesOrder {
name: "branch-1".to_string(), // wrong order
commit_ids: vec![
Oid::from_str("6").unwrap(),
Oid::from_str("5").unwrap(),
Oid::from_str("4").unwrap(),
],
},
SeriesOrder {
name: "branch-2".to_string(), // wrong order
commit_ids: vec![
Oid::from_str("3").unwrap(),
Oid::from_str("2").unwrap(),
Oid::from_str("1").unwrap(),
],
},
],
};
let result = new_order.validate(existing_order());
assert_eq!(
result.unwrap_err().to_string(),
"Series 'branch-1' in the order does not match the series 'branch-2' in the stack. Series can't be reordered with this API, it's only for commits"
);
Ok(())
}
#[test]
fn different_series_name_errors_out() -> Result<()> {
let new_order = StackOrder {
series: vec![
SeriesOrder {
name: "does-not-exist".to_string(), // invalid series name
commit_ids: vec![
Oid::from_str("6").unwrap(),
Oid::from_str("5").unwrap(),
Oid::from_str("4").unwrap(),
],
},
SeriesOrder {
name: "branch-1".to_string(),
commit_ids: vec![
Oid::from_str("3").unwrap(),
Oid::from_str("2").unwrap(),
Oid::from_str("1").unwrap(),
],
},
],
};
let result = new_order.validate(existing_order());
assert_eq!(
result.unwrap_err().to_string(),
"Series 'does-not-exist' does not exist in the stack"
);
Ok(())
}
#[test]
fn different_number_of_series_errors_out() -> Result<()> {
let new_order = StackOrder {
series: vec![SeriesOrder {
name: "branch-1".to_string(),
commit_ids: vec![
Oid::from_str("3").unwrap(),
Oid::from_str("2").unwrap(),
Oid::from_str("1").unwrap(),
],
}],
};
let result = new_order.validate(existing_order());
assert_eq!(
result.unwrap_err().to_string(),
"The number of series in the order (1) does not match the number of series in the stack (2)"
);
Ok(())
}
fn existing_order() -> StackOrder {
StackOrder {
series: vec![
SeriesOrder {
name: "branch-2".to_string(),
commit_ids: vec![
Oid::from_str("6").unwrap(),
Oid::from_str("5").unwrap(),
Oid::from_str("4").unwrap(),
],
},
SeriesOrder {
name: "branch-1".to_string(),
commit_ids: vec![
Oid::from_str("3").unwrap(),
Oid::from_str("2").unwrap(),
Oid::from_str("1").unwrap(),
],
},
],
}
}
}

View File

@ -1,3 +1,5 @@
mod virtual_branches;
mod extra;
mod reorder;

View File

@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -eu -o pipefail
CLI=${1:?The first argument is the GitButler CLI}
git init remote
(cd remote
echo first > file
git add . && git commit -m "init"
)
export GITBUTLER_CLI_DATA_DIR=../user/gitbutler/app-data
git clone remote multiple-commits
(cd multiple-commits
git config user.name "Author"
git config user.email "author@example.com"
git branch existing-branch
$CLI project add --switch-to-workspace "$(git rev-parse --symbolic-full-name @{u})"
$CLI branch create --set-default other_stack
echo change0 >> other_file
$CLI branch commit other_stack -m "commit 0"
$CLI branch create --set-default my_stack
echo change1 >> file
$CLI branch commit my_stack -m "commit 1"
echo change2 >> file
$CLI branch commit my_stack -m "commit 2"
echo change3 >> file
$CLI branch commit my_stack -m "commit 3"
$CLI branch series my_stack -s "top-series"
echo change4 >> file
$CLI branch commit my_stack -m "commit 4"
echo change5 >> file
$CLI branch commit my_stack -m "commit 5"
echo change6 >> file
$CLI branch commit my_stack -m "commit 6"
)

View File

@ -0,0 +1,245 @@
use std::collections::HashMap;
use anyhow::Result;
use git2::Oid;
use gitbutler_branch_actions::{list_virtual_branches, reorder_stack, SeriesOrder, StackOrder};
use gitbutler_command_context::CommandContext;
use gitbutler_stack::VirtualBranchesHandle;
use itertools::Itertools;
use tempfile::TempDir;
#[test]
fn noop_reorder_errors() -> Result<()> {
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
let test_ctx = test_ctx(&ctx)?;
let order = order(vec![
vec![
test_ctx.top_commits["commit 6"],
test_ctx.top_commits["commit 5"],
test_ctx.top_commits["commit 4"],
],
vec![
test_ctx.bottom_commits["commit 3"],
test_ctx.bottom_commits["commit 2"],
test_ctx.bottom_commits["commit 1"],
],
]);
let result = reorder_stack(ctx.project(), test_ctx.stack.id, order);
assert_eq!(
result.unwrap_err().to_string(),
"The new order is the same as the current order"
);
Ok(())
}
#[test]
fn reorder_in_top_series() -> Result<()> {
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
let test_ctx = test_ctx(&ctx)?;
let order = order(vec![
vec![
test_ctx.top_commits["commit 6"],
test_ctx.top_commits["commit 4"], // currently 5
test_ctx.top_commits["commit 5"], // currently 4
],
vec![
test_ctx.bottom_commits["commit 3"],
test_ctx.bottom_commits["commit 2"],
test_ctx.bottom_commits["commit 1"],
],
]);
reorder_stack(ctx.project(), test_ctx.stack.id, order.clone())?;
let commits = vb_commits(&ctx);
// Verify the commit messages and ids in the second (top) series - top-series
assert_eq!(commits[0].msgs(), vec!["commit 6", "commit 4", "commit 5"]);
assert_ne!(commits[0].ids()[0], order.series[0].commit_ids[0]);
assert_ne!(commits[0].ids()[1], order.series[0].commit_ids[1]);
assert_ne!(commits[0].ids()[2], order.series[0].commit_ids[2]);
// Verify the commit messages and ids in the first (bottom) series
assert_eq!(commits[1].msgs(), vec!["commit 3", "commit 2", "commit 1"]);
assert_eq!(commits[1].ids(), order.series[1].commit_ids);
Ok(())
}
#[test]
fn reorder_in_top_series_head() -> Result<()> {
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
let test_ctx = test_ctx(&ctx)?;
let order = order(vec![
vec![
test_ctx.top_commits["commit 5"], // currently 6
test_ctx.top_commits["commit 6"], // currently 5
test_ctx.top_commits["commit 4"],
],
vec![
test_ctx.bottom_commits["commit 3"],
test_ctx.bottom_commits["commit 2"],
test_ctx.bottom_commits["commit 1"],
],
]);
reorder_stack(ctx.project(), test_ctx.stack.id, order.clone())?;
let commits = vb_commits(&ctx);
// Verify the commit messages and ids in the second (top) series - top-series
assert_eq!(commits[0].msgs(), vec!["commit 5", "commit 6", "commit 4"]);
assert_ne!(commits[0].ids()[0], order.series[0].commit_ids[0]);
assert_ne!(commits[0].ids()[1], order.series[0].commit_ids[1]);
assert_eq!(commits[0].ids()[2], order.series[0].commit_ids[2]); // not rebased from here down
// Verify the commit messages and ids in the first (bottom) series
assert_eq!(commits[1].msgs(), vec!["commit 3", "commit 2", "commit 1"]);
assert_eq!(commits[1].ids(), order.series[1].commit_ids);
Ok(())
}
#[test]
fn reorder_between_series() -> Result<()> {
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
let test_ctx = test_ctx(&ctx)?;
let order = order(vec![
vec![
test_ctx.top_commits["commit 6"],
test_ctx.top_commits["commit 5"],
test_ctx.bottom_commits["commit 2"], // from the bottom series
test_ctx.top_commits["commit 4"],
],
vec![
test_ctx.bottom_commits["commit 3"],
test_ctx.bottom_commits["commit 1"],
],
]);
reorder_stack(ctx.project(), test_ctx.stack.id, order.clone())?;
let commits = vb_commits(&ctx);
// Verify the commit messages and ids in the second (top) series - top-series
assert_eq!(
commits[0].msgs(),
vec!["commit 6", "commit 5", "commit 2", "commit 4"]
);
for i in 0..3 {
assert_ne!(commits[0].ids()[i], order.series[0].commit_ids[i]); // all in the top series are rebased
}
// Verify the commit messages and ids in the first (bottom) series
assert_eq!(commits[1].msgs(), vec!["commit 3", "commit 1"]);
assert_ne!(commits[1].ids()[0], order.series[1].commit_ids[0]);
assert_eq!(commits[1].ids()[1], order.series[1].commit_ids[1]); // the bottom most commit is the same
Ok(())
}
#[test]
fn reorder_series_head_to_another_series() -> Result<()> {
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
let test_ctx = test_ctx(&ctx)?;
let order = order(vec![
vec![
test_ctx.top_commits["commit 6"],
test_ctx.top_commits["commit 5"],
test_ctx.bottom_commits["commit 3"],
test_ctx.top_commits["commit 4"],
],
vec![
test_ctx.bottom_commits["commit 2"],
test_ctx.bottom_commits["commit 1"],
],
]);
reorder_stack(ctx.project(), test_ctx.stack.id, order.clone())?;
let commits = vb_commits(&ctx);
// Verify the commit messages and ids in the second (top) series - top-series
assert_eq!(
commits[0].msgs(),
vec!["commit 6", "commit 5", "commit 3", "commit 4"]
);
for i in 0..3 {
assert_ne!(commits[0].ids()[i], order.series[0].commit_ids[i]); // all in the top series are rebased
}
// Verify the commit messages and ids in the first (bottom) series
assert_eq!(commits[1].msgs(), vec!["commit 2", "commit 1"]);
assert_eq!(commits[1].ids()[0], order.series[1].commit_ids[0]);
assert_eq!(commits[1].ids()[1], order.series[1].commit_ids[1]);
Ok(())
}
fn order(series: Vec<Vec<Oid>>) -> StackOrder {
StackOrder {
series: vec![
SeriesOrder {
name: "top-series".to_string(),
commit_ids: series[0].clone(),
},
SeriesOrder {
name: "a-branch-2".to_string(),
commit_ids: series[1].clone(),
},
],
}
}
trait CommitHelpers {
fn msgs(&self) -> Vec<String>;
fn ids(&self) -> Vec<Oid>;
}
impl CommitHelpers for Vec<(Oid, String)> {
fn msgs(&self) -> Vec<String> {
self.iter().map(|(_, msg)| msg.clone()).collect_vec()
}
fn ids(&self) -> Vec<Oid> {
self.iter().map(|(id, _)| *id).collect_vec()
}
}
/// Commits from list_virtual_branches
fn vb_commits(ctx: &CommandContext) -> Vec<Vec<(git2::Oid, String)>> {
let (vbranches, _) = list_virtual_branches(ctx.project()).unwrap();
let vbranch = vbranches.iter().find(|vb| vb.name == "my_stack").unwrap();
let mut out = vec![];
for series in vbranch.series.clone() {
let messages = series
.patches
.iter()
.map(|p| (p.id, p.description.to_string()))
.collect_vec();
out.push(messages)
}
out
}
fn command_ctx(name: &str) -> Result<(CommandContext, TempDir)> {
gitbutler_testsupport::writable::fixture("reorder.sh", name)
}
fn test_ctx(ctx: &CommandContext) -> Result<TestContext> {
let handle = VirtualBranchesHandle::new(ctx.project().gb_dir());
let branches = handle.list_all_branches()?;
let stack = branches.iter().find(|b| b.name == "my_stack").unwrap();
let all_series = stack.list_series(ctx)?;
let top_commits: HashMap<String, git2::Oid> = all_series[1]
.local_commits
.iter()
.map(|c| (c.message().unwrap().to_string(), c.id()))
.collect();
let bottom_commits: HashMap<String, git2::Oid> = all_series[0]
.local_commits
.iter()
.map(|c| (c.message().unwrap().to_string(), c.id()))
.collect();
Ok(TestContext {
stack: stack.clone(),
top_commits,
bottom_commits,
})
}
struct TestContext {
stack: gitbutler_stack::Stack,
top_commits: HashMap<String, git2::Oid>,
bottom_commits: HashMap<String, git2::Oid>,
}

View File

@ -353,10 +353,18 @@ impl Stack {
.ok_or_else(|| anyhow!("Series with name {} not found", branch_name))?;
new_head.target = target_update.target.clone();
validate_target(&new_head, ctx.repository(), self.head(), &state)?;
let preceding_head = update
let preceding_head = if let Some(preceding_head_name) = update
.target_update
.clone()
.and_then(|update| update.preceding_head);
.and_then(|update| update.preceding_head_name)
{
let (_, preceding_head) = get_head(&self.heads, &preceding_head_name)
.context("The specified preceding_head could not be found")?;
Some(preceding_head)
} else {
None
};
// drop the old head and add the new one
let (idx, _) = get_head(&updated_heads, &branch_name)?;
updated_heads.remove(idx);
@ -646,7 +654,7 @@ pub struct TargetUpdate {
pub target: CommitOrChangeId,
/// If there are multiple heads that point to the same patch, the order can be disambiguated by specifying the `preceding_head`.
/// Leaving this field empty will make the new head first in relation to other references pointing to this commit.
pub preceding_head: Option<PatchReference>,
pub preceding_head_name: Option<String>,
}
/// Push details to be supplied to `RepoActionsExt`'s `push` method.

View File

@ -505,7 +505,7 @@ fn update_series_target_fails_commit_not_in_stack() -> Result<()> {
name: None,
target_update: Some(TargetUpdate {
target: CommitOrChangeId::CommitId(other_commit_id.clone()),
preceding_head: None,
preceding_head_name: None,
}),
description: None,
};
@ -533,7 +533,7 @@ fn update_series_target_orphan_commit_fails() -> Result<()> {
name: Some("new-lol".into()),
target_update: Some(TargetUpdate {
target: CommitOrChangeId::ChangeId(first_commit_change_id.clone()),
preceding_head: None,
preceding_head_name: None,
}),
description: None,
};
@ -568,7 +568,7 @@ fn update_series_target_success() -> Result<()> {
name: None,
target_update: Some(TargetUpdate {
target: commit_1_change_id.clone(),
preceding_head: None,
preceding_head_name: None,
}),
description: None,
};

View File

@ -177,6 +177,7 @@ fn main() {
virtual_branches::commands::undo_commit,
virtual_branches::commands::insert_blank_commit,
virtual_branches::commands::reorder_commit,
virtual_branches::commands::reorder_stack,
virtual_branches::commands::update_commit_message,
virtual_branches::commands::list_local_branches,
virtual_branches::commands::list_branches,

View File

@ -7,7 +7,7 @@ pub mod commands {
};
use gitbutler_branch_actions::{
BaseBranch, BranchListing, BranchListingDetails, BranchListingFilter, RemoteBranch,
RemoteBranchData, RemoteBranchFile, RemoteCommit, VirtualBranches,
RemoteBranchData, RemoteBranchFile, RemoteCommit, StackOrder, VirtualBranches,
};
use gitbutler_command_context::CommandContext;
use gitbutler_project as projects;
@ -397,6 +397,20 @@ pub mod commands {
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(projects, windows), err(Debug))]
pub fn reorder_stack(
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
branch_id: StackId,
stack_order: StackOrder,
) -> Result<(), Error> {
let project = projects.get(project_id)?;
gitbutler_branch_actions::reorder_stack(&project, branch_id, stack_order)?;
emit_vbranches(&windows, project_id);
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(projects, windows), err(Debug))]
pub fn reorder_commit(