Skip to content

Commit 6fef2b7

Browse files
committed
but discard commit/file
Add the ability to discard a commit or file completely Usage: ``` but uncommit <commit-id> -d but uncommit <committed-file-id> -d ``` Update the skills file
1 parent b4f34bc commit 6fef2b7

6 files changed

Lines changed: 378 additions & 29 deletions

File tree

crates/but/skill/references/reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ Uncommit changes back to unstaged area.
334334
```bash
335335
but uncommit <commit-id> # Uncommit entire commit
336336
but uncommit <file-id> # Uncommit specific file from its commit
337+
but uncommit <commit-id> -d # Discard committed changes instead of moving to unassigned
338+
but uncommit <file-id> --discard # Discard committed file changes completely
337339
but uncommit <commit-id> --status-after # Uncommit then show workspace status
338340
```
339341

crates/but/src/args/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,12 +862,17 @@ pub enum Subcommands {
862862

863863
/// Uncommit changes from a commit or file-in-commit to the unstaged area.
864864
///
865+
/// Use `--discard` to remove the selected committed changes entirely instead.
866+
///
865867
/// Wrapper for `but rub <source> zz`.
866868
#[cfg(feature = "legacy")]
867869
#[clap(verbatim_doc_comment)]
868870
Uncommit {
869871
/// Commit ID or file-in-commit ID to uncommit
870872
source: String,
873+
/// Discard the selected committed changes instead of moving them to unassigned
874+
#[clap(long, short = 'd')]
875+
discard: bool,
871876
},
872877

873878
/// Amend a file change into a specific commit and rebases any dependent commits.

crates/but/src/command/commit/file.rs

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::{Context as _, Result};
22
use bstr::BStr;
3+
use bstr::ByteSlice;
34
use but_core::{DiffSpec, diff::tree_changes};
45
use but_ctx::Context;
56
use gitbutler_branch_actions::update_workspace_commit;
@@ -13,18 +14,7 @@ pub fn commited_file_to_another_commit(
1314
target_id: gix::ObjectId,
1415
out: &mut OutputChannel,
1516
) -> Result<()> {
16-
let relevant_changes = {
17-
let repo = ctx.repo.get()?;
18-
let source_commit = repo.find_commit(source_id)?;
19-
let source_commit_parent_id = source_commit.parent_ids().next().context("First parent")?;
20-
21-
let tree_changes = tree_changes(&repo, Some(source_commit_parent_id.detach()), source_id)?;
22-
tree_changes
23-
.into_iter()
24-
.filter(|tc| tc.path == path)
25-
.map(Into::into)
26-
.collect::<Vec<DiffSpec>>()
27-
};
17+
let relevant_changes = changes_for_path_in_commit(ctx, path, source_id)?;
2818

2919
but_api::commit::move_changes::commit_move_changes_between_only(
3020
ctx,
@@ -53,20 +43,7 @@ pub fn uncommit_file(
5343
) -> Result<()> {
5444
// Convert target_branch to StackId if provided (for hunk assignment after uncommit)
5545
let assign_to = find_stack_id_for_branch(ctx, target_branch)?;
56-
57-
let relevant_changes = {
58-
let repo = ctx.repo.get()?;
59-
60-
let source_commit = repo.find_commit(source_id)?;
61-
let source_commit_parent_id = source_commit.parent_ids().next().context("First parent")?;
62-
63-
let tree_changes = tree_changes(&repo, Some(source_commit_parent_id.detach()), source_id)?;
64-
tree_changes
65-
.into_iter()
66-
.filter(|tc| tc.path == path)
67-
.map(Into::into)
68-
.collect::<Vec<DiffSpec>>()
69-
};
46+
let relevant_changes = changes_for_path_in_commit(ctx, path, source_id)?;
7047

7148
but_api::commit::uncommit::commit_uncommit_changes_only(
7249
ctx,
@@ -86,6 +63,66 @@ pub fn uncommit_file(
8663
Ok(())
8764
}
8865

66+
pub fn uncommit_file_and_discard(
67+
ctx: &mut Context,
68+
path: &BStr,
69+
source_id: gix::ObjectId,
70+
out: &mut OutputChannel,
71+
emit_output: bool,
72+
) -> Result<()> {
73+
let relevant_changes = changes_for_path_in_commit(ctx, path, source_id)?;
74+
75+
let context_lines = ctx.settings.context_lines;
76+
but_api::commit::uncommit::commit_uncommit_changes(
77+
ctx,
78+
source_id,
79+
relevant_changes.clone(),
80+
None,
81+
)?;
82+
83+
let repo = ctx.repo.get()?;
84+
let dropped = but_workspace::discard_workspace_changes(&repo, relevant_changes, context_lines)?;
85+
86+
update_workspace_commit(ctx, false)?;
87+
88+
if emit_output {
89+
if let Some(out) = out.for_human() {
90+
if !dropped.is_empty() {
91+
writeln!(
92+
out,
93+
"Warning: Some changes could not be discarded (possibly already discarded or modified):"
94+
)?;
95+
for spec in &dropped {
96+
writeln!(out, " {}", spec.path.as_bstr())?;
97+
}
98+
}
99+
writeln!(out, "Discarded committed changes")?;
100+
} else if let Some(out) = out.for_json() {
101+
out.write_value(serde_json::json!({"ok": true}))?;
102+
}
103+
}
104+
105+
Ok(())
106+
}
107+
108+
fn changes_for_path_in_commit(
109+
ctx: &Context,
110+
path: &BStr,
111+
source_id: gix::ObjectId,
112+
) -> Result<Vec<DiffSpec>> {
113+
let repo = ctx.repo.get()?;
114+
115+
let source_commit = repo.find_commit(source_id)?;
116+
let source_commit_parent_id = source_commit.parent_ids().next().context("First parent")?;
117+
118+
let tree_changes = tree_changes(&repo, Some(source_commit_parent_id.detach()), source_id)?;
119+
Ok(tree_changes
120+
.into_iter()
121+
.filter(|tc| tc.path == path)
122+
.map(Into::into)
123+
.collect())
124+
}
125+
89126
/// Determine which stack contains the target branch, if any.
90127
fn find_stack_id_for_branch(
91128
ctx: &Context,

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
CliId, IdMap,
1919
command::commit::r#move::move_commit_to_branch,
2020
id::parser::{parse_sources_with_disambiguation, prompt_for_disambiguation},
21-
utils::OutputChannel,
21+
utils::{OutputChannel, shorten_object_id},
2222
};
2323

2424
/// A description of a set of hunks.
@@ -850,6 +850,7 @@ pub(crate) fn handle_uncommit(
850850
ctx: &mut Context,
851851
out: &mut OutputChannel,
852852
source_str: &str,
853+
discard: bool,
853854
) -> anyhow::Result<()> {
854855
let id_map = IdMap::legacy_new_from_context(ctx, None)?;
855856
let sources = parse_sources_with_disambiguation(ctx, &id_map, source_str, out)?;
@@ -870,6 +871,47 @@ pub(crate) fn handle_uncommit(
870871
}
871872
}
872873

874+
if discard {
875+
let json_mode = out.for_json().is_some();
876+
877+
for source in sources {
878+
match source {
879+
CliId::Commit { commit_id, .. } => {
880+
but_api::commit::discard_commit::commit_discard(ctx, commit_id)?;
881+
882+
if !json_mode && let Some(out) = out.for_human() {
883+
let repo = ctx.repo.get()?;
884+
writeln!(
885+
out,
886+
"Discarded {}",
887+
shorten_object_id(&repo, commit_id).blue()
888+
)?;
889+
}
890+
}
891+
CliId::CommittedFile {
892+
path, commit_id, ..
893+
} => {
894+
crate::command::commit::file::uncommit_file_and_discard(
895+
ctx,
896+
path.as_ref(),
897+
commit_id,
898+
out,
899+
!json_mode,
900+
)?;
901+
}
902+
_ => {
903+
unreachable!("uncommit sources were validated before execution");
904+
}
905+
}
906+
}
907+
908+
if json_mode && let Some(out) = out.for_json() {
909+
out.write_value(serde_json::json!({"ok": true}))?;
910+
}
911+
912+
return Ok(());
913+
}
914+
873915
// Call the main rub handler with "zz" as target
874916
handle(ctx, out, source_str, "zz")
875917
}

crates/but/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,7 +1260,7 @@ async fn match_subcommand(
12601260
.show_root_cause_error_then_exit_without_destructors(output)
12611261
}
12621262
#[cfg(feature = "legacy")]
1263-
Subcommands::Uncommit { source } => {
1263+
Subcommands::Uncommit { source, discard } => {
12641264
let status_after = args.status_after;
12651265
let mut ctx = setup::init_ctx(
12661266
&args,
@@ -1271,7 +1271,7 @@ async fn match_subcommand(
12711271
out,
12721272
)?;
12731273
out.begin_status_after(status_after);
1274-
let result = command::legacy::rub::handle_uncommit(&mut ctx, out, &source)
1274+
let result = command::legacy::rub::handle_uncommit(&mut ctx, out, &source, discard)
12751275
.context("Failed to uncommit.")
12761276
.emit_metrics(metrics_ctx);
12771277
maybe_run_status_after(status_after, &result, &mut ctx, out).await;

0 commit comments

Comments
 (0)