From 2566ed1ca23818cbe1c1bd1d9f4dd373913e7259 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 19 Mar 2026 08:36:23 +0100 Subject: [PATCH] feat(tui): insert commits above or below existing commits Previously it was only possible to insert new commits at the top of a branch, but with this change it's now also possible to insert new commits above or below existing commits. --- crates/but/src/command/legacy/status/mod.rs | 5 + .../but/src/command/legacy/status/output.rs | 34 ++- .../command/legacy/status/tui/cursor/tests.rs | 35 ++- .../legacy/status/tui/graph_extension.rs | 203 +++++++++++++++ .../src/command/legacy/status/tui/key_bind.rs | 19 ++ .../but/src/command/legacy/status/tui/mod.rs | 150 ++++++++--- .../legacy/status/tui/tests/commit_tests.rs | 246 +++++++++++++++++- ...commit_confirm_on_source_is_noop_final.txt | 20 ++ ...es_creates_commit_visible_in_tui_final.txt | 20 ++ ...toggle_commit_target_insert_side_final.txt | 20 ++ .../commit_mode_enter_and_escape_final.txt | 20 ++ ...anges_stays_within_current_stack_final.txt | 20 ++ ...entered_from_non_commitable_rows_final.txt | 20 ++ ...hows_commit_above_on_commit_rows_final.txt | 20 ++ ...ve_creates_commit_visible_in_tui_final.txt | 20 ++ ...ow_creates_commit_visible_in_tui_final.txt | 20 ++ ..._be_forced_to_other_stack_target_final.txt | 20 ++ 17 files changed, 847 insertions(+), 45 deletions(-) create mode 100644 crates/but/src/command/legacy/status/tui/graph_extension.rs create mode 100644 crates/but/src/command/legacy/status/tui/tests/snapshots/commit_confirm_on_source_is_noop_final.txt create mode 100644 crates/but/src/command/legacy/status/tui/tests/snapshots/commit_from_unstaged_changes_creates_commit_visible_in_tui_final.txt create mode 100644 crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_can_toggle_commit_target_insert_side_final.txt create mode 100644 crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_enter_and_escape_final.txt create mode 100644 crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_from_staged_changes_stays_within_current_stack_final.txt create mode 100644 crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_not_entered_from_non_commitable_rows_final.txt create mode 100644 crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_shows_commit_above_on_commit_rows_final.txt create mode 100644 crates/but/src/command/legacy/status/tui/tests/snapshots/commit_to_commit_above_creates_commit_visible_in_tui_final.txt create mode 100644 crates/but/src/command/legacy/status/tui/tests/snapshots/commit_to_commit_below_creates_commit_visible_in_tui_final.txt create mode 100644 crates/but/src/command/legacy/status/tui/tests/snapshots/staged_source_commit_cannot_be_forced_to_other_stack_target_final.txt diff --git a/crates/but/src/command/legacy/status/mod.rs b/crates/but/src/command/legacy/status/mod.rs index f2133f073f..cc26e8524e 100644 --- a/crates/but/src/command/legacy/status/mod.rs +++ b/crates/but/src/command/legacy/status/mod.rs @@ -941,6 +941,7 @@ fn print_group( print_commit( &repo, status_ctx, + stack_with_id.id, commit.short_id.clone(), &commit.inner, CommitChanges::Remote(&details.diff_with_first_parent), @@ -974,6 +975,7 @@ fn print_group( print_commit( &repo, status_ctx, + stack_with_id.id, commit.short_id.clone(), &commit.inner.inner, CommitChanges::Workspace(&commit.tree_changes_using_repo(&repo)?), @@ -1088,6 +1090,7 @@ enum CommitChanges<'a> { fn print_commit( repo: &gix::Repository, status_ctx: &StatusContext<'_>, + stack_id: Option, short_id: ShortId, commit: &but_workspace::ref_info::Commit, commit_changes: CommitChanges, @@ -1166,6 +1169,7 @@ fn print_commit( .collect(), }, commit_cli_id.clone(), + stack_id, )?; let (message, is_empty_message) = commit_message_display_cli( &commit.message, @@ -1220,6 +1224,7 @@ fn print_commit( .collect(), }, commit_cli_id.clone(), + stack_id, )?; } if status_ctx.flags.show_files { diff --git a/crates/but/src/command/legacy/status/output.rs b/crates/but/src/command/legacy/status/output.rs index 1c817c19a3..e40f17f21c 100644 --- a/crates/but/src/command/legacy/status/output.rs +++ b/crates/but/src/command/legacy/status/output.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use gitbutler_stack::StackId; use ratatui::text::Span; use crate::{CliId, command::legacy::status::render_oneshot, utils::WriteWithUtils}; @@ -151,12 +152,14 @@ impl StatusOutput<'_> { connector: Vec>, line: CommitLineContent, id: CliId, + stack_id: Option, ) -> anyhow::Result<()> { self.push_line( Some(connector), StatusOutputContent::Commit(line), StatusOutputLineData::Commit { cli_id: Arc::new(id), + stack_id, }, ) } @@ -309,15 +312,30 @@ impl StatusOutputLine { pub(super) enum StatusOutputLineData { UpdateNotice, Connector, - StagedChanges { cli_id: Arc }, - StagedFile { cli_id: Arc }, - UnstagedChanges { cli_id: Arc }, - UnstagedFile { cli_id: Arc }, - Branch { cli_id: Arc }, - Commit { cli_id: Arc }, + StagedChanges { + cli_id: Arc, + }, + StagedFile { + cli_id: Arc, + }, + UnstagedChanges { + cli_id: Arc, + }, + UnstagedFile { + cli_id: Arc, + }, + Branch { + cli_id: Arc, + }, + Commit { + cli_id: Arc, + stack_id: Option, + }, CommitMessage, EmptyCommitMessage, - File { cli_id: Arc }, + File { + cli_id: Arc, + }, MergeBase, UpstreamChanges, Warning, @@ -333,7 +351,7 @@ impl StatusOutputLineData { | StatusOutputLineData::Branch { cli_id } | StatusOutputLineData::StagedChanges { cli_id } | StatusOutputLineData::StagedFile { cli_id } - | StatusOutputLineData::Commit { cli_id } + | StatusOutputLineData::Commit { cli_id, .. } | StatusOutputLineData::File { cli_id } => Some(cli_id), StatusOutputLineData::UpdateNotice | StatusOutputLineData::Connector diff --git a/crates/but/src/command/legacy/status/tui/cursor/tests.rs b/crates/but/src/command/legacy/status/tui/cursor/tests.rs index 25f79b6cb7..d864accd5e 100644 --- a/crates/but/src/command/legacy/status/tui/cursor/tests.rs +++ b/crates/but/src/command/legacy/status/tui/cursor/tests.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use but_rebase::graph_rebase::mutate::InsertSide; +use gitbutler_stack::StackId; use ratatui_textarea::TextArea; use super::{Cursor, is_selectable_in_mode}; @@ -7,7 +9,7 @@ use crate::{ CliId, command::legacy::status::{ output::{StatusOutputContent, StatusOutputLine, StatusOutputLineData}, - tui::{InlineRewordMode, Mode, RubMode}, + tui::{CommitMode, CommitSource, InlineRewordMode, Mode, RubMode}, }, }; @@ -135,6 +137,7 @@ fn select_finds_commit_line_by_object_id() { }), line(StatusOutputLineData::Commit { cli_id: commit_cli_id(wanted, "c1"), + stack_id: None, }), ]; @@ -148,6 +151,7 @@ fn select_finds_commit_line_by_object_id() { fn select_returns_none_when_commit_is_missing() { let lines = vec![line(StatusOutputLineData::Commit { cli_id: commit_cli_id("1111111111111111111111111111111111111111", "c1"), + stack_id: None, })]; assert_eq!( @@ -165,9 +169,11 @@ fn select_uses_first_matching_commit_when_object_id_appears_multiple_times() { let lines = vec![ line(StatusOutputLineData::Commit { cli_id: commit_cli_id(wanted, "c0"), + stack_id: None, }), line(StatusOutputLineData::Commit { cli_id: commit_cli_id(wanted, "c1"), + stack_id: None, }), ]; @@ -182,6 +188,7 @@ fn select_branch_finds_branch_line_by_name() { let lines = vec![ line(StatusOutputLineData::Commit { cli_id: commit_cli_id("1111111111111111111111111111111111111111", "c0"), + stack_id: None, }), line(StatusOutputLineData::Branch { cli_id: Arc::new(CliId::Branch { @@ -444,6 +451,7 @@ fn selection_cli_id_for_reload_uses_commit_as_parent_for_hidden_file() { let lines = vec![ line(StatusOutputLineData::Commit { cli_id: parent_commit.clone(), + stack_id: None, }), line(StatusOutputLineData::File { cli_id: unassigned("file0"), @@ -801,6 +809,7 @@ fn move_next_section_skips_non_jump_targets_like_commits() { }), line(StatusOutputLineData::Commit { cli_id: commit_cli_id("1111111111111111111111111111111111111111", "c0"), + stack_id: None, }), line(StatusOutputLineData::StagedChanges { cli_id: unassigned("s0"), @@ -825,6 +834,7 @@ fn move_next_section_can_jump_to_merge_base_line() { }), line(StatusOutputLineData::Commit { cli_id: commit_cli_id("1111111111111111111111111111111111111111", "c0"), + stack_id: None, }), line(StatusOutputLineData::MergeBase), ]; @@ -847,6 +857,7 @@ fn move_previous_section_can_jump_from_merge_base_line() { }), line(StatusOutputLineData::Commit { cli_id: commit_cli_id("1111111111111111111111111111111111111111", "c0"), + stack_id: None, }), line(StatusOutputLineData::MergeBase), ]; @@ -1031,3 +1042,25 @@ fn is_selectable_is_true_in_inline_reword_mode() { // Inline reword intentionally returns selectable so rows are not dimmed during editing. assert!(is_selectable_in_mode(&selectable_line, &inline_reword)); } + +#[test] +fn is_selectable_in_commit_mode_scopes_commit_targets_to_stack() { + let scoped_stack_id = StackId::single_branch_id(); + let mode = Mode::Commit(CommitMode { + source: Arc::new(CommitSource::Unassigned { id: "zz".into() }), + scope_to_stack: Some(scoped_stack_id), + insert_side: InsertSide::Above, + }); + + let same_stack_commit_line = line(StatusOutputLineData::Commit { + cli_id: commit_cli_id("1111111111111111111111111111111111111111", "c0"), + stack_id: Some(scoped_stack_id), + }); + let other_stack_commit_line = line(StatusOutputLineData::Commit { + cli_id: commit_cli_id("2222222222222222222222222222222222222222", "c1"), + stack_id: None, + }); + + assert!(is_selectable_in_mode(&same_stack_commit_line, &mode)); + assert!(!is_selectable_in_mode(&other_stack_commit_line, &mode)); +} diff --git a/crates/but/src/command/legacy/status/tui/graph_extension.rs b/crates/but/src/command/legacy/status/tui/graph_extension.rs new file mode 100644 index 0000000000..7fb203635f --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/graph_extension.rs @@ -0,0 +1,203 @@ +use ratatui::text::Span; + +/// Direction in which to extend a graph connector line. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ExtensionDirection { + /// Build the connector for a synthetic line rendered above the source line. + Above, + /// Build the connector for a synthetic line rendered below the source line. + Below, +} + +/// Build a connector extension line for the provided connector spans. +/// +/// The returned connector preserves the input connector width (character count), +/// and applies graph-continuity rules per connector glyph. +/// +/// If the input connector is empty, this returns two spaces (`" "`) so +/// extension-only lines still align with regular graph rows. +pub(super) fn extend_connector_spans( + connector: &[Span<'_>], + direction: ExtensionDirection, + out: &mut E, +) where + E: Extend>, +{ + let connector_text = connector + .iter() + .map(|span| span.content.as_ref()) + .collect::(); + out.extend([Span::raw(extend_connector_text(&connector_text, direction))]); +} + +/// Build a connector extension string for a connector prefix. +/// +/// This function preserves the number of characters in `connector`. +/// +/// Graph rules: +/// - `┊` stays `┊` (column trunk marker) +/// - `│` stays `│` +/// - `●`, `◐`, `├`, `-` continue as `│` in both directions +/// - `╭` starts here: no above continuation, but below becomes `│` +/// - `╯` ends here: above becomes `│`, but no below continuation +/// - `┴` is a terminal merge-base cap: above becomes `│`, below becomes space +/// - `┄` is horizontal-only and becomes space in extension rows +/// - spaces stay spaces +/// +/// Unknown characters are kept as-is. +fn extend_connector_text(connector: &str, direction: ExtensionDirection) -> String { + if connector.is_empty() { + return " ".to_string(); + } + + connector + .chars() + .map(|ch| extension_char(ch, direction)) + .collect() +} + +/// Map one connector glyph to its extension glyph in the chosen direction. +const fn extension_char(ch: char, direction: ExtensionDirection) -> char { + match ch { + ' ' => ' ', + '┊' => '┊', + '│' => '│', + '●' | '◐' | '├' | '-' => '│', + '╭' => match direction { + ExtensionDirection::Above => ' ', + ExtensionDirection::Below => '│', + }, + '╯' => match direction { + ExtensionDirection::Above => '│', + ExtensionDirection::Below => ' ', + }, + '┴' => match direction { + ExtensionDirection::Above => '│', + ExtensionDirection::Below => ' ', + }, + '┄' => ' ', + _ => ch, + } +} + +#[cfg(test)] +mod tests { + use super::{ExtensionDirection, extend_connector_spans, extend_connector_text}; + use ratatui::text::Span; + + #[test] + fn extends_every_connector_shape_emitted_by_status_output() { + let cases = [ + ("╭┄", " ", "│ "), + ("├╯", "││", "│ "), + ("┊", "┊", "┊"), + ("┊╭┄", "┊ ", "┊│ "), + ("┊├┄", "┊│ ", "┊│ "), + ("┊╭┄┄", "┊ ", "┊│ "), + ("┊-", "┊│", "┊│"), + ("┊┊", "┊┊", "┊┊"), + ("┊│", "┊│", "┊│"), + ("┊● ", "┊│ ", "┊│ "), + ("┊◐ ", "┊│ ", "┊│ "), + ("┊ ", "┊ ", "┊ "), + ("┊ ╭┄", "┊ ", "┊ │ "), + ("┊ │", "┊ │", "┊ │"), + ("┊ │ ", "┊ │ ", "┊ │ "), + ("┊│ ", "┊│ ", "┊│ "), + ]; + + for (input, expected_above, expected_below) in cases { + assert_eq!( + extend_connector_text(input, ExtensionDirection::Above), + expected_above, + "above for {input:?}" + ); + assert_eq!( + extend_connector_text(input, ExtensionDirection::Below), + expected_below, + "below for {input:?}" + ); + assert_eq!( + input.chars().count(), + expected_above.chars().count(), + "above width for {input:?}" + ); + assert_eq!( + input.chars().count(), + expected_below.chars().count(), + "below width for {input:?}" + ); + } + } + + #[test] + fn extends_single_character_connectors() { + let cases = [ + ('┊', '┊', '┊'), + ('│', '│', '│'), + ('●', '│', '│'), + ('◐', '│', '│'), + ('├', '│', '│'), + ('-', '│', '│'), + ('╭', ' ', '│'), + ('╯', '│', ' '), + ('┴', '│', ' '), + ('┄', ' ', ' '), + ]; + + for (input, expected_above, expected_below) in cases { + assert_eq!( + extend_connector_text(&input.to_string(), ExtensionDirection::Above), + expected_above.to_string(), + "above for {input:?}" + ); + assert_eq!( + extend_connector_text(&input.to_string(), ExtensionDirection::Below), + expected_below.to_string(), + "below for {input:?}" + ); + } + } + + #[test] + fn empty_input_becomes_two_spaces() { + assert_eq!( + extend_connector_text("", ExtensionDirection::Above), + " ".to_string() + ); + assert_eq!( + extend_connector_text("", ExtensionDirection::Below), + " ".to_string() + ); + } + + #[test] + fn extends_terminal_merge_base_connector_above_and_below() { + let connector = "┴ "; + + assert_eq!( + extend_connector_text(connector, ExtensionDirection::Above), + "│ ".to_string() + ); + assert_eq!( + extend_connector_text(connector, ExtensionDirection::Below), + " ".to_string() + ); + } + + #[test] + fn span_based_api_flattens_and_extends_connector() { + let input = vec![Span::raw("┊"), Span::raw("●"), Span::raw(" ")]; + + let mut above = Vec::>::new(); + extend_connector_spans(&input, ExtensionDirection::Above, &mut above); + + let mut below = Vec::>::new(); + extend_connector_spans(&input, ExtensionDirection::Below, &mut below); + + assert_eq!(above.len(), 1); + assert_eq!(below.len(), 1); + assert_eq!(above[0].content.as_ref(), "┊│ "); + assert_eq!(below[0].content.as_ref(), "┊│ "); + } +} diff --git a/crates/but/src/command/legacy/status/tui/key_bind.rs b/crates/but/src/command/legacy/status/tui/key_bind.rs index 7201d67efe..7a53b1027d 100644 --- a/crates/but/src/command/legacy/status/tui/key_bind.rs +++ b/crates/but/src/command/legacy/status/tui/key_bind.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use but_rebase::graph_rebase::mutate::InsertSide; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use strum::IntoEnumIterator; @@ -274,6 +275,24 @@ fn register_commit_mode_key_binds(key_binds: &mut KeyBinds) { hide_from_hotbar: false, }); + key_binds.register(StaticKeyBind { + short_description: "above", + chord_display: "a", + key_matcher: press().code(KeyCode::Char('a')), + modes: Vec::from([ModeDiscriminant::Commit]), + message: Message::Commit(CommitMessage::SetInsertSide(InsertSide::Above)), + hide_from_hotbar: false, + }); + + key_binds.register(StaticKeyBind { + short_description: "below", + chord_display: "b", + key_matcher: press().code(KeyCode::Char('b')), + modes: Vec::from([ModeDiscriminant::Commit]), + message: Message::Commit(CommitMessage::SetInsertSide(InsertSide::Below)), + hide_from_hotbar: false, + }); + key_binds.register(StaticKeyBind { short_description: "back", chord_display: "esc", diff --git a/crates/but/src/command/legacy/status/tui/mod.rs b/crates/but/src/command/legacy/status/tui/mod.rs index 782489977d..ad3030d9bc 100644 --- a/crates/but/src/command/legacy/status/tui/mod.rs +++ b/crates/but/src/command/legacy/status/tui/mod.rs @@ -17,6 +17,7 @@ use but_rebase::graph_rebase::mutate::InsertSide; use crossterm::event::{self, Event}; use gitbutler_operating_modes::OperatingMode; use gitbutler_stack::StackId; +use itertools::Either; use ratatui::{ Frame, prelude::*, @@ -39,6 +40,7 @@ use crate::{ StatusFlags, StatusOutput, StatusOutputLine, build_status_context, build_status_output, tui::{ cursor::{Cursor, is_selectable_in_mode}, + graph_extension::{ExtensionDirection, extend_connector_spans}, key_bind::{KeyBinds, default_key_binds}, }, }, @@ -51,6 +53,7 @@ use crate::{ use super::output::{StatusOutputContent, StatusOutputLineData}; mod cursor; +mod graph_extension; mod key_bind; mod rub_api; @@ -282,6 +285,9 @@ impl App { CommitMessage::Confirm { with_message } => { self.handle_commit_confirm(ctx, messages, with_message)? } + CommitMessage::SetInsertSide(insert_side) => { + self.handle_commit_set_insert_side(insert_side); + } }, Message::Reword(reword_message) => match reword_message { RewordMessage::WithEditor => { @@ -544,7 +550,7 @@ impl App { commit_result.new_commit, )))); } - StatusOutputLineData::Commit { cli_id } => { + StatusOutputLineData::Commit { cli_id, .. } => { let CliId::Commit { commit_id, .. } = &**cli_id else { return Ok(()); }; @@ -598,6 +604,7 @@ impl App { CommitMode { source: Arc::new(source), scope_to_stack: None, + insert_side: InsertSide::Above, } } StatusOutputLineData::UnstagedFile { cli_id } => { @@ -608,6 +615,7 @@ impl App { CommitMode { source: Arc::new(source), scope_to_stack: None, + insert_side: InsertSide::Above, } } StatusOutputLineData::StagedChanges { cli_id } @@ -619,6 +627,7 @@ impl App { CommitMode { source: Arc::new(source), scope_to_stack: cli_id.stack_id(), + insert_side: InsertSide::Above, } } StatusOutputLineData::UpdateNotice @@ -647,6 +656,7 @@ impl App { let Mode::Commit(CommitMode { source, scope_to_stack, + insert_side, }) = &self.mode else { return Ok(()); @@ -666,21 +676,44 @@ impl App { return Ok(()); } - let StatusOutputLineData::Branch { cli_id: target } = &selection.data else { - return Ok(()); + let target = match &selection.data { + StatusOutputLineData::Branch { cli_id } + | StatusOutputLineData::Commit { cli_id, .. } => cli_id, + StatusOutputLineData::UpdateNotice + | StatusOutputLineData::Connector + | StatusOutputLineData::StagedChanges { .. } + | StatusOutputLineData::StagedFile { .. } + | StatusOutputLineData::UnstagedChanges { .. } + | StatusOutputLineData::UnstagedFile { .. } + | StatusOutputLineData::CommitMessage + | StatusOutputLineData::EmptyCommitMessage + | StatusOutputLineData::File { .. } + | StatusOutputLineData::MergeBase + | StatusOutputLineData::UpstreamChanges + | StatusOutputLineData::Warning + | StatusOutputLineData::Hint + | StatusOutputLineData::NoAssignmentsUnstaged => { + return Ok(()); + } }; - let CliId::Branch { - name: target_branch_name, - .. - } = &**target - else { - return Ok(()); - }; - let target_full_name = { - let repo = ctx.repo.get()?; - let reference = repo.find_reference(target_branch_name)?; - RelativeTo::Reference(reference.name().to_owned()) + let (insert_commit_relative_to, insert_side) = match &**target { + CliId::Branch { name, .. } => { + let repo = ctx.repo.get()?; + let reference = repo.find_reference(name)?; + ( + RelativeTo::Reference(reference.name().to_owned()), + InsertSide::Below, + ) + } + CliId::Commit { commit_id, .. } => (RelativeTo::Commit(*commit_id), *insert_side), + CliId::Uncommitted(_) + | CliId::PathPrefix { .. } + | CliId::CommittedFile { .. } + | CliId::Unassigned { .. } + | CliId::Stack { .. } => { + return Ok(()); + } }; // find what to commit @@ -733,8 +766,8 @@ impl App { // create commit let commit_create_result = but_api::commit::commit_create( ctx, - target_full_name, - InsertSide::Below, + insert_commit_relative_to, + insert_side, changes_to_commit, // we reword the commit with the editor before the next render String::new(), @@ -760,6 +793,12 @@ impl App { Ok(()) } + fn handle_commit_set_insert_side(&mut self, insert_side: InsertSide) { + if let Mode::Commit(mode) = &mut self.mode { + mode.insert_side = insert_side; + } + } + /// Handles opening the full-screen commit reword editor for the selected commit. fn handle_reword_with_editor( &mut self, @@ -956,7 +995,7 @@ impl App { fn selected_commit_id(&self) -> Option { let selection = self.cursor.selected_line(&self.status_lines)?; - let StatusOutputLineData::Commit { cli_id } = &selection.data else { + let StatusOutputLineData::Commit { cli_id, .. } = &selection.data else { return None; }; @@ -985,10 +1024,12 @@ impl App { (area, None) }; - let items = self - .cursor - .iter_lines(&self.status_lines) - .map(|(tui_line, is_selected)| self.render_status_list_item(tui_line, is_selected)); + let items = + self.cursor + .iter_lines(&self.status_lines) + .flat_map(|(tui_line, is_selected)| { + self.render_status_list_item(tui_line, is_selected) + }); let list = List::new(items); frame.render_widget(list, content_area); @@ -1006,7 +1047,7 @@ impl App { &self, tui_line: &StatusOutputLine, is_selected: bool, - ) -> ListItem<'_> { + ) -> impl IntoIterator> { let StatusOutputLine { connector, content, @@ -1035,7 +1076,11 @@ impl App { self.render_rub_api_inline_labels_for_selected_line(data, source, &mut line); } Mode::Commit(mode) => { - self.render_commit_labels_for_selected_line(data, mode, &mut line); + if data.cli_id().is_some_and(|target| *mode.source == **target) + || matches!(data, StatusOutputLineData::Branch { .. }) + { + self.render_commit_labels_for_selected_line(data, mode, &mut line); + } } } } else { @@ -1068,7 +1113,12 @@ impl App { let content_spans = match content { StatusOutputContent::Plain(spans) => spans.clone(), StatusOutputContent::Commit(commit_content) => { - let mut spans = Vec::new(); + let mut spans = Vec::with_capacity( + commit_content.sha.len() + + commit_content.author.len() + + commit_content.message.len() + + commit_content.suffix.len(), + ); spans.extend(commit_content.sha.iter().cloned()); spans.extend(commit_content.author.iter().cloned()); spans.extend(commit_content.message.iter().cloned()); @@ -1105,10 +1155,34 @@ impl App { } if is_selected && !matches!(self.mode, Mode::Command(..)) { - line = line.style(Style::default().bg(CURSOR_BG)); + line = line.bg(CURSOR_BG); } - ListItem::new(line) + if is_selected + && let Mode::Commit(commit_mode) = &self.mode + && matches!(data, StatusOutputLineData::Commit { .. }) + { + let mut extension_line = Line::default().bg(CURSOR_BG); + extend_connector_spans( + connector.as_deref().unwrap_or_default(), + match commit_mode.insert_side { + InsertSide::Above => ExtensionDirection::Above, + InsertSide::Below => ExtensionDirection::Below, + }, + &mut extension_line, + ); + self.render_commit_labels_for_selected_line(data, commit_mode, &mut extension_line); + match commit_mode.insert_side { + InsertSide::Above => { + Either::Right([ListItem::new(extension_line), ListItem::new(line)].into_iter()) + } + InsertSide::Below => { + Either::Right([ListItem::new(line), ListItem::new(extension_line)].into_iter()) + } + } + } else { + Either::Left([ListItem::new(line)].into_iter()) + } } fn render_rub_inline_labels_for_selected_line( @@ -1417,6 +1491,7 @@ enum CommandMessage { enum CommitMessage { CreateEmpty, Start, + SetInsertSide(InsertSide), Confirm { with_message: bool }, } @@ -1462,6 +1537,11 @@ struct CommitMode { /// /// Used when committing changes staged to a specific stack scope_to_stack: Option, + /// The side to insert the new commit on, relative to the target commit. + /// + /// Note this is only respected when inserting at a commit. If inserting at a branch we'll + /// always use [`InsertSide::Below`]. + insert_side: InsertSide, } /// A subset of [`CliId`] that supports being committed @@ -1647,11 +1727,23 @@ fn commit_operation_display( // don't allow selecting branches outside the scoped stack None } else { - Some("commit") + Some("commit to branch") + } + } + StatusOutputLineData::Commit { stack_id, .. } => { + if let Some(stack_scope) = mode.scope_to_stack + && Some(stack_scope) != *stack_id + { + // don't allow selecting commits outside the scoped stack + None + } else { + match mode.insert_side { + InsertSide::Above => Some("insert commit above"), + InsertSide::Below => Some("insert commit below"), + } } } - StatusOutputLineData::Commit { .. } - | StatusOutputLineData::StagedChanges { .. } + StatusOutputLineData::StagedChanges { .. } | StatusOutputLineData::StagedFile { .. } | StatusOutputLineData::UnstagedChanges { .. } | StatusOutputLineData::UnstagedFile { .. } diff --git a/crates/but/src/command/legacy/status/tui/tests/commit_tests.rs b/crates/but/src/command/legacy/status/tui/tests/commit_tests.rs index 273bb18c0e..e7d6dba88f 100644 --- a/crates/but/src/command/legacy/status/tui/tests/commit_tests.rs +++ b/crates/but/src/command/legacy/status/tui/tests/commit_tests.rs @@ -1,6 +1,6 @@ use but_testsupport::Sandbox; use crossterm::event::*; -use snapbox::str; +use snapbox::{file, str}; use temp_env::with_var; use crate::command::legacy::status::tui::tests::utils::test_tui; @@ -23,10 +23,11 @@ fn commit_mode_enter_and_escape() { .assert_current_line_eq(str!["╭┄<< source >> << noop >> zz [unstaged changes]"]); tui.input_then_render(KeyCode::Down) - .assert_current_line_eq(str!["┊╭┄<< commit >> g0 [A]"]); + .assert_current_line_eq(str!["┊╭┄<< commit to branch >> g0 [A]"]); tui.input_then_render(KeyCode::Esc) - .assert_current_line_eq(str!["┊╭┄g0 [A]"]); + .assert_current_line_eq(str!["┊╭┄g0 [A]"]) + .assert_rendered_eq(file!["snapshots/commit_mode_enter_and_escape_final.txt"]); } #[test] @@ -45,7 +46,10 @@ fn commit_confirm_on_source_is_noop() { .assert_current_line_eq(str!["╭┄<< source >> << noop >> zz [unstaged changes]"]); tui.input_then_render(KeyCode::Enter) - .assert_current_line_eq(str!["╭┄zz [unstaged changes]"]); + .assert_current_line_eq(str!["╭┄zz [unstaged changes]"]) + .assert_rendered_eq(file![ + "snapshots/commit_confirm_on_source_is_noop_final.txt" + ]); } #[test] @@ -68,7 +72,10 @@ fn commit_mode_not_entered_from_non_commitable_rows() { .assert_current_line_eq(str!["┊● 9477ae7 add A"]); tui.input_then_render('c') - .assert_current_line_eq(str!["┊● 9477ae7 add A"]); + .assert_current_line_eq(str!["┊● 9477ae7 add A"]) + .assert_rendered_eq(file![ + "snapshots/commit_mode_not_entered_from_non_commitable_rows_final.txt" + ]); } #[test] @@ -94,7 +101,229 @@ fn commit_from_unstaged_changes_creates_commit_visible_in_tui() { .assert_current_line_eq(str!["╭┄<< source >> << noop >> zz [unstaged changes]"]); tui.input_then_render(KeyCode::Down) - .assert_current_line_eq(str!["┊╭┄<< commit >> g0 [A]"]); + .assert_current_line_eq(str!["┊╭┄<< commit to branch >> g0 [A]"]); + + with_var("GIT_EDITOR", Some(editor_command), || { + tui.input_then_render(KeyCode::Enter) + .assert_current_line_eq(str!["┊● [..] commit from tui test[..]"]); + }); + + tui.input_then_render(None) + .assert_current_line_eq(str!["┊● [..] commit from tui test[..]"]) + .assert_rendered_eq(file![ + "snapshots/commit_from_unstaged_changes_creates_commit_visible_in_tui_final.txt" + ]); +} + +#[test] +fn commit_mode_shows_commit_above_on_commit_rows() { + let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack").unwrap(); + env.setup_metadata(&["A"]).unwrap(); + + let mut tui = test_tui(env); + + tui.env.file("test.txt", "content"); + + tui.input_then_render(None) + .assert_current_line_eq(str!["╭┄zz [unstaged changes]"]); + + tui.input_then_render('c') + .assert_current_line_eq(str!["╭┄<< source >> << noop >> zz [unstaged changes]"]); + + tui.input_then_render([KeyCode::Down, KeyCode::Down]) + .assert_current_line_eq(str!["┊│ << insert commit above >>"]) + .assert_rendered_eq(file![ + "snapshots/commit_mode_shows_commit_above_on_commit_rows_final.txt" + ]); +} + +#[test] +fn commit_mode_can_toggle_commit_target_insert_side() { + let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack").unwrap(); + env.setup_metadata(&["A"]).unwrap(); + + let mut tui = test_tui(env); + + tui.env.file("test.txt", "content"); + + tui.input_then_render(None) + .assert_current_line_eq(str!["╭┄zz [unstaged changes]"]); + + tui.input_then_render('c') + .assert_current_line_eq(str!["╭┄<< source >> << noop >> zz [unstaged changes]"]); + + tui.input_then_render([KeyCode::Down, KeyCode::Down]) + .assert_current_line_eq(str!["┊│ << insert commit above >>"]); + + tui.input_then_render('b') + .assert_current_line_eq(str!["┊● 9477ae7 add A"]); + + tui.input_then_render('a') + .assert_current_line_eq(str!["┊│ << insert commit above >>"]) + .assert_rendered_eq(file![ + "snapshots/commit_mode_can_toggle_commit_target_insert_side_final.txt" + ]); +} + +#[test] +fn commit_to_commit_above_creates_commit_visible_in_tui() { + let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack").unwrap(); + env.setup_metadata(&["A"]).unwrap(); + + env.file( + "editor.sh", + format!("printf '{TEST_EDITOR_MESSAGE}\\n' > \"$1\"\n"), + ); + let editor_path = env.projects_root().join("editor.sh"); + let editor_command = format!("sh {}", editor_path.display()); + + let mut tui = test_tui(env); + + tui.env.file("test.txt", "content"); + + tui.input_then_render(None) + .assert_current_line_eq(str!["╭┄zz [unstaged changes]"]); + + tui.input_then_render('c') + .assert_current_line_eq(str!["╭┄<< source >> << noop >> zz [unstaged changes]"]); + + tui.input_then_render([KeyCode::Down, KeyCode::Down]) + .assert_current_line_eq(str!["┊│ << insert commit above >>"]); + + with_var("GIT_EDITOR", Some(editor_command), || { + tui.input_then_render(KeyCode::Enter) + .assert_current_line_eq(str!["┊● [..] commit from tui test[..]"]); + }); + + tui.input_then_render(None) + .assert_current_line_eq(str!["┊● [..] commit from tui test[..]"]) + .assert_rendered_eq(file![ + "snapshots/commit_to_commit_above_creates_commit_visible_in_tui_final.txt" + ]); +} + +#[test] +fn commit_to_commit_below_creates_commit_visible_in_tui() { + let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack").unwrap(); + env.setup_metadata(&["A"]).unwrap(); + + env.file( + "editor.sh", + format!("printf '{TEST_EDITOR_MESSAGE}\\n' > \"$1\"\n"), + ); + let editor_path = env.projects_root().join("editor.sh"); + let editor_command = format!("sh {}", editor_path.display()); + + let mut tui = test_tui(env); + + tui.env.file("test.txt", "content"); + + tui.input_then_render(None) + .assert_current_line_eq(str!["╭┄zz [unstaged changes]"]); + + tui.input_then_render('c') + .assert_current_line_eq(str!["╭┄<< source >> << noop >> zz [unstaged changes]"]); + + tui.input_then_render([KeyCode::Down, KeyCode::Down]) + .assert_current_line_eq(str!["┊│ << insert commit above >>"]); + + tui.input_then_render('b') + .assert_current_line_eq(str!["┊● 9477ae7 add A"]); + + with_var("GIT_EDITOR", Some(editor_command), || { + tui.input_then_render(KeyCode::Enter) + .assert_current_line_eq(str!["┊● [..] commit from tui test[..]"]); + }); + + tui.input_then_render(None) + .assert_current_line_eq(str!["┊● [..] commit from tui test[..]"]) + .assert_rendered_eq(file![ + "snapshots/commit_to_commit_below_creates_commit_visible_in_tui_final.txt" + ]); +} + +#[test] +fn commit_mode_from_staged_changes_stays_within_current_stack() { + let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks").unwrap(); + env.setup_metadata(&["A", "B"]).unwrap(); + + let mut tui = test_tui(env); + + tui.env.file("test.txt", "content"); + + tui.input_then_render(None) + .assert_current_line_eq(str!["╭┄zz [unstaged changes]"]); + + tui.input_then_render(KeyCode::Down) + .assert_current_line_eq(str!["┊ [..] A test.txt"]); + + tui.input_then_render('r') + .assert_current_line_eq(str!["┊ << source >> << noop >> [..] A test.txt"]); + + tui.input_then_render(KeyCode::Down) + .assert_current_line_eq(str!["┊╭┄<< assign hunks >> [..] [A]"]); + + tui.input_then_render(KeyCode::Enter) + .assert_current_line_eq(str!["┊╭┄[..] [A]"]); + + tui.input_then_render([KeyCode::Up, KeyCode::Up]) + .assert_current_line_eq(str!["┊ ╭┄[..] [staged to A]"]); + + tui.input_then_render('c') + .assert_current_line_eq(str!["┊ ╭┄<< source >> << noop >> [..] [staged to A]"]); + + tui.input_then_render(KeyCode::Down) + .assert_current_line_eq(str!["┊╭┄<< commit to branch >> [..] [A]"]); + + tui.input_then_render(KeyCode::Down) + .assert_current_line_eq(str!["┊│ << insert commit above >>"]); + + tui.input_then_render(KeyCode::Down) + .assert_current_line_eq(str!["┊│ << insert commit above >>"]) + .assert_rendered_eq(file![ + "snapshots/commit_mode_from_staged_changes_stays_within_current_stack_final.txt" + ]); +} + +#[test] +fn staged_source_commit_cannot_be_forced_to_other_stack_target() { + let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks").unwrap(); + env.setup_metadata(&["A", "B"]).unwrap(); + + env.file( + ".git/editor.sh", + format!("printf '{TEST_EDITOR_MESSAGE}\\n' > \"$1\"\n"), + ); + let editor_path = env.projects_root().join(".git/editor.sh"); + let editor_command = format!("sh {}", editor_path.display()); + + let mut tui = test_tui(env); + + tui.env.file("test.txt", "content"); + + tui.input_then_render(None) + .assert_current_line_eq(str!["╭┄zz [unstaged changes]"]); + + tui.input_then_render(KeyCode::Down) + .assert_current_line_eq(str!["┊ [..] A test.txt"]); + + tui.input_then_render('r') + .assert_current_line_eq(str!["┊ << source >> << noop >> [..] A test.txt"]); + + tui.input_then_render(KeyCode::Down) + .assert_current_line_eq(str!["┊╭┄<< assign hunks >> [..] [A]"]); + + tui.input_then_render(KeyCode::Enter) + .assert_current_line_eq(str!["┊╭┄[..] [A]"]); + + tui.input_then_render([KeyCode::Up, KeyCode::Up]) + .assert_current_line_eq(str!["┊ ╭┄[..] [staged to A]"]); + + tui.input_then_render('c') + .assert_current_line_eq(str!["┊ ╭┄<< source >> << noop >> [..] [staged to A]"]); + + tui.input_then_render([KeyCode::Down, KeyCode::Down, KeyCode::Down, KeyCode::Down]) + .assert_current_line_eq(str!["┊│ << insert commit above >>"]); with_var("GIT_EDITOR", Some(editor_command), || { tui.input_then_render(KeyCode::Enter) @@ -102,5 +331,8 @@ fn commit_from_unstaged_changes_creates_commit_visible_in_tui() { }); tui.input_then_render(None) - .assert_current_line_eq(str!["┊● [..] commit from tui test[..]"]); + .assert_current_line_eq(str!["┊● [..] commit from tui test[..]"]) + .assert_rendered_eq(file![ + "snapshots/staged_source_commit_cannot_be_forced_to_other_stack_target_final.txt" + ]); } diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_confirm_on_source_is_noop_final.txt b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_confirm_on_source_is_noop_final.txt new file mode 100644 index 0000000000..d7d347b6a7 --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_confirm_on_source_is_noop_final.txt @@ -0,0 +1,20 @@ +"╭┄zz [unstaged changes] " +"┊ vo A test.txt " +"┊ " +"┊╭┄g0 [A] " +"┊● 9477ae7 add A " +"├╯ " +"┊ " +"┴ 0dc3733 [origin/main] 2000-01-02 add M " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" normal ↓/j down • ↑/k up • f files • q quit • r rub • c commit • n new commit • enter reword inl" diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_from_unstaged_changes_creates_commit_visible_in_tui_final.txt b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_from_unstaged_changes_creates_commit_visible_in_tui_final.txt new file mode 100644 index 0000000000..f4a6943df7 --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_from_unstaged_changes_creates_commit_visible_in_tui_final.txt @@ -0,0 +1,20 @@ +"╭┄zz [unstaged changes] " +"┊ no changes " +"┊ " +"┊╭┄g0 [A] " +"┊● [..] commit from tui test " +"┊● 9477ae7 add A " +"├╯ " +"┊ " +"┴ 0dc3733 [origin/main] 2000-01-02 add M " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" normal ↓/j down • ↑/k up • f files • q quit • r rub • c commit • n new commit • enter reword inl" diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_can_toggle_commit_target_insert_side_final.txt b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_can_toggle_commit_target_insert_side_final.txt new file mode 100644 index 0000000000..487dd5db3b --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_can_toggle_commit_target_insert_side_final.txt @@ -0,0 +1,20 @@ +"╭┄<< source >> zz [unstaged changes] " +"┊ vo A test.txt " +"┊ " +"┊╭┄g0 [A] " +"┊│ << insert commit above >> " +"┊● 9477ae7 add A " +"├╯ " +"┊ " +"┴ 0dc3733 [origin/main] 2000-01-02 add M " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" commit ↓/j down • ↑/k up • f files • q quit • enter commit • a above • b below • esc back " diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_enter_and_escape_final.txt b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_enter_and_escape_final.txt new file mode 100644 index 0000000000..d7d347b6a7 --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_enter_and_escape_final.txt @@ -0,0 +1,20 @@ +"╭┄zz [unstaged changes] " +"┊ vo A test.txt " +"┊ " +"┊╭┄g0 [A] " +"┊● 9477ae7 add A " +"├╯ " +"┊ " +"┴ 0dc3733 [origin/main] 2000-01-02 add M " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" normal ↓/j down • ↑/k up • f files • q quit • r rub • c commit • n new commit • enter reword inl" diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_from_staged_changes_stays_within_current_stack_final.txt b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_from_staged_changes_stays_within_current_stack_final.txt new file mode 100644 index 0000000000..faf2cb08ca --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_from_staged_changes_stays_within_current_stack_final.txt @@ -0,0 +1,20 @@ +"╭┄zz [unstaged changes] " +"┊ no changes " +"┊ " +"┊ ╭┄<< source >> k0 [staged to A] " +"┊ │ yr A test.txt " +"┊ │ " +"┊╭┄g0 [A] " +"┊│ << insert commit above >> " +"┊● 9477ae7 add A " +"├╯ " +"┊ " +"┊╭┄h0 [B] " +"┊● d3e2ba3 add B " +"├╯ " +"┊ " +"┴ 0dc3733 [origin/main] 2000-01-02 add M " +" " +" " +" " +" commit ↓/j down • ↑/k up • f files • q quit • enter commit • a above • b below • esc back " diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_not_entered_from_non_commitable_rows_final.txt b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_not_entered_from_non_commitable_rows_final.txt new file mode 100644 index 0000000000..f8723250b4 --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_not_entered_from_non_commitable_rows_final.txt @@ -0,0 +1,20 @@ +"╭┄zz [unstaged changes] " +"┊ no changes " +"┊ " +"┊╭┄g0 [A] " +"┊● 9477ae7 add A " +"├╯ " +"┊ " +"┴ 0dc3733 [origin/main] 2000-01-02 add M " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" normal ↓/j down • ↑/k up • f files • q quit • r rub • c commit • n new commit • enter reword inl" diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_shows_commit_above_on_commit_rows_final.txt b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_shows_commit_above_on_commit_rows_final.txt new file mode 100644 index 0000000000..487dd5db3b --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_mode_shows_commit_above_on_commit_rows_final.txt @@ -0,0 +1,20 @@ +"╭┄<< source >> zz [unstaged changes] " +"┊ vo A test.txt " +"┊ " +"┊╭┄g0 [A] " +"┊│ << insert commit above >> " +"┊● 9477ae7 add A " +"├╯ " +"┊ " +"┴ 0dc3733 [origin/main] 2000-01-02 add M " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" commit ↓/j down • ↑/k up • f files • q quit • enter commit • a above • b below • esc back " diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_to_commit_above_creates_commit_visible_in_tui_final.txt b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_to_commit_above_creates_commit_visible_in_tui_final.txt new file mode 100644 index 0000000000..f4a6943df7 --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_to_commit_above_creates_commit_visible_in_tui_final.txt @@ -0,0 +1,20 @@ +"╭┄zz [unstaged changes] " +"┊ no changes " +"┊ " +"┊╭┄g0 [A] " +"┊● [..] commit from tui test " +"┊● 9477ae7 add A " +"├╯ " +"┊ " +"┴ 0dc3733 [origin/main] 2000-01-02 add M " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" normal ↓/j down • ↑/k up • f files • q quit • r rub • c commit • n new commit • enter reword inl" diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_to_commit_below_creates_commit_visible_in_tui_final.txt b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_to_commit_below_creates_commit_visible_in_tui_final.txt new file mode 100644 index 0000000000..4894e914da --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_to_commit_below_creates_commit_visible_in_tui_final.txt @@ -0,0 +1,20 @@ +"╭┄zz [unstaged changes] " +"┊ no changes " +"┊ " +"┊╭┄g0 [A] " +"┊● [..] add A " +"┊● [..] commit from tui test " +"├╯ " +"┊ " +"┴ 0dc3733 [origin/main] 2000-01-02 add M " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" normal ↓/j down • ↑/k up • f files • q quit • r rub • c commit • n new commit • enter reword inl" diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/staged_source_commit_cannot_be_forced_to_other_stack_target_final.txt b/crates/but/src/command/legacy/status/tui/tests/snapshots/staged_source_commit_cannot_be_forced_to_other_stack_target_final.txt new file mode 100644 index 0000000000..54c902bdb0 --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/staged_source_commit_cannot_be_forced_to_other_stack_target_final.txt @@ -0,0 +1,20 @@ +"╭┄zz [unstaged changes] " +"┊ no changes " +"┊ " +"┊╭┄g0 [A] " +"┊● [..] commit from tui test " +"┊● 9477ae7 add A " +"├╯ " +"┊ " +"┊╭┄h0 [B] " +"┊● d3e2ba3 add B " +"├╯ " +"┊ " +"┴ 0dc3733 [origin/main] 2000-01-02 add M " +" " +" " +" " +" " +" " +" " +" normal ↓/j down • ↑/k up • f files • q quit • r rub • c commit • n new commit • enter reword inl"