Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
2184a9c
add a basic but config command
schacon Aug 25, 2025
ada46d1
oplog and undo v1
schacon Aug 26, 2025
bb5d2f6
Rename 'revert' to 'but restore' and add undo/oplog commands
schacon Aug 26, 2025
7ee34e2
Rewrite `but status` to list branches with commits and assigned files
schacon Aug 26, 2025
54b0366
Show base info only with -b and connect stacked branches
schacon Aug 26, 2025
2c351f3
Add `but branch new` command to create virtual branches
schacon Aug 26, 2025
e681d2b
Don't show applied stacks or duplicate stacks in status
schacon Aug 26, 2025
cd1c386
better
schacon Aug 26, 2025
43208e8
`but commit` command
schacon Aug 27, 2025
189a8a2
Updated commit message for blank commit
schacon Aug 27, 2025
c3baa86
cargo fmt
schacon Aug 27, 2025
05a845a
Add `but new <commit-id>` command to insert blank commits
schacon Aug 27, 2025
029daac
Show "(blank message)" for empty commit messages
schacon Aug 27, 2025
2f6cdc1
mmore
schacon Aug 27, 2025
0f9b258
Remove unused next_help_heading attrs and derive grouped help from clap
schacon Aug 27, 2025
22c23c1
Group unspecified Clap subcommands into MISC
schacon Aug 27, 2025
19f04e8
Add 'st' alias for 'but status'
schacon Aug 27, 2025
8cb608c
status -f: show modified files per commit with shortcodes
schacon Aug 27, 2025
7f0cec2
Restrict CliId hashes to letters g–z
schacon Aug 27, 2025
0848ad7
Special-case unassigned area as "00"
schacon Aug 27, 2025
a9f7f80
Differentiate committed files in IDs and rub operations
schacon Aug 27, 2025
8db84f0
Hide StatusFiles subcommand from help
schacon Aug 27, 2025
5b63714
Support 'rub uncommit' for files in commits
schacon Aug 27, 2025
0a5454a
Change shortcode ID algorithm to expand second char range
schacon Aug 27, 2025
d26771b
Allow branch names and partial SHAs for Rub source/id
schacon Aug 27, 2025
72753aa
Improve error messages for missing stack commits
schacon Aug 27, 2025
f9c08dd
Decorate commits with status letters (R/P/L)
schacon Aug 27, 2025
fa9125b
Add 'but branch unapply' command
schacon Aug 27, 2025
914ee9d
Implement basic stacked branch creation from target branch
schacon Aug 27, 2025
6bcab43
Stack new branch on target branch head
schacon Aug 27, 2025
a7ed778
Bail with clear message when file is locked
schacon Aug 27, 2025
012b518
Add --only flag to commit to skip unassigned files
schacon Aug 27, 2025
93f51d4
Allow specifying branch instead of --stack for commit
schacon Aug 27, 2025
70ba293
Preserve stack when committing by using explicit parent
schacon Aug 27, 2025
89a66c6
Make every `but rub` command create an oplog snapshot
schacon Aug 27, 2025
fcd7b05
Limit undo to last non-restore operation
schacon Aug 27, 2025
5565248
Undo: restore oplog head as undo target
schacon Aug 27, 2025
05c56a8
Restore second-most recent snapshot instead of latest
schacon Aug 27, 2025
3777111
allow but rub to accept ranges or lists. so if you have status:
schacon Aug 27, 2025
d98db13
Support multiple source IDs (ranges and lists)
schacon Aug 27, 2025
3ada88c
Support ranges using displayed file order
schacon Aug 27, 2025
e6fe222
Support JSON output for but -j oplog
schacon Aug 27, 2025
9ef601c
Support getting/setting git config values via `but config`
schacon Aug 27, 2025
dc61ac2
Add moving files between commits
Caleb-T-Owens Aug 27, 2025
edf6f35
Added uncommitting committed file into stack
Caleb-T-Owens Aug 27, 2025
7256e7e
BUTCLI: adds the ability to mark stack for auto assignments
krlvi Aug 27, 2025
0af3316
BUTCLI: adds the ability to remove marks
krlvi Aug 27, 2025
fe1fc41
Display marked branches in but status and log
krlvi Aug 27, 2025
0889f8c
BUTCLI: add commit marking
krlvi Aug 27, 2025
a0010ec
Amending rule now uses change ID
krlvi Aug 27, 2025
5e5e5bf
Fix a thing
krlvi Aug 27, 2025
a69fb63
Show marked commits in but status too
krlvi Aug 27, 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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,4 @@ Want to show your support? Add a GitButler badge to your project's README:
```

[![BADGE][s6]][l6]
Modified line for testing
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.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"
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.36.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.142"
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