mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-24 10:02:26 +03:00
Merge pull request #5240 from gitbutlerapp/implement-reorder-stack
Implement stack commit reordering
This commit is contained in:
commit
a9c600e1a5
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
412
crates/gitbutler-branch-actions/src/reorder.rs
Normal file
412
crates/gitbutler-branch-actions/src/reorder.rs
Normal 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(),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
mod virtual_branches;
|
||||
|
||||
mod extra;
|
||||
|
||||
mod reorder;
|
||||
|
40
crates/gitbutler-branch-actions/tests/fixtures/reorder.sh
vendored
Normal file
40
crates/gitbutler-branch-actions/tests/fixtures/reorder.sh
vendored
Normal 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"
|
||||
)
|
245
crates/gitbutler-branch-actions/tests/reorder.rs
Normal file
245
crates/gitbutler-branch-actions/tests/reorder.rs
Normal 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>,
|
||||
}
|
@ -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.
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user