Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
70eac44
add a basic but config command
schacon Sep 2, 2025
0487a5a
oplog and undo v1
schacon Aug 26, 2025
2ddc637
Rename 'revert' to 'but restore' and add undo/oplog commands
schacon Aug 26, 2025
cf0d573
Rewrite `but status` to list branches with commits and assigned files
schacon Sep 2, 2025
31a8fe9
Show base info only with -b and connect stacked branches
schacon Aug 26, 2025
6270a22
Add `but branch new` command to create virtual branches
schacon Sep 2, 2025
720913b
Improve branch resolution logic
schacon Aug 26, 2025
296b0d4
`but commit` command
schacon Aug 27, 2025
f3916d2
Add `but new <commit-id>` command to insert blank commits
schacon Aug 27, 2025
748307a
Show "(blank message)" for empty commit messages
schacon Aug 27, 2025
623f40d
Implement oplog restore functionality
schacon Aug 27, 2025
8ccc3e4
Remove unused next_help_heading attrs and derive grouped help from clap
schacon Aug 27, 2025
0fa0885
add status --files and shorthands
schacon Aug 27, 2025
1f94aff
Restrict CliId hash char range
schacon Aug 27, 2025
74b74d5
Differentiate committed files in IDs and rub operations
schacon Sep 2, 2025
c8cc499
Support 'rub uncommit' for files in commits
schacon Aug 27, 2025
bf9a013
Allow branch names and partial SHAs for rub source/id
schacon Aug 27, 2025
2d7f9eb
Improve error messages for missing stack commits
schacon Aug 27, 2025
03a3e41
Decorate commits with status letters (R/P/L)
schacon Aug 27, 2025
faae4eb
Add 'but branch unapply' command
schacon Aug 27, 2025
3fc770a
Implement basic stacked branch creation from target branch
schacon Aug 27, 2025
4cafa79
Bail with clear message when file is locked
schacon Aug 27, 2025
d0f886a
Add --only flag to commit to skip unassigned files
schacon Aug 27, 2025
55830ef
Allow specifying branch instead of --stack for commit
schacon Aug 27, 2025
a1424a4
Preserve stack when committing by using explicit parent
schacon Aug 27, 2025
ad91831
Make every `but rub` command create an oplog snapshot
schacon Aug 27, 2025
58e72f8
Limit undo to last non-restore operation
schacon Aug 27, 2025
4ca4adb
allow but rub to accept ranges or lists
schacon Aug 27, 2025
445d67f
Support multiple source IDs (ranges and lists)
schacon Aug 27, 2025
ce7fe3e
Support ranges using displayed file order
schacon Sep 2, 2025
c770101
Support JSON output for but -j oplog
schacon Aug 27, 2025
74e30e0
Support getting/setting git config values via `but config`
schacon Aug 27, 2025
7b1ba0a
Add moving files between commits
Caleb-T-Owens Aug 27, 2025
0b66ab8
Added uncommitting committed file into stack
Caleb-T-Owens Aug 27, 2025
fdf989a
BUTCLI: adds the ability to mark stack for auto assignments
krlvi Aug 27, 2025
4054211
BUTCLI: adds the ability to remove marks
krlvi Aug 27, 2025
e424406
Display marked branches in but status and log
krlvi Aug 27, 2025
48701ee
BUTCLI: add commit marking
krlvi Aug 27, 2025
49c75c7
Amending rule now uses change ID
krlvi Aug 27, 2025
8a72e9e
Show marked commits in but status too
krlvi Sep 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/but-rules/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ anyhow = "1.0.99"
itertools.workspace = true
serde.workspace = true
regex = "1.11.2"
gix = { workspace = true }
chrono = { version = "0.4.41", features = [] }
serde_regex = "1.1.0"
serde_json = "1.0.143"
Expand Down
63 changes: 62 additions & 1 deletion crates/but-rules/src/handler.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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 { change_id }) => {
let assignments = matching(assignments, rule.filters.clone());
handle_amend(ctx, assignments, change_id).unwrap_or_default();
}
_ => continue,
};
}
Expand Down Expand Up @@ -137,6 +144,60 @@ fn handle_assign(
}
}

fn handle_amend(
ctx: &mut CommandContext,
assignments: Vec<HunkAssignment>,
change_id: String,
) -> anyhow::Result<()> {
let changes: Vec<DiffSpec> = 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<gix::ObjectId> = 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,
// 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<Filter>) -> Vec<HunkAssignment> {
if filters.is_empty() {
return wt_assignments.to_vec();
Expand Down
12 changes: 10 additions & 2 deletions crates/but-rules/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ impl WorkspaceRule {
}
}

pub fn target_commit_id(&self) -> Option<String> {
if let Action::Explicit(Operation::Amend { change_id }) = &self.action {
Some(change_id.clone())
} else {
None
}
}

pub fn id(&self) -> String {
self.id.clone()
}
Expand Down Expand Up @@ -139,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 },
}
Expand Down Expand Up @@ -292,7 +300,7 @@ pub fn list_rules(ctx: &mut CommandContext) -> anyhow::Result<Vec<WorkspaceRule>
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(
Expand Down
44 changes: 32 additions & 12 deletions crates/but-workspace/src/stacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,20 +167,20 @@ pub fn stacks_v3(
applied_stacks: &[branch::Stack],
) -> anyhow::Result<Vec<ui::StackEntry>> {
let mut out = Vec::new();
// Create a set of all ref names that are in applied stacks for efficient lookup
let applied_ref_names: std::collections::HashSet<_> = applied_stacks
.iter()
.flat_map(|stack| &stack.segments)
.filter_map(|segment| segment.ref_name.as_ref())
.collect();

for item in meta.iter() {
let (ref_name, ref_meta) = item?;
if !ref_meta.is::<but_core::ref_metadata::Branch>() {
continue;
};
let is_applied = applied_stacks.iter().any(|stack| {
stack.segments.iter().any(|segment| {
segment
.ref_name
.as_ref()
.is_some_and(|name| name == &ref_name)
})
});
if is_applied {
// Check if this ref_name is in our applied_ref_names set
if applied_ref_names.contains(&ref_name) {
continue;
}

Expand Down Expand Up @@ -222,21 +222,41 @@ pub fn stacks_v3(
stacks: Vec<branch::Stack>,
meta: &VirtualBranchesTomlMetadata,
) -> Vec<ui::StackEntry> {
use std::collections::HashSet;
let mut seen_ids = HashSet::new();

stacks
.into_iter()
.filter_map(|stack| try_from_stack_v3(repo, stack, meta).ok())
.filter(|entry| {
// Deduplicate by stack ID if present
match entry.id {
Some(id) => seen_ids.insert(id),
None => true, // Always include stacks without IDs
}
})
.collect()
}

let unapplied_stacks = unapplied_stacks(repo, meta, &info.stacks)?;
Ok(match filter {
StacksFilter::InWorkspace => into_ui_stacks(repo, info.stacks, meta),
StacksFilter::All => {
let unapplied_stacks = unapplied_stacks(repo, meta, &info.stacks)?;
let mut all_stacks = unapplied_stacks;
all_stacks.extend(into_ui_stacks(repo, info.stacks, meta));
all_stacks

// Deduplicate by ID across both applied and unapplied stacks
use std::collections::HashMap;
let mut deduped: HashMap<Option<StackId>, ui::StackEntry> = HashMap::new();
for stack in all_stacks {
deduped.insert(stack.id, stack);
}
deduped.into_values().collect()
}
StacksFilter::Unapplied => {
let unapplied_stacks = unapplied_stacks(repo, meta, &info.stacks)?;
unapplied_stacks
}
StacksFilter::Unapplied => unapplied_stacks,
})
}

Expand Down
10 changes: 10 additions & 0 deletions crates/but/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ anyhow.workspace = true
rmcp.workspace = true
command-group = { version = "5.0.1", features = ["with-tokio"] }
sysinfo = "0.37.0"
regex = "1.11.1"
gitbutler-project.workspace = true
gix.workspace = true
but-core.workspace = true
Expand All @@ -42,15 +43,24 @@ 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
gitbutler-commit.workspace = true
gitbutler-branch-actions.workspace = true
gitbutler-branch.workspace = true
gitbutler-reference.workspace = true
gitbutler-secret.workspace = true
gitbutler-oxidize.workspace = true
gitbutler-repo.workspace = true
gitbutler-user.workspace = true
gitbutler-oplog.workspace = true
gitbutler-diff.workspace = true
git2.workspace = true
colored = "3.0.0"
serde_json = "1.0.143"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
tracing.workspace = true
tracing-subscriber = { version = "0.3", features = [
"env-filter",
Expand Down
Loading
Loading