mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-28 12:05:22 +03:00
Reorder stack API stub
This commit is contained in:
parent
34cb68e52c
commit
16135675bc
@ -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;
|
||||
mod reorder;
|
||||
pub use reorder::{SeriesOrder, StackOrder};
|
||||
mod reorder_commits;
|
||||
mod undo_commit;
|
||||
|
||||
|
345
crates/gitbutler-branch-actions/src/reorder.rs
Normal file
345
crates/gitbutler-branch-actions/src/reorder.rs
Normal file
@ -0,0 +1,345 @@
|
||||
use anyhow::{bail, Result};
|
||||
use git2::Oid;
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_project::access::WorktreeWritePermission;
|
||||
use gitbutler_stack::{Series, StackId};
|
||||
|
||||
use itertools::Itertools;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::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,
|
||||
stack_order: StackOrder,
|
||||
_perm: &mut WorktreeWritePermission,
|
||||
) -> Result<()> {
|
||||
let state = ctx.project().virtual_branches();
|
||||
let stack = state.get_branch(branch_id)?;
|
||||
let all_series = stack.list_series(ctx)?;
|
||||
stack_order.validate(series_order(&all_series))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Represents the order of series (branches) and changes (commits) in a stack.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StackOrder {
|
||||
/// The series are ordered from newest to oldest (most recent stacks go first)
|
||||
series: Vec<SeriesOrder>,
|
||||
}
|
||||
|
||||
/// Represents the order of changes (commits) in a series (branch).
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct SeriesOrder {
|
||||
/// Unique name of the series (branch). Must already exist in the stack.
|
||||
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")]
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
fn series_order(all_series: &[Series<'_>]) -> StackOrder {
|
||||
let series_order: Vec<SeriesOrder> = all_series
|
||||
.iter()
|
||||
.map(|series| {
|
||||
let commit_ids = series
|
||||
.local_commits
|
||||
.iter()
|
||||
.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(),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
@ -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