|
| 1 | +//! Cherry Apply - Applying individual commits into the workspace. |
| 2 | +//! |
| 3 | +//! For now this doesn't consider the single branch mode, but it hopfully |
| 4 | +//! shouldn't be too much of a strech to adapt it to work. |
| 5 | +//! |
| 6 | +//! We want to have two steps: |
| 7 | +//! - cherry_apply_status: Returns a list of stack IDs where a given commit can |
| 8 | +//! be applied to |
| 9 | +//! - cherry_apply: Executes the apply |
| 10 | +//! |
| 11 | +//! ## Getting the status |
| 12 | +//! |
| 13 | +//! - list out the applied stacks with stacks_v3 |
| 14 | +//! - simulate cherry picking the desired commit on to each of the stacks |
| 15 | +//! - if the cherry pick results in a conflict with one of the stacks, it MUST |
| 16 | +//! be applied there |
| 17 | +//! - if the cherry pick results in conflicts with multiple stacks, it can't |
| 18 | +//! be applied since it will cause a workspace conflict. |
| 19 | +//! There is the chance that this looks like this because the commit is |
| 20 | +//! instead conflicting your workspace's base, but this is hard to |
| 21 | +//! disambiguate accurately. |
| 22 | +//! |
| 23 | +//! - otherwise, it can be applied anywhere |
| 24 | +
|
| 25 | +use anyhow::{Context, Result}; |
| 26 | +use but_graph::VirtualBranchesTomlMetadata; |
| 27 | +use but_workspace::{StackId, StacksFilter, stacks_v3}; |
| 28 | +use gitbutler_command_context::CommandContext; |
| 29 | +use gitbutler_oxidize::GixRepositoryExt; |
| 30 | +use gix::{ObjectId, Repository}; |
| 31 | +use serde::Serialize; |
| 32 | + |
| 33 | +#[derive(Debug, Clone, Serialize)] |
| 34 | +#[serde(tag = "type", content = "subject", rename_all = "camelCase")] |
| 35 | +pub enum CherryApplyStatus { |
| 36 | + CausesWorkspaceConflict, |
| 37 | + /// This also means that when it gets applied to the stack, it will be in a conflicted state |
| 38 | + LockedToStack(StackId), |
| 39 | + ApplicableToAnyStack, |
| 40 | + NoStacks, |
| 41 | +} |
| 42 | + |
| 43 | +pub fn cherry_apply_status(ctx: &CommandContext, subject: ObjectId) -> Result<CherryApplyStatus> { |
| 44 | + let repo = ctx.gix_repo()?; |
| 45 | + let project = ctx.project(); |
| 46 | + let meta = |
| 47 | + VirtualBranchesTomlMetadata::from_path(project.gb_dir().join("virtual_branches.toml"))?; |
| 48 | + let stacks = stacks_v3(&repo, &meta, StacksFilter::InWorkspace, None)?; |
| 49 | + |
| 50 | + if stacks.is_empty() { |
| 51 | + return Ok(CherryApplyStatus::NoStacks); |
| 52 | + } |
| 53 | + |
| 54 | + let mut locked_stack = None; |
| 55 | + for stack in stacks { |
| 56 | + let tip = stack |
| 57 | + .heads |
| 58 | + .first() |
| 59 | + .context("Stacks always have a head")? |
| 60 | + .tip; |
| 61 | + if cherry_pick_conflicts(&repo, subject, tip)? { |
| 62 | + if locked_stack.is_some() { |
| 63 | + // Locked stack has already been set to another stack. Now there |
| 64 | + // are at least two stacks that it should be locked to, so we |
| 65 | + // can return early. |
| 66 | + return Ok(CherryApplyStatus::CausesWorkspaceConflict); |
| 67 | + } else { |
| 68 | + locked_stack = Some( |
| 69 | + stack |
| 70 | + .id |
| 71 | + .context("Currently cherry-apply only works with stacks that have ids")?, |
| 72 | + ); |
| 73 | + } |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + if let Some(stack) = locked_stack { |
| 78 | + Ok(CherryApplyStatus::LockedToStack(stack)) |
| 79 | + } else { |
| 80 | + Ok(CherryApplyStatus::ApplicableToAnyStack) |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +// Can a given commit be cleanly cherry picked onto another commit |
| 85 | +fn cherry_pick_conflicts(repo: &Repository, from: ObjectId, onto: ObjectId) -> Result<bool> { |
| 86 | + let from = repo.find_commit(from)?; |
| 87 | + let onto = repo.find_commit(onto)?; |
| 88 | + let base = from |
| 89 | + .parent_ids() |
| 90 | + .next() |
| 91 | + .context("The commit to be cherry picked must have a parent")? |
| 92 | + .object()? |
| 93 | + .into_commit(); |
| 94 | + |
| 95 | + let (merge_options_fail_fast, conflict_kind) = repo.merge_options_no_rewrites_fail_fast()?; |
| 96 | + let result = repo.merge_trees( |
| 97 | + base.tree_id()?, |
| 98 | + from.tree_id()?, |
| 99 | + onto.tree_id()?, |
| 100 | + repo.default_merge_labels(), |
| 101 | + merge_options_fail_fast, |
| 102 | + )?; |
| 103 | + |
| 104 | + Ok(result.has_unresolved_conflicts(conflict_kind)) |
| 105 | +} |
0 commit comments