Skip to content

Commit c25c586

Browse files
committed
but: add split_commit tool and plumb workspace integration
Implements split_commit tool entry point, types, serde (SplitCommit, CommitShard), and command logic for orchestrating splits and wiring Rust workspace core API. This also updates workspace module exports, error types, and results to expose Vec<gix::ObjectId> and new workspace split outcome types.
1 parent 494d061 commit c25c586

File tree

3 files changed

+219
-25
lines changed

3 files changed

+219
-25
lines changed

crates/but-action/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ pub fn freestyle(
9090
- `absorb`: Take a set of file changes and amend them into the existing commits in the project.
9191
This requires you to figure out where the changes should go based on the locks, assingments and any other user provided information.
9292
- `split a commit`: Take an existing commit and split it into multiple commits based on the the user directive.
93-
This is a multi-step operation where you will need to create one or more black commits, and the move the file changes from the original commit to the new commits.
93+
This can be achieved by using the `split_commit` tool.
9494
- `split a branch`: Take an existing branch and split it into two branches. This basically takes a set of committed file changes and moves them to a new branch, removing them from the original branch.
9595
This is useful when you want to separate the changes into a new branch for further work.
9696
In order to do this, you will need to get the branch changes for the intended source branch (call the `get_branch_changes` tool), and then call the split branch tool with the changes you want to split off.

crates/but-tools/src/tool.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,8 @@ impl ToolResult for Result<gix::ObjectId, anyhow::Error> {
144144
result_to_json(self, action_identifier, "gix::ObjectId")
145145
}
146146
}
147+
impl ToolResult for Result<Vec<gix::ObjectId>, anyhow::Error> {
148+
fn to_json(&self, action_identifier: &str) -> serde_json::Value {
149+
result_to_json(self, action_identifier, "Vec<gix::ObjectId>")
150+
}
151+
}

crates/but-tools/src/workspace.rs

Lines changed: 213 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use anyhow::Context;
66
use bstr::BString;
77
use but_core::{TreeChange, UnifiedDiff};
88
use but_graph::VirtualBranchesTomlMetadata;
9-
use but_workspace::StackId;
109
use but_workspace::ui::StackEntry;
10+
use but_workspace::{CommmitSplitOutcome, StackId};
1111
use gitbutler_branch_actions::{BranchManagerExt, update_workspace_commit};
1212
use gitbutler_command_context::CommandContext;
1313
use gitbutler_oplog::entry::{OperationKind, SnapshotDetails};
@@ -34,11 +34,11 @@ pub fn workspace_toolset<'a>(
3434
toolset.register_tool(Amend);
3535
toolset.register_tool(SquashCommits);
3636
toolset.register_tool(GetProjectStatus);
37-
toolset.register_tool(CreateBlankCommit);
3837
toolset.register_tool(MoveFileChanges);
3938
toolset.register_tool(GetCommitDetails);
4039
toolset.register_tool(GetBranchChanges);
4140
toolset.register_tool(SplitBranch);
41+
toolset.register_tool(SplitCommit);
4242

4343
Ok(toolset)
4444
}
@@ -276,24 +276,6 @@ pub fn create_commit(
276276
Ok(outcome)
277277
}
278278

279-
fn stacks(
280-
ctx: &CommandContext,
281-
repo: &gix::Repository,
282-
) -> anyhow::Result<Vec<but_workspace::ui::StackEntry>> {
283-
let project = ctx.project();
284-
if ctx.app_settings().feature_flags.ws3 {
285-
let meta = ref_metadata_toml(ctx.project())?;
286-
but_workspace::stacks_v3(repo, &meta, but_workspace::StacksFilter::InWorkspace)
287-
} else {
288-
but_workspace::stacks(
289-
ctx,
290-
&project.gb_dir(),
291-
repo,
292-
but_workspace::StacksFilter::InWorkspace,
293-
)
294-
}
295-
}
296-
297279
pub struct CreateBranch;
298280

299281
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, JsonSchema)]
@@ -722,10 +704,7 @@ impl Tool for CreateBlankCommit {
722704
</description>
723705
724706
<important_notes>
725-
Use this tool when you want to split a commit into more parts.
726-
That you you can:
727-
1. Create one or more blank commits on top of an existing commit.
728-
2. Move the file changes from the existing commit to the new commit.
707+
Use this tool when you want to create a new commit without any file changes and only want to prepare a branch structure.
729708
</important_notes>
730709
"
731710
.to_string()
@@ -1422,6 +1401,198 @@ pub fn split_branch(
14221401
Ok(stack_id)
14231402
}
14241403

1404+
pub struct SplitCommit;
1405+
1406+
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, JsonSchema)]
1407+
#[serde(rename_all = "camelCase")]
1408+
pub struct SplitCommitParameters {
1409+
/// The stack id containing the commit to split.
1410+
#[schemars(description = "
1411+
<description>
1412+
The stack id containing the commit to split.
1413+
</description>
1414+
1415+
<important_notes>
1416+
The stack id should refer to a stack in the workspace that contains the source commit.
1417+
</important_notes>
1418+
")]
1419+
pub source_stack_id: String,
1420+
/// The commit id to split.
1421+
#[schemars(description = "
1422+
<description>
1423+
The commit id of the commit to split.
1424+
</description>
1425+
1426+
<important_notes>
1427+
The commit id should refer to a commit in the workspace.
1428+
This is the commit whose changes will be split into multiple new commits.
1429+
The commit id should be contained in the stack specified by `source_stack_id`.
1430+
</important_notes>
1431+
")]
1432+
pub source_commit_id: String,
1433+
1434+
/// The definitions for each new commit shard.
1435+
#[schemars(description = "
1436+
<description>
1437+
The definitions for each new commit shard.
1438+
Each shard specifies the commit message and the list of files to include in that shard.
1439+
</description>
1440+
1441+
<important_notes>
1442+
Each shard must have a unique set of files (no overlap).
1443+
All files in the source commit must be assigned to a shard.
1444+
The order of the shards determines the order of the resulting commits (first being the newest or 'child-most' commit and las being the oldest or 'parent-most').
1445+
</important_notes>
1446+
")]
1447+
pub shards: Vec<CommitShard>,
1448+
}
1449+
1450+
impl Tool for SplitCommit {
1451+
fn name(&self) -> String {
1452+
"split_commit".to_string()
1453+
}
1454+
1455+
fn description(&self) -> String {
1456+
"
1457+
<description>
1458+
Split a single commit into multiple new commits, each with its own message and file set.
1459+
</description>
1460+
1461+
<important_notes>
1462+
This tool allows you to break up a commit into several smaller commits, each defined by a shard.
1463+
Each shard must have a unique set of files, and all files in the source commit must be assigned to a shard.
1464+
The order of the shards determines the order of the resulting commits.
1465+
</important_notes>
1466+
".to_string()
1467+
}
1468+
1469+
fn parameters(&self) -> serde_json::Value {
1470+
let schema = schema_for!(SplitCommitParameters);
1471+
serde_json::to_value(&schema).unwrap_or_default()
1472+
}
1473+
1474+
fn call(
1475+
self: Arc<Self>,
1476+
parameters: serde_json::Value,
1477+
ctx: &mut CommandContext,
1478+
app_handle: Option<&tauri::AppHandle>,
1479+
commit_mapping: &mut HashMap<gix::ObjectId, gix::ObjectId>,
1480+
) -> anyhow::Result<serde_json::Value> {
1481+
let params = serde_json::from_value::<SplitCommitParameters>(parameters)
1482+
.map_err(|e| anyhow::anyhow!("Failed to parse input parameters: {}", e))?;
1483+
1484+
let value = split_commit(ctx, params, app_handle, commit_mapping).to_json("split_commit");
1485+
Ok(value)
1486+
}
1487+
}
1488+
pub fn split_commit(
1489+
ctx: &mut CommandContext,
1490+
params: SplitCommitParameters,
1491+
app_handle: Option<&tauri::AppHandle>,
1492+
commit_mapping: &mut HashMap<gix::ObjectId, gix::ObjectId>,
1493+
) -> Result<Vec<gix::ObjectId>, anyhow::Error> {
1494+
let source_stack_id = StackId::from_str(&params.source_stack_id)?;
1495+
let source_commit_id = gix::ObjectId::from_str(&params.source_commit_id)
1496+
.map(|id| find_the_right_commit_id(id, commit_mapping))?;
1497+
1498+
let pieces = params
1499+
.shards
1500+
.into_iter()
1501+
.map(Into::into)
1502+
.collect::<Vec<but_workspace::CommitFiles>>();
1503+
1504+
let outcome = but_workspace::split_commit(
1505+
ctx,
1506+
source_stack_id,
1507+
source_commit_id,
1508+
&pieces,
1509+
ctx.app_settings().context_lines,
1510+
)?;
1511+
1512+
let CommmitSplitOutcome {
1513+
new_commits,
1514+
move_changes_result,
1515+
} = outcome;
1516+
1517+
// Emit an stack update for the frontend.
1518+
if let Some(app_handle) = app_handle {
1519+
let project_id = ctx.project().id;
1520+
app_handle.emit_stack_update(project_id, source_stack_id);
1521+
}
1522+
1523+
// Update the commit mapping with the new commit ids.
1524+
for (old_commit_id, new_commit_id) in move_changes_result.replaced_commits.iter() {
1525+
commit_mapping.insert(*old_commit_id, *new_commit_id);
1526+
}
1527+
1528+
Ok(new_commits)
1529+
}
1530+
1531+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
1532+
#[serde(rename_all = "camelCase")]
1533+
pub struct CommitShard {
1534+
/// The commit title.
1535+
#[schemars(description = "
1536+
<description>
1537+
The commit message title.
1538+
This is only a short summary of the commit.
1539+
</description>
1540+
1541+
<important_notes>
1542+
The commit message title should be concise and descriptive.
1543+
It is typically a single line that summarizes the changes made in the commit.
1544+
For example: 'Fix issue with user login' or 'Update README with installation instructions'.
1545+
Don't excede 50 characters in length.
1546+
</important_notes>
1547+
")]
1548+
pub message_title: String,
1549+
/// The commit description.
1550+
#[schemars(description = "
1551+
<description>
1552+
The commit message body.
1553+
This is a more detailed description of the changes made in the commit.
1554+
</description>
1555+
1556+
<important_notes>
1557+
The commit message body should provide context and details about the changes made.
1558+
It should span multiple lines if necessary.
1559+
A good description focuses on describing the 'what' of the changes.
1560+
Don't make assumption about the 'why', only describe the changes in the context of the branch (and other commits if any).
1561+
</important_notes>
1562+
")]
1563+
pub message_body: String,
1564+
/// The list of file paths to be included in the commit.
1565+
///
1566+
/// Each entry is a string representing the relative path to a file.
1567+
#[schemars(description = "
1568+
<description>
1569+
The list of file paths to be included in the commit.
1570+
Each entry is a string representing the relative path to a file.
1571+
</description>
1572+
1573+
<important_notes>
1574+
The file paths should be files that exist in the the source commit.
1575+
The file paths are unique to this commit shard, there can't be duplicates.
1576+
</important_notes>
1577+
")]
1578+
pub files: Vec<String>,
1579+
}
1580+
1581+
impl From<CommitShard> for but_workspace::CommitFiles {
1582+
fn from(value: CommitShard) -> Self {
1583+
let message = format!(
1584+
"{}\n\n{}",
1585+
value.message_title.trim(),
1586+
value.message_body.trim()
1587+
);
1588+
1589+
but_workspace::CommitFiles {
1590+
message,
1591+
files: value.files,
1592+
}
1593+
}
1594+
}
1595+
14251596
fn ref_metadata_toml(project: &Project) -> anyhow::Result<VirtualBranchesTomlMetadata> {
14261597
VirtualBranchesTomlMetadata::from_path(project.gb_dir().join("virtual_branches.toml"))
14271598
}
@@ -1832,3 +2003,21 @@ fn find_the_right_commit_id(
18322003
}
18332004
commit_id
18342005
}
2006+
2007+
fn stacks(
2008+
ctx: &CommandContext,
2009+
repo: &gix::Repository,
2010+
) -> anyhow::Result<Vec<but_workspace::ui::StackEntry>> {
2011+
let project = ctx.project();
2012+
if ctx.app_settings().feature_flags.ws3 {
2013+
let meta = ref_metadata_toml(ctx.project())?;
2014+
but_workspace::stacks_v3(repo, &meta, but_workspace::StacksFilter::InWorkspace)
2015+
} else {
2016+
but_workspace::stacks(
2017+
ctx,
2018+
&project.gb_dir(),
2019+
repo,
2020+
but_workspace::StacksFilter::InWorkspace,
2021+
)
2022+
}
2023+
}

0 commit comments

Comments
 (0)