Skip to content

Commit e223291

Browse files
authored
Merge pull request #13313 from gitbutlerapp/davids-rescue-mission
A grab bag of TUI UX changes
2 parents e10ce03 + 0128ff9 commit e223291

142 files changed

Lines changed: 15020 additions & 14933 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/but/src/command/legacy/rub/mod.rs

Lines changed: 125 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ pub(crate) struct StackToBranchOperation<'a> {
9999
pub(crate) to: &'a str,
100100
}
101101

102+
/// Represents squashing all assignments from a stack to a commit.
103+
#[derive(Debug)]
104+
pub(crate) struct StackToCommitOperation {
105+
/// The source stack id.
106+
pub(crate) from: StackId,
107+
pub(crate) to: gix::ObjectId,
108+
}
109+
102110
/// Represents amending all unassigned hunks into a commit.
103111
#[derive(Debug)]
104112
pub(crate) struct UnassignedToCommitOperation {
@@ -122,11 +130,20 @@ pub(crate) struct UnassignedToStackOperation {
122130

123131
/// Represents undoing a commit.
124132
#[derive(Debug)]
125-
pub(crate) struct UndoCommitOperation {
133+
pub(crate) struct CommitToUnassignedOperation {
126134
/// The commit id to undo.
127135
pub(crate) oid: gix::ObjectId,
128136
}
129137

138+
/// Represents undoing a commit to a stack.
139+
#[derive(Debug)]
140+
pub(crate) struct CommitToStackOperation {
141+
/// The commit id to undo.
142+
pub(crate) oid: gix::ObjectId,
143+
/// The stack to assign the changes to.
144+
pub(crate) stack: StackId,
145+
}
146+
130147
/// Represents squashing one commit into another.
131148
#[derive(Debug)]
132149
pub(crate) struct SquashCommitsOperation {
@@ -221,10 +238,12 @@ pub(crate) enum RubOperation<'a> {
221238
StackToUnassigned(StackToUnassignedOperation),
222239
StackToStack(StackToStackOperation),
223240
StackToBranch(StackToBranchOperation<'a>),
241+
StackToCommit(StackToCommitOperation),
224242
UnassignedToCommit(UnassignedToCommitOperation),
225243
UnassignedToBranch(UnassignedToBranchOperation<'a>),
226244
UnassignedToStack(UnassignedToStackOperation),
227-
UndoCommit(UndoCommitOperation),
245+
CommitToUnassigned(CommitToUnassignedOperation),
246+
CommitToStack(CommitToStackOperation),
228247
SquashCommits(SquashCommitsOperation),
229248
MoveCommitToBranch(MoveCommitToBranchOperation<'a>),
230249
BranchToUnassigned(BranchToUnassignedOperation<'a>),
@@ -425,6 +444,44 @@ impl<'a> StackToBranchOperation<'a> {
425444
}
426445
}
427446

447+
impl StackToCommitOperation {
448+
/// Executes this operation.
449+
pub(crate) fn execute(self, ctx: &mut Context, out: &mut OutputChannel) -> anyhow::Result<()> {
450+
let result = self.execute_inner(ctx)?;
451+
if let Some(out) = out.for_human() {
452+
let repo = ctx.repo.get()?;
453+
let new_commit = result
454+
.new_commit
455+
.map(|c| {
456+
let short = shorten_object_id(&repo, c);
457+
let (lead, rest) = split_short_id(&short, 2);
458+
format!("{}{}", lead.blue().bold(), rest.blue())
459+
})
460+
.unwrap_or_default();
461+
writeln!(
462+
out,
463+
"Amended files assigned to {} → {}",
464+
stack_id_to_branch_name(ctx, self.from)
465+
.map(|b| format!("[{b}]").green())
466+
.unwrap_or_else(|| "stack".to_string().bold()),
467+
new_commit,
468+
)?;
469+
} else if let Some(out) = out.for_json() {
470+
out.write_value(serde_json::json!({
471+
"ok": true,
472+
"new_commit_id": result.new_commit.map(|c| c.to_string()),
473+
}))?;
474+
}
475+
Ok(())
476+
}
477+
478+
/// Executes `StackToCommit` by squashing all hunks from the source stack to the target commit.
479+
pub(crate) fn execute_inner(&self, ctx: &mut Context) -> anyhow::Result<CommitCreateResult> {
480+
let changes = changes_for_stack_assignment(ctx, Some(self.from))?;
481+
but_api::commit::amend::commit_amend(ctx, self.to, changes, DryRun::No)
482+
}
483+
}
484+
428485
impl UnassignedToCommitOperation {
429486
/// Executes this operation.
430487
pub(crate) fn execute(self, ctx: &mut Context, out: &mut OutputChannel) -> anyhow::Result<()> {
@@ -505,7 +562,7 @@ impl UnassignedToStackOperation {
505562
}
506563
}
507564

508-
impl UndoCommitOperation {
565+
impl CommitToUnassignedOperation {
509566
/// Executes this operation.
510567
pub(crate) fn execute(self, ctx: &mut Context, out: &mut OutputChannel) -> anyhow::Result<()> {
511568
self.execute_inner(ctx)?;
@@ -524,11 +581,36 @@ impl UndoCommitOperation {
524581

525582
/// Executes `UndoCommit` by uncommitting all changes from the selected commit.
526583
pub(crate) fn execute_inner(&self, ctx: &mut Context) -> anyhow::Result<CommitUndoResult> {
527-
// TODO(David): Have fun. - Love, Caleb
528584
but_api::commit::undo::commit_undo(ctx, self.oid, None, DryRun::No)
529585
}
530586
}
531587

588+
impl CommitToStackOperation {
589+
/// Executes this operation.
590+
pub(crate) fn execute(self, ctx: &mut Context, out: &mut OutputChannel) -> anyhow::Result<()> {
591+
self.execute_inner(ctx)?;
592+
if let Some(out) = out.for_human() {
593+
let repo = ctx.repo.get()?;
594+
writeln!(
595+
out,
596+
"Uncommitted {} to {}",
597+
shorten_object_id(&repo, self.oid).blue(),
598+
stack_id_to_branch_name(ctx, self.stack)
599+
.map(|b| format!("[{b}]").green())
600+
.unwrap_or_else(|| "stack".bold()),
601+
)?;
602+
} else if let Some(out) = out.for_json() {
603+
out.write_value(serde_json::json!({"ok": true}))?;
604+
}
605+
Ok(())
606+
}
607+
608+
/// Executes `UndoCommit` by uncommitting all changes from the selected commit.
609+
pub(crate) fn execute_inner(&self, ctx: &mut Context) -> anyhow::Result<CommitUndoResult> {
610+
but_api::commit::undo::commit_undo(ctx, self.oid, Some(self.stack), DryRun::No)
611+
}
612+
}
613+
532614
impl SquashCommitsOperation {
533615
/// Executes this operation.
534616
pub(crate) fn execute(self, ctx: &mut Context, out: &mut OutputChannel) -> anyhow::Result<()> {
@@ -795,7 +877,8 @@ impl<'a> RubOperation<'a> {
795877
RubOperation::UnassignedToCommit(operation) => operation.execute(ctx, out),
796878
RubOperation::UnassignedToBranch(operation) => operation.execute(ctx, out),
797879
RubOperation::UnassignedToStack(operation) => operation.execute(ctx, out),
798-
RubOperation::UndoCommit(operation) => operation.execute(ctx, out),
880+
RubOperation::CommitToUnassigned(operation) => operation.execute(ctx, out),
881+
RubOperation::CommitToStack(operation) => operation.execute(ctx, out),
799882
RubOperation::SquashCommits(operation) => operation.execute(ctx, out),
800883
RubOperation::MoveCommitToBranch(operation) => operation.execute(ctx, out),
801884
RubOperation::BranchToUnassigned(operation) => operation.execute(ctx, out),
@@ -805,6 +888,7 @@ impl<'a> RubOperation<'a> {
805888
RubOperation::CommittedFileToBranch(operation) => operation.execute(ctx, out),
806889
RubOperation::CommittedFileToCommit(operation) => operation.execute(ctx, out),
807890
RubOperation::CommittedFileToUnassigned(operation) => operation.execute(ctx, out),
891+
RubOperation::StackToCommit(operation) => operation.execute(ctx, out),
808892
}
809893
}
810894
}
@@ -951,6 +1035,12 @@ pub(crate) fn route_operation<'a>(
9511035
to,
9521036
}))
9531037
}
1038+
(Stack { stack_id, .. }, Commit { commit_id, .. }) => {
1039+
Some(RubOperation::StackToCommit(StackToCommitOperation {
1040+
from: *stack_id,
1041+
to: *commit_id,
1042+
}))
1043+
}
9541044
// Unassigned -> *
9551045
(Unassigned { .. }, Commit { commit_id, .. }) => Some(RubOperation::UnassignedToCommit(
9561046
UnassignedToCommitOperation { oid: *commit_id },
@@ -962,11 +1052,9 @@ pub(crate) fn route_operation<'a>(
9621052
UnassignedToStackOperation { to: *stack_id },
9631053
)),
9641054
// Commit -> *
965-
(Commit { commit_id, .. }, Unassigned { .. }) => {
966-
Some(RubOperation::UndoCommit(UndoCommitOperation {
967-
oid: *commit_id,
968-
}))
969-
}
1055+
(Commit { commit_id, .. }, Unassigned { .. }) => Some(RubOperation::CommitToUnassigned(
1056+
CommitToUnassignedOperation { oid: *commit_id },
1057+
)),
9701058
(
9711059
Commit {
9721060
commit_id: source, ..
@@ -985,6 +1073,12 @@ pub(crate) fn route_operation<'a>(
9851073
name,
9861074
},
9871075
)),
1076+
(Commit { commit_id, .. }, Stack { stack_id, .. }) => {
1077+
Some(RubOperation::CommitToStack(CommitToStackOperation {
1078+
oid: *commit_id,
1079+
stack: *stack_id,
1080+
}))
1081+
}
9881082
// Branch -> *
9891083
(Branch { name, .. }, Unassigned { .. }) => Some(RubOperation::BranchToUnassigned(
9901084
BranchToUnassignedOperation { from: name },
@@ -1748,12 +1842,12 @@ mod tests {
17481842
// Valid: Commit -> Branch
17491843
assert!(route_operation(&commit, &branch_id()).is_some());
17501844

1845+
// Valid: Commit -> Stack
1846+
assert!(route_operation(&commit, &stack_id()).is_some());
1847+
17511848
// Invalid: Commit -> Uncommitted
17521849
assert!(route_operation(&commit, &uncommitted_id()).is_none());
17531850

1754-
// Invalid: Commit -> Stack
1755-
assert!(route_operation(&commit, &stack_id()).is_none());
1756-
17571851
// Invalid: Commit -> CommittedFile
17581852
assert!(route_operation(&commit, &committed_file_id()).is_none());
17591853
}
@@ -1794,12 +1888,12 @@ mod tests {
17941888
// Valid: Stack -> Branch
17951889
assert!(route_operation(&stack, &branch_id()).is_some());
17961890

1891+
// Valid: Stack -> Commit
1892+
assert!(route_operation(&stack, &commit_id()).is_some());
1893+
17971894
// Invalid: Stack -> Uncommitted
17981895
assert!(route_operation(&stack, &uncommitted_id()).is_none());
17991896

1800-
// Invalid: Stack -> Commit
1801-
assert!(route_operation(&stack, &commit_id()).is_none());
1802-
18031897
// Invalid: Stack -> CommittedFile
18041898
assert!(route_operation(&stack, &committed_file_id()).is_none());
18051899
}
@@ -1882,10 +1976,16 @@ mod tests {
18821976
_ => panic!("Expected SquashCommits variant"),
18831977
}
18841978

1885-
// Commit -> Unassigned should be UndoCommit
1979+
// Commit -> Unassigned should be CommitToUnassigned
18861980
match route_operation(&commit, &unassigned) {
1887-
Some(RubOperation::UndoCommit(..)) => {}
1888-
_ => panic!("Expected UndoCommit variant"),
1981+
Some(RubOperation::CommitToUnassigned(..)) => {}
1982+
_ => panic!("Expected CommitToUnassigned variant"),
1983+
}
1984+
1985+
// Commit -> Stack should be CommitToStack
1986+
match route_operation(&commit, &stack) {
1987+
Some(RubOperation::CommitToStack(..)) => {}
1988+
_ => panic!("Expected CommitToStack variant"),
18891989
}
18901990

18911991
// Branch -> Stack should be BranchToStack
@@ -1900,6 +2000,12 @@ mod tests {
19002000
_ => panic!("Expected StackToBranch variant"),
19012001
}
19022002

2003+
// Stack -> Commit should be StackToCommit
2004+
match route_operation(&stack, &commit) {
2005+
Some(RubOperation::StackToCommit(..)) => {}
2006+
_ => panic!("Expected StackToCommit variant"),
2007+
}
2008+
19032009
// CommittedFile -> Commit should be CommittedFileToCommit
19042010
match route_operation(&committed_file, &commit) {
19052011
Some(RubOperation::CommittedFileToCommit(..)) => {}

crates/but/src/command/legacy/status/mod.rs

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use std::collections::BTreeMap;
77

8+
use anyhow::Context as _;
89
use assignment::FileAssignment;
910
use bstr::{BStr, BString, ByteSlice};
1011
use but_api::diff::ComputeLineStats;
@@ -66,6 +67,16 @@ impl StatusFlags {
6667
hint: false,
6768
}
6869
}
70+
71+
pub fn for_tui() -> Self {
72+
Self {
73+
show_files: FilesStatusFlag::None,
74+
verbose: false,
75+
refresh_prs: false,
76+
show_upstream: false,
77+
hint: false,
78+
}
79+
}
6980
}
7081

7182
#[derive(Debug, Copy, Clone)]
@@ -708,28 +719,25 @@ fn print_assignments(
708719
unstaged: bool,
709720
output: &mut StatusOutput<'_>,
710721
) -> anyhow::Result<()> {
711-
// if there are no assignments and we're in the unstaged section, print "(no changes)" and return
712-
if assignments.is_empty() && unstaged {
713-
output.no_assignments_unstaged(
714-
Vec::from([Span::raw("┊ ")]),
715-
Vec::from([Span::styled("no changes", Style::default().dim().italic())]),
716-
)?;
717-
return Ok(());
718-
}
719-
720722
let id = stack
721723
.and_then(|s| status_ctx.id_map.resolve_stack(s))
722724
.map(|s| Span::styled(s.to_short_string(), Style::default().bold().blue()))
723725
.unwrap_or_default();
724726

725-
if !unstaged && !assignments.is_empty() {
726-
let staged_changes_cli_id = stack
727-
.and_then(|stack_id| status_ctx.id_map.resolve_stack(stack_id).cloned())
728-
.ok_or_else(|| anyhow::anyhow!("Could not resolve stack CLI id for staged changes"))?;
727+
if let Some(stack) = stack
728+
&& (!unstaged && !assignments.is_empty())
729+
{
730+
let staged_changes_cli_id = status_ctx
731+
.id_map
732+
.resolve_stack(stack)
733+
.cloned()
734+
.with_context(|| {
735+
format!("Could not resolve stack CLI id for staged changes. stack_id={stack:?}")
736+
})?;
729737

730738
output.staged_changes(
731739
Vec::from([Span::raw("┊ ╭┄")]),
732-
Vec::from([
740+
[
733741
id,
734742
Span::raw(" ["),
735743
Span::styled(
@@ -740,7 +748,21 @@ fn print_assignments(
740748
Style::default().cyan().bold(),
741749
),
742750
Span::raw("]"),
743-
]),
751+
]
752+
.into_iter()
753+
.chain(
754+
assignments
755+
.is_empty()
756+
.then(|| {
757+
[
758+
Span::raw(" "),
759+
Span::styled("(no changes)", Style::default().dim().italic()),
760+
]
761+
})
762+
.into_iter()
763+
.flatten(),
764+
)
765+
.collect(),
744766
staged_changes_cli_id,
745767
)?;
746768
}
@@ -1015,15 +1037,22 @@ fn print_group(
10151037
Style::default().bold().blue(),
10161038
),
10171039
Span::raw(" ["),
1018-
Span::styled("unstaged changes", Style::default().bold().cyan()),
1040+
Span::styled("unassigned changes", Style::default().bold().cyan()),
10191041
Span::raw("]"),
10201042
]);
1043+
if assignments.is_empty() {
1044+
line.extend([
1045+
Span::raw(" "),
1046+
Span::styled("(no changes)", Style::default().dim().italic()),
1047+
]);
1048+
}
10211049
if let Some(stack_mark) = stack_mark {
1022-
line.push(Span::raw(" "));
1023-
line.push(stack_mark.clone());
1050+
line.extend([Span::raw(" "), stack_mark.clone()]);
10241051
}
10251052
output.unstaged_changes(Vec::from([Span::raw("╭┄")]), line, cli_id.clone())?;
1026-
print_assignments(&repo, status_ctx, None, None, assignments, true, output)?;
1053+
if !assignments.is_empty() {
1054+
print_assignments(&repo, status_ctx, None, None, assignments, true, output)?;
1055+
}
10271056
}
10281057
if !first {
10291058
output.connector(Vec::from([Span::raw("├╯")]))?;

crates/but/src/command/legacy/status/output.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ impl StatusOutput<'_> {
210210
)
211211
}
212212

213+
#[expect(dead_code)]
213214
pub(super) fn no_assignments_unstaged(
214215
&mut self,
215216
connector: Vec<Span<'static>>,
@@ -299,7 +300,7 @@ pub(super) struct StatusOutputLine {
299300
///
300301
/// Example:
301302
///
302-
/// ╭┄zz [unstaged changes] | Some("╭┄")
303+
/// ╭┄zz [unassigned changes] | Some("╭┄")
303304
/// ┊ ur M flake.nix | Some("┊ ")
304305
/// ┊ | Some("┊ ")
305306
/// ┊╭┄dp [dp-branch-4] | Some("┊╭┄")

0 commit comments

Comments
 (0)