From d02b40591baecb2679f563b433aaa8cffa922105 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Wed, 27 Aug 2025 13:29:20 +0200 Subject: [PATCH 1/5] BUTCLI: adds the ability to mark stack for auto assignments --- Cargo.lock | 2 ++ crates/but-rules/src/lib.rs | 2 +- crates/but/Cargo.toml | 2 ++ crates/but/src/args.rs | 5 ++++ crates/but/src/log/mod.rs | 1 + crates/but/src/main.rs | 10 ++++++++ crates/but/src/mark/mod.rs | 47 ++++++++++++++++++++++++++++++++++++ crates/but/src/rub/mod.rs | 1 + crates/but/src/status/mod.rs | 1 + 9 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 crates/but/src/mark/mod.rs diff --git a/Cargo.lock b/Cargo.lock index dfd1d5e83e..3115b1678d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -776,6 +776,7 @@ dependencies = [ "but-graph", "but-hunk-assignment", "but-hunk-dependency", + "but-rules", "but-settings", "but-tools", "but-workspace", @@ -794,6 +795,7 @@ dependencies = [ "gitbutler-stack", "gix", "posthog-rs", + "regex", "rmcp", "serde", "serde_json", diff --git a/crates/but-rules/src/lib.rs b/crates/but-rules/src/lib.rs index 1c6efa5e19..3f8a8d965f 100644 --- a/crates/but-rules/src/lib.rs +++ b/crates/but-rules/src/lib.rs @@ -292,7 +292,7 @@ pub fn list_rules(ctx: &mut CommandContext) -> anyhow::Result Ok(rules) } -fn process_rules(ctx: &mut CommandContext) -> anyhow::Result<()> { +pub fn process_rules(ctx: &mut CommandContext) -> anyhow::Result<()> { let wt_changes = but_core::diff::worktree_changes(&ctx.gix_repo()?)?; let dependencies = hunk_dependencies_for_workspace_changes_by_worktree_dir( diff --git a/crates/but/Cargo.toml b/crates/but/Cargo.toml index 4875ebee22..a6e60b2c4f 100644 --- a/crates/but/Cargo.toml +++ b/crates/but/Cargo.toml @@ -30,6 +30,7 @@ anyhow.workspace = true rmcp.workspace = true command-group = { version = "5.0.1", features = ["with-tokio"] } sysinfo = "0.36.0" +regex = "1.11.1" gitbutler-project.workspace = true gix.workspace = true but-core.workspace = true @@ -42,6 +43,7 @@ but-hunk-assignment.workspace = true but-hunk-dependency.workspace = true but-claude.workspace = true but-tools.workspace = true +but-rules.workspace = true gitbutler-command-context.workspace = true gitbutler-serde.workspace = true gitbutler-stack.workspace = true diff --git a/crates/but/src/args.rs b/crates/but/src/args.rs index 32b525390c..3255006976 100644 --- a/crates/but/src/args.rs +++ b/crates/but/src/args.rs @@ -42,6 +42,11 @@ For examples see `but rub --help`." /// The target entity to combine with the source target: String, }, + /// Creates a rule for auto-assigning or auto-comitting + Mark { + /// The target entity that will be marked + target: String, + }, /// Starts up the MCP server. Mcp { /// Starts the internal MCP server which has more granular tools. diff --git a/crates/but/src/log/mod.rs b/crates/but/src/log/mod.rs index ea0a8710d1..5f6294fd72 100644 --- a/crates/but/src/log/mod.rs +++ b/crates/but/src/log/mod.rs @@ -14,6 +14,7 @@ use crate::id::CliId; pub(crate) fn commit_graph(repo_path: &Path, _json: bool) -> anyhow::Result<()> { let project = Project::from_path(repo_path).expect("Failed to create project from path"); let ctx = &mut CommandContext::open(&project, AppSettings::load_from_default_path_creating()?)?; + but_rules::process_rules(ctx).ok(); // TODO: this is doing double work (dependencies can be reused) let stacks = stacks(ctx)? .iter() .filter_map(|s| s.id.map(|id| stack_details(ctx, id))) diff --git a/crates/but/src/main.rs b/crates/but/src/main.rs index 38b60fa381..4983e2d5b2 100644 --- a/crates/but/src/main.rs +++ b/crates/but/src/main.rs @@ -9,6 +9,7 @@ use but_claude::hooks::OutputAsJson; mod command; mod id; mod log; +mod mark; mod mcp; mod mcp_internal; mod metrics; @@ -98,6 +99,15 @@ async fn main() -> Result<()> { metrics_if_configured(app_settings, CommandName::Rub, props(start, &result)).ok(); Ok(()) } + Subcommands::Mark { target } => { + let result = mark::handle(&args.current_dir, args.json, target) + .context("Can't mark this. Taaaa-na-na-na. Can't mark this."); + if let Err(e) = &result { + eprintln!("{} {}", e, e.root_cause()); + } + metrics_if_configured(app_settings, CommandName::Rub, props(start, &result)).ok(); + Ok(()) + } } } diff --git a/crates/but/src/mark/mod.rs b/crates/but/src/mark/mod.rs new file mode 100644 index 0000000000..6d310c30cf --- /dev/null +++ b/crates/but/src/mark/mod.rs @@ -0,0 +1,47 @@ +use std::path::Path; + +use crate::rub::branch_name_to_stack_id; +use anyhow::bail; +use but_rules::Operation; +use but_settings::AppSettings; +use gitbutler_command_context::CommandContext; +use gitbutler_project::Project; +pub(crate) fn handle(repo_path: &Path, _json: bool, target_str: &str) -> anyhow::Result<()> { + let project = Project::from_path(repo_path).expect("Failed to create project from path"); + let ctx = &mut CommandContext::open(&project, AppSettings::load_from_default_path_creating()?)?; + let target_result = crate::id::CliId::from_str(ctx, target_str)?; + if target_result.len() != 1 { + return Err(anyhow::anyhow!( + "Target {} is ambiguous: {:?}", + target_str, + target_result + )); + } + match target_result[0].clone() { + crate::id::CliId::Branch { name } => mark_branch(ctx, name), + crate::id::CliId::Commit { oid } => mark_commit(oid), + _ => bail!("Nope"), + } +} + +fn mark_commit(_oid: gix::ObjectId) -> anyhow::Result<()> { + bail!("Not implemented yet"); +} + +fn mark_branch(ctx: &mut CommandContext, branch_name: String) -> anyhow::Result<()> { + let stack_id = + branch_name_to_stack_id(ctx, Some(&branch_name))?.expect("Cant find stack for this branch"); + let action = but_rules::Action::Explicit(Operation::Assign { + target: but_rules::StackTarget::StackId(stack_id.to_string()), + }); + let req = but_rules::CreateRuleRequest { + trigger: but_rules::Trigger::FileSytemChange, + filters: vec![but_rules::Filter::PathMatchesRegex(regex::Regex::new( + ".*", + )?)], + action, + }; + but_rules::create_rule(ctx, req)?; + println!("Changes will be assigned to → {}", branch_name); + Ok(()) +} diff --git a/crates/but/src/rub/mod.rs b/crates/but/src/rub/mod.rs index e0187242cf..abf21190fe 100644 --- a/crates/but/src/rub/mod.rs +++ b/crates/but/src/rub/mod.rs @@ -1,6 +1,7 @@ use std::path::Path; use anyhow::bail; +pub(crate) use assign::branch_name_to_stack_id; use but_settings::AppSettings; use colored::Colorize; use gitbutler_command_context::CommandContext; diff --git a/crates/but/src/status/mod.rs b/crates/but/src/status/mod.rs index f4388dbfb8..87c32d58fb 100644 --- a/crates/but/src/status/mod.rs +++ b/crates/but/src/status/mod.rs @@ -15,6 +15,7 @@ use crate::id::CliId; pub(crate) fn worktree(repo_path: &Path, _json: bool) -> anyhow::Result<()> { let project = Project::from_path(repo_path).expect("Failed to create project from path"); let ctx = &mut CommandContext::open(&project, AppSettings::load_from_default_path_creating()?)?; + but_rules::process_rules(ctx).ok(); // TODO: this is doing double work (dependencies can be reused) let stack_id_to_branch = crate::log::stacks(ctx)? .iter() From 0bec66eb39f9b929e639bdf379f70c8772a85c2a Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Wed, 27 Aug 2025 13:38:54 +0200 Subject: [PATCH 2/5] BUTCLI: adds the ability to remove marks --- crates/but/src/args.rs | 5 ++++- crates/but/src/main.rs | 4 ++-- crates/but/src/mark/mod.rs | 29 ++++++++++++++++++++++------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/crates/but/src/args.rs b/crates/but/src/args.rs index 3255006976..3232210dc0 100644 --- a/crates/but/src/args.rs +++ b/crates/but/src/args.rs @@ -42,10 +42,13 @@ For examples see `but rub --help`." /// The target entity to combine with the source target: String, }, - /// Creates a rule for auto-assigning or auto-comitting + /// Creates or removes a rule for auto-assigning or auto-comitting Mark { /// The target entity that will be marked target: String, + /// Deletes a mark + #[clap(long, short = 'd')] + delete: bool, }, /// Starts up the MCP server. Mcp { diff --git a/crates/but/src/main.rs b/crates/but/src/main.rs index 4983e2d5b2..99ee3ed980 100644 --- a/crates/but/src/main.rs +++ b/crates/but/src/main.rs @@ -99,8 +99,8 @@ async fn main() -> Result<()> { metrics_if_configured(app_settings, CommandName::Rub, props(start, &result)).ok(); Ok(()) } - Subcommands::Mark { target } => { - let result = mark::handle(&args.current_dir, args.json, target) + Subcommands::Mark { target, delete } => { + let result = mark::handle(&args.current_dir, args.json, target, *delete) .context("Can't mark this. Taaaa-na-na-na. Can't mark this."); if let Err(e) = &result { eprintln!("{} {}", e, e.root_cause()); diff --git a/crates/but/src/mark/mod.rs b/crates/but/src/mark/mod.rs index 6d310c30cf..edc292635c 100644 --- a/crates/but/src/mark/mod.rs +++ b/crates/but/src/mark/mod.rs @@ -6,7 +6,12 @@ use but_rules::Operation; use but_settings::AppSettings; use gitbutler_command_context::CommandContext; use gitbutler_project::Project; -pub(crate) fn handle(repo_path: &Path, _json: bool, target_str: &str) -> anyhow::Result<()> { +pub(crate) fn handle( + repo_path: &Path, + _json: bool, + target_str: &str, + delete: bool, +) -> anyhow::Result<()> { let project = Project::from_path(repo_path).expect("Failed to create project from path"); let ctx = &mut CommandContext::open(&project, AppSettings::load_from_default_path_creating()?)?; let target_result = crate::id::CliId::from_str(ctx, target_str)?; @@ -18,19 +23,29 @@ pub(crate) fn handle(repo_path: &Path, _json: bool, target_str: &str) -> anyhow: )); } match target_result[0].clone() { - crate::id::CliId::Branch { name } => mark_branch(ctx, name), - crate::id::CliId::Commit { oid } => mark_commit(oid), + crate::id::CliId::Branch { name } => mark_branch(ctx, name, delete), + crate::id::CliId::Commit { oid } => mark_commit(oid, delete), _ => bail!("Nope"), } } -fn mark_commit(_oid: gix::ObjectId) -> anyhow::Result<()> { +fn mark_commit(_oid: gix::ObjectId, _delete: bool) -> anyhow::Result<()> { bail!("Not implemented yet"); } -fn mark_branch(ctx: &mut CommandContext, branch_name: String) -> anyhow::Result<()> { - let stack_id = - branch_name_to_stack_id(ctx, Some(&branch_name))?.expect("Cant find stack for this branch"); +fn mark_branch(ctx: &mut CommandContext, branch_name: String, delete: bool) -> anyhow::Result<()> { + let stack_id = branch_name_to_stack_id(ctx, Some(&branch_name))?; + if delete { + let rules = but_rules::list_rules(ctx)?; + for rule in rules { + if rule.target_stack_id() == stack_id.map(|s| s.to_string()) { + but_rules::delete_rule(ctx, &rule.id())?; + } + } + println!("Mark was removed"); + return Ok(()); + } + let stack_id = stack_id.expect("Cant find stack for this branch"); let action = but_rules::Action::Explicit(Operation::Assign { target: but_rules::StackTarget::StackId(stack_id.to_string()), }); From 0021f34d689f5d0d1da94513e101cbfa286f701c Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Wed, 27 Aug 2025 15:11:44 +0200 Subject: [PATCH 3/5] Display marked branches in but status and log --- crates/but/src/log/mod.rs | 16 ++++++++++++---- crates/but/src/mark/mod.rs | 9 +++++++++ crates/but/src/status/mod.rs | 18 +++++++++++++++--- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/crates/but/src/log/mod.rs b/crates/but/src/log/mod.rs index 5f6294fd72..acef028931 100644 --- a/crates/but/src/log/mod.rs +++ b/crates/but/src/log/mod.rs @@ -17,12 +17,18 @@ pub(crate) fn commit_graph(repo_path: &Path, _json: bool) -> anyhow::Result<()> but_rules::process_rules(ctx).ok(); // TODO: this is doing double work (dependencies can be reused) let stacks = stacks(ctx)? .iter() - .filter_map(|s| s.id.map(|id| stack_details(ctx, id))) + .filter_map(|s| s.id.map(|id| stack_details(ctx, id).map(|d| (id, d)))) .filter_map(Result::ok) .collect::>(); let mut nesting = 0; - for (i, stack) in stacks.iter().enumerate() { + for (i, (stack_id, stack)) in stacks.iter().enumerate() { + let marked = crate::mark::stack_marked(ctx, *stack_id).unwrap_or_default(); + let mut mark = if marked { + Some("◀ Marked ▶".red().bold()) + } else { + None + }; let mut second_consecutive = false; let mut stacked = false; for branch in stack.branch_details.iter() { @@ -46,13 +52,15 @@ pub(crate) fn commit_graph(repo_path: &Path, _json: bool) -> anyhow::Result<()> .underline() .blue(); println!( - "{}{}{} [{}] {}", + "{}{}{} [{}] {} {}", "│ ".repeat(nesting), extra_space, line, branch.name.to_string().green().bold(), - id + id, + mark.clone().unwrap_or_default() ); + mark = None; // show this on the first branch in the stack for (j, commit) in branch.upstream_commits.iter().enumerate() { let time_string = chrono::DateTime::from_timestamp_millis(commit.created_at as i64) .ok_or(anyhow::anyhow!("Could not parse timestamp"))? diff --git a/crates/but/src/mark/mod.rs b/crates/but/src/mark/mod.rs index edc292635c..873d655a4c 100644 --- a/crates/but/src/mark/mod.rs +++ b/crates/but/src/mark/mod.rs @@ -4,6 +4,7 @@ use crate::rub::branch_name_to_stack_id; use anyhow::bail; use but_rules::Operation; use but_settings::AppSettings; +use but_workspace::StackId; use gitbutler_command_context::CommandContext; use gitbutler_project::Project; pub(crate) fn handle( @@ -45,6 +46,7 @@ fn mark_branch(ctx: &mut CommandContext, branch_name: String, delete: bool) -> a println!("Mark was removed"); return Ok(()); } + // TODO: if there are other marks of this kind, get rid of them let stack_id = stack_id.expect("Cant find stack for this branch"); let action = but_rules::Action::Explicit(Operation::Assign { target: but_rules::StackTarget::StackId(stack_id.to_string()), @@ -60,3 +62,10 @@ fn mark_branch(ctx: &mut CommandContext, branch_name: String, delete: bool) -> a println!("Changes will be assigned to → {}", branch_name); Ok(()) } + +pub(crate) fn stack_marked(ctx: &mut CommandContext, stack_id: StackId) -> anyhow::Result { + let rules = but_rules::list_rules(ctx)? + .iter() + .any(|r| r.target_stack_id() == Some(stack_id.to_string())); + Ok(rules) +} diff --git a/crates/but/src/status/mod.rs b/crates/but/src/status/mod.rs index 87c32d58fb..d4d1907fff 100644 --- a/crates/but/src/status/mod.rs +++ b/crates/but/src/status/mod.rs @@ -52,12 +52,13 @@ pub(crate) fn worktree(repo_path: &Path, _json: bool) -> anyhow::Result<()> { } let unassigned = assignment::filter_by_stack_id(assignments_by_file.values(), &None); - print_group(None, unassigned, &changes)?; + print_group(None, unassigned, &changes, false)?; for (stack_id, branch) in &stack_id_to_branch { let filtered = assignment::filter_by_stack_id(assignments_by_file.values(), &Some(*stack_id)); - print_group(Some(branch.as_str()), filtered, &changes)?; + let marked = crate::mark::stack_marked(ctx, *stack_id).unwrap_or_default(); + print_group(Some(branch.as_str()), filtered, &changes, marked)?; } Ok(()) } @@ -66,6 +67,7 @@ pub fn print_group( group: Option<&str>, assignments: Vec, changes: &[TreeChange], + marked: bool, ) -> anyhow::Result<()> { let id = if let Some(group) = group { CliId::branch(group) @@ -78,7 +80,17 @@ pub fn print_group( let group = &group .map(|s| format!("[{}]", s)) .unwrap_or("".to_string()); - println!("{} {}", id, group.green().bold()); + let mark = if marked { + Some("◀ Marked ▶".red().bold()) + } else { + None + }; + println!( + "{} {} {}", + id, + group.green().bold(), + mark.unwrap_or_default() + ); for fa in assignments { let state = status_from_changes(changes, fa.path.clone()); let path = match state { From 98ea02a8c42b8b0fd1265c8ffdb329ff39aa5d69 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Wed, 27 Aug 2025 17:17:44 +0200 Subject: [PATCH 4/5] BUTCLI: add commit marking --- Cargo.lock | 1 + crates/but-rules/Cargo.toml | 1 + crates/but-rules/src/handler.rs | 35 ++++++++++++++++++++++++++++- crates/but-rules/src/lib.rs | 8 +++++++ crates/but/src/log/mod.rs | 12 ++++++++-- crates/but/src/mark/mod.rs | 39 ++++++++++++++++++++++++++++++--- 6 files changed, 90 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3115b1678d..5b06da8460 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1124,6 +1124,7 @@ dependencies = [ "gitbutler-command-context", "gitbutler-project", "gitbutler-stack", + "gix", "itertools", "regex", "serde", diff --git a/crates/but-rules/Cargo.toml b/crates/but-rules/Cargo.toml index 1c2787db72..b1ac325136 100644 --- a/crates/but-rules/Cargo.toml +++ b/crates/but-rules/Cargo.toml @@ -14,6 +14,7 @@ anyhow = "1.0.98" itertools.workspace = true serde.workspace = true regex = "1.11.1" +gix = { workspace = true } chrono = { version = "0.4.41", features = [] } serde_regex = "1.1.0" serde_json = "1.0.142" diff --git a/crates/but-rules/src/handler.rs b/crates/but-rules/src/handler.rs index ac1ee06994..a0c12f41fe 100644 --- a/crates/but-rules/src/handler.rs +++ b/crates/but-rules/src/handler.rs @@ -1,7 +1,7 @@ use but_graph::VirtualBranchesTomlMetadata; use but_hunk_assignment::{HunkAssignment, assign, assignments_to_requests}; use but_hunk_dependency::ui::HunkDependencies; -use but_workspace::{StackId, StacksFilter, ui::StackEntry}; +use but_workspace::{DiffSpec, StackId, StacksFilter, commit_engine, ui::StackEntry}; use gitbutler_command_context::CommandContext; use itertools::Itertools; use std::str::FromStr; @@ -26,6 +26,9 @@ pub fn process_workspace_rules( matches!( &r.action, super::Action::Explicit(super::Operation::Assign { .. }) + ) || matches!( + &r.action, + super::Action::Explicit(super::Operation::Amend { .. }) ) }) .collect_vec(); @@ -60,6 +63,10 @@ pub fn process_workspace_rules( handle_assign(ctx, assignments, dependencies.as_ref()).unwrap_or_default(); } } + super::Action::Explicit(super::Operation::Amend { commit_id }) => { + let assignments = matching(assignments, rule.filters.clone()); + handle_amend(ctx, assignments, commit_id).unwrap_or_default(); + } _ => continue, }; } @@ -137,6 +144,32 @@ fn handle_assign( } } +fn handle_amend( + ctx: &mut CommandContext, + assignments: Vec, + commit_id: String, +) -> anyhow::Result<()> { + let changes: Vec = assignments.into_iter().map(|a| a.into()).collect(); + let project = ctx.project(); + let mut guard = project.exclusive_worktree_access(); + let repo = but_core::open_repo_for_merging(project.worktree_path())?; + commit_engine::create_commit_and_update_refs_with_project( + &repo, + project, + None, + commit_engine::Destination::AmendCommit { + commit_id: gix::ObjectId::from_str(&commit_id)?, + // TODO: Expose this in the UI for 'edit message' functionality. + new_message: None, + }, + None, + changes, + ctx.app_settings().context_lines, + guard.write_permission(), + )?; + Ok(()) +} + fn matching(wt_assignments: &[HunkAssignment], filters: Vec) -> Vec { if filters.is_empty() { return wt_assignments.to_vec(); diff --git a/crates/but-rules/src/lib.rs b/crates/but-rules/src/lib.rs index 3f8a8d965f..490d0aa94f 100644 --- a/crates/but-rules/src/lib.rs +++ b/crates/but-rules/src/lib.rs @@ -46,6 +46,14 @@ impl WorkspaceRule { } } + pub fn target_commit_id(&self) -> Option { + if let Action::Explicit(Operation::Amend { commit_id }) = &self.action { + Some(commit_id.clone()) + } else { + None + } + } + pub fn id(&self) -> String { self.id.clone() } diff --git a/crates/but/src/log/mod.rs b/crates/but/src/log/mod.rs index acef028931..a0e47c242c 100644 --- a/crates/but/src/log/mod.rs +++ b/crates/but/src/log/mod.rs @@ -92,6 +92,13 @@ pub(crate) fn commit_graph(repo_path: &Path, _json: bool) -> anyhow::Result<()> } } for commit in branch.commits.iter() { + let marked = + crate::mark::commit_marked(ctx, commit.id.to_string()).unwrap_or_default(); + let mark = if marked { + Some("◀ Marked ▶".red().bold()) + } else { + None + }; let state_str = match commit.state { but_workspace::ui::CommitState::LocalOnly => "{local}".normal(), but_workspace::ui::CommitState::LocalAndRemote(_) => "{pushed}".cyan(), @@ -107,14 +114,15 @@ pub(crate) fn commit_graph(repo_path: &Path, _json: bool) -> anyhow::Result<()> .format("%Y-%m-%d %H:%M:%S") .to_string(); println!( - "{}● {}{} {} {} {} {}", + "{}● {}{} {} {} {} {} {}", "│ ".repeat(nesting), &commit.id.to_string()[..2].blue().underline(), &commit.id.to_string()[2..7].blue(), state_str, conflicted_str, commit.author.name, - time_string.dimmed() + time_string.dimmed(), + mark.clone().unwrap_or_default() ); println!( "{}│ {}", diff --git a/crates/but/src/mark/mod.rs b/crates/but/src/mark/mod.rs index 873d655a4c..80b145ddba 100644 --- a/crates/but/src/mark/mod.rs +++ b/crates/but/src/mark/mod.rs @@ -23,15 +23,41 @@ pub(crate) fn handle( target_result )); } + // Hack - delete all other rules + for rule in but_rules::list_rules(ctx)? { + but_rules::delete_rule(ctx, &rule.id())?; + } match target_result[0].clone() { crate::id::CliId::Branch { name } => mark_branch(ctx, name, delete), - crate::id::CliId::Commit { oid } => mark_commit(oid, delete), + crate::id::CliId::Commit { oid } => mark_commit(ctx, oid, delete), _ => bail!("Nope"), } } -fn mark_commit(_oid: gix::ObjectId, _delete: bool) -> anyhow::Result<()> { - bail!("Not implemented yet"); +fn mark_commit(ctx: &mut CommandContext, oid: gix::ObjectId, delete: bool) -> anyhow::Result<()> { + if delete { + let rules = but_rules::list_rules(ctx)?; + for rule in rules { + if rule.target_commit_id() == Some(oid.to_string()) { + but_rules::delete_rule(ctx, &rule.id())?; + } + } + println!("Mark was removed"); + return Ok(()); + } + let action = but_rules::Action::Explicit(Operation::Amend { + commit_id: oid.to_string(), + }); + let req = but_rules::CreateRuleRequest { + trigger: but_rules::Trigger::FileSytemChange, + filters: vec![but_rules::Filter::PathMatchesRegex(regex::Regex::new( + ".*", + )?)], + action, + }; + but_rules::create_rule(ctx, req)?; + println!("Changes will be amended into commit → {}", &oid.to_string()); + Ok(()) } fn mark_branch(ctx: &mut CommandContext, branch_name: String, delete: bool) -> anyhow::Result<()> { @@ -69,3 +95,10 @@ pub(crate) fn stack_marked(ctx: &mut CommandContext, stack_id: StackId) -> anyho .any(|r| r.target_stack_id() == Some(stack_id.to_string())); Ok(rules) } + +pub(crate) fn commit_marked(ctx: &mut CommandContext, commit_id: String) -> anyhow::Result { + let rules = but_rules::list_rules(ctx)? + .iter() + .any(|r| r.target_commit_id() == Some(commit_id.clone())); + Ok(rules) +} From 9cf3ab360403bec36cbff25808dec7cc4bd865ff Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Wed, 27 Aug 2025 18:53:11 +0200 Subject: [PATCH 5/5] Amending rule now uses change ID --- Cargo.lock | 1 + crates/but-rules/src/handler.rs | 36 +++++++++++++++++++++++++++++---- crates/but-rules/src/lib.rs | 6 +++--- crates/but/Cargo.toml | 1 + crates/but/src/mark/mod.rs | 22 +++++++++++++++----- 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b06da8460..afc41bbadc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -788,6 +788,7 @@ dependencies = [ "gitbutler-branch", "gitbutler-branch-actions", "gitbutler-command-context", + "gitbutler-commit", "gitbutler-oxidize", "gitbutler-project", "gitbutler-secret", diff --git a/crates/but-rules/src/handler.rs b/crates/but-rules/src/handler.rs index a0c12f41fe..76400c3de2 100644 --- a/crates/but-rules/src/handler.rs +++ b/crates/but-rules/src/handler.rs @@ -63,9 +63,9 @@ pub fn process_workspace_rules( handle_assign(ctx, assignments, dependencies.as_ref()).unwrap_or_default(); } } - super::Action::Explicit(super::Operation::Amend { commit_id }) => { + super::Action::Explicit(super::Operation::Amend { change_id }) => { let assignments = matching(assignments, rule.filters.clone()); - handle_amend(ctx, assignments, commit_id).unwrap_or_default(); + handle_amend(ctx, assignments, change_id).unwrap_or_default(); } _ => continue, }; @@ -147,18 +147,46 @@ fn handle_assign( fn handle_amend( ctx: &mut CommandContext, assignments: Vec, - commit_id: String, + change_id: String, ) -> anyhow::Result<()> { let changes: Vec = assignments.into_iter().map(|a| a.into()).collect(); let project = ctx.project(); let mut guard = project.exclusive_worktree_access(); let repo = but_core::open_repo_for_merging(project.worktree_path())?; + + let meta = VirtualBranchesTomlMetadata::from_path( + ctx.project().gb_dir().join("virtual_branches.toml"), + )?; + let ref_info_options = but_workspace::ref_info::Options { + expensive_commit_info: true, + traversal: meta.graph_options(), + }; + let info = but_workspace::head_info(&repo, &meta, ref_info_options)?; + let mut commit_id: Option = None; + 'outer: for stack in info.stacks { + for segment in stack.segments { + for commit in segment.commits { + if Some(change_id.clone()) == commit.change_id.map(|c| c.to_string()) { + commit_id = Some(commit.id); + break 'outer; + } + } + } + } + + let commit_id = commit_id.ok_or_else(|| { + anyhow::anyhow!( + "No commit with Change-Id {} found in the current workspace", + change_id + ) + })?; + commit_engine::create_commit_and_update_refs_with_project( &repo, project, None, commit_engine::Destination::AmendCommit { - commit_id: gix::ObjectId::from_str(&commit_id)?, + commit_id, // TODO: Expose this in the UI for 'edit message' functionality. new_message: None, }, diff --git a/crates/but-rules/src/lib.rs b/crates/but-rules/src/lib.rs index 490d0aa94f..32e7fb40c3 100644 --- a/crates/but-rules/src/lib.rs +++ b/crates/but-rules/src/lib.rs @@ -47,8 +47,8 @@ impl WorkspaceRule { } pub fn target_commit_id(&self) -> Option { - if let Action::Explicit(Operation::Amend { commit_id }) = &self.action { - Some(commit_id.clone()) + if let Action::Explicit(Operation::Amend { change_id }) = &self.action { + Some(change_id.clone()) } else { None } @@ -147,7 +147,7 @@ pub enum Operation { /// Assign the matched changes to a specific stack ID. Assign { target: StackTarget }, /// Amend the matched changes into a specific commit. - Amend { commit_id: String }, + Amend { change_id: String }, /// Create a new commit with the matched changes on a specific branch. NewCommit { branch_name: String }, } diff --git a/crates/but/Cargo.toml b/crates/but/Cargo.toml index a6e60b2c4f..4c9d6faf5f 100644 --- a/crates/but/Cargo.toml +++ b/crates/but/Cargo.toml @@ -47,6 +47,7 @@ but-rules.workspace = true gitbutler-command-context.workspace = true gitbutler-serde.workspace = true gitbutler-stack.workspace = true +gitbutler-commit.workspace = true gitbutler-branch-actions.workspace = true gitbutler-branch.workspace = true gitbutler-secret.workspace = true diff --git a/crates/but/src/mark/mod.rs b/crates/but/src/mark/mod.rs index 80b145ddba..2b705bf847 100644 --- a/crates/but/src/mark/mod.rs +++ b/crates/but/src/mark/mod.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{path::Path, str::FromStr}; use crate::rub::branch_name_to_stack_id; use anyhow::bail; @@ -6,6 +6,7 @@ use but_rules::Operation; use but_settings::AppSettings; use but_workspace::StackId; use gitbutler_command_context::CommandContext; +use gitbutler_commit::commit_ext::CommitExt; use gitbutler_project::Project; pub(crate) fn handle( repo_path: &Path, @@ -45,9 +46,12 @@ fn mark_commit(ctx: &mut CommandContext, oid: gix::ObjectId, delete: bool) -> an println!("Mark was removed"); return Ok(()); } - let action = but_rules::Action::Explicit(Operation::Amend { - commit_id: oid.to_string(), - }); + let repo = ctx.gix_repo()?; + let commit = repo.find_commit(oid)?; + let change_id = commit.change_id().ok_or_else(|| { + anyhow::anyhow!("Commit {} does not have a Change-Id, cannot mark it", oid) + })?; + let action = but_rules::Action::Explicit(Operation::Amend { change_id }); let req = but_rules::CreateRuleRequest { trigger: but_rules::Trigger::FileSytemChange, filters: vec![but_rules::Filter::PathMatchesRegex(regex::Regex::new( @@ -97,8 +101,16 @@ pub(crate) fn stack_marked(ctx: &mut CommandContext, stack_id: StackId) -> anyho } pub(crate) fn commit_marked(ctx: &mut CommandContext, commit_id: String) -> anyhow::Result { + let repo = ctx.gix_repo()?; + let commit = repo.find_commit(gix::ObjectId::from_str(&commit_id)?)?; + let change_id = commit.change_id().ok_or_else(|| { + anyhow::anyhow!( + "Commit {} does not have a Change-Id, cannot mark it", + commit_id + ) + })?; let rules = but_rules::list_rules(ctx)? .iter() - .any(|r| r.target_commit_id() == Some(commit_id.clone())); + .any(|r| r.target_commit_id() == Some(change_id.clone())); Ok(rules) }