Skip to content

Commit 91c26d9

Browse files
authored
Merge pull request #9430 from gitbutlerapp/enhance-branch-name-generation
enhance-branch-name-generation
2 parents 3bd138b + c185520 commit 91c26d9

File tree

4 files changed

+81
-20
lines changed

4 files changed

+81
-20
lines changed

crates/but-action/src/generate.rs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ pub struct StructuredOutput {
9696
pub async fn branch_name(
9797
client: &Client<OpenAIConfig>,
9898
commit_messages: &[String],
99+
existing_branch_names: &[String],
99100
) -> anyhow::Result<String> {
100101
let system_message =
101102
"You are a version control assistant that helps with Git branch naming.".to_string();
@@ -105,31 +106,72 @@ pub async fn branch_name(
105106
Don't use other special characters or spaces.
106107
The branche name should reflect the main content of the commit messages.
107108
109+
Try to make the branch name unique and noticeably different from existing branch names.
110+
111+
Here are the existing branch names:
112+
{}
113+
108114
Here are the commit messages:
109115
110116
{}",
117+
existing_branch_names.join(",\n"),
111118
commit_messages.join("\n==================\n")
112119
);
113120

121+
let schema = schema_for!(GenerateBranchNameOutput);
122+
let schema_json = serde_json::to_value(schema)?;
123+
let response_format = ResponseFormat::JsonSchema {
124+
json_schema: ResponseFormatJsonSchema {
125+
description: None,
126+
name: "branch_name".into(),
127+
schema: Some(schema_json),
128+
strict: Some(false),
129+
},
130+
};
131+
114132
let request = CreateChatCompletionRequestArgs::default()
115133
.model("gpt-4o")
116134
.messages([
117135
ChatCompletionRequestSystemMessage::from(system_message).into(),
118136
ChatCompletionRequestUserMessage::from(user_message).into(),
119137
])
138+
.response_format(response_format)
120139
.build()?;
121140

122141
let response = client.chat().create(request).await?;
123-
let response_string = response
142+
let choice = response
124143
.choices
125144
.first()
126-
.unwrap()
145+
.ok_or_else(|| anyhow::anyhow!("No choices returned from OpenAI response"))?;
146+
147+
let response_string = choice
127148
.message
128149
.content
129150
.as_ref()
130-
.unwrap();
151+
.ok_or_else(|| anyhow::anyhow!("No content in OpenAI response message"))?;
152+
153+
let structured_output: GenerateBranchNameOutput = serde_json::from_str(response_string)
154+
.map_err(|e| anyhow::anyhow!("Failed to parse response: {}", e))?;
131155

132-
Ok(response_string.trim().to_string())
156+
Ok(structured_output.branch_name)
157+
}
158+
159+
#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
160+
#[serde(rename_all = "camelCase")]
161+
pub struct GenerateBranchNameOutput {
162+
#[schemars(description = "
163+
<description>
164+
The generated branch name based on the commit messages.
165+
</description>
166+
167+
<important_notes>
168+
The branch name should be concise, descriptive, and follow the naming conventions.
169+
It should not contain spaces or special characters other than hyphens.
170+
Return the branch name only, no backticks or quotes.
171+
It should be noticeably different from existing branch names.
172+
</important_notes>
173+
")]
174+
pub branch_name: String,
133175
}
134176

135177
const DEFAULT_COMMIT_MESSAGE_INSTRUCTIONS: &str = r#"The message should be a short summary line, followed by two newlines, then a short paragraph explaining WHY the change was needed based off the prompt.

crates/but-action/src/rename_branch.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,30 @@ use gix::bstr::BString;
55

66
use crate::workflow::{self, Workflow};
77

8+
pub struct RenameBranchParams {
9+
pub commit_id: gix::ObjectId,
10+
pub commit_message: BString,
11+
pub stack_id: StackId,
12+
pub current_branch_name: String,
13+
pub existing_branch_names: Vec<String>,
14+
}
15+
816
pub async fn rename_branch(
917
ctx: &mut CommandContext,
1018
client: &Client<OpenAIConfig>,
11-
commit_id: gix::ObjectId,
12-
commit_message: BString,
13-
stack_id: StackId,
14-
current_branch_name: String,
19+
parameters: RenameBranchParams,
1520
trigger_id: uuid::Uuid,
1621
) -> anyhow::Result<()> {
22+
let RenameBranchParams {
23+
commit_id,
24+
commit_message,
25+
stack_id,
26+
current_branch_name,
27+
existing_branch_names,
28+
} = parameters;
1729
let commit_messages = vec![commit_message.to_string()];
18-
let branch_name = crate::generate::branch_name(client, &commit_messages).await?;
30+
let branch_name =
31+
crate::generate::branch_name(client, &commit_messages, &existing_branch_names).await?;
1932
let normalized_branch_name = gitbutler_reference::normalize_branch_name(&branch_name)?;
2033

2134
let update = gitbutler_branch_actions::stack::update_branch_name(

crates/but-tools/src/workspace.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,8 @@ impl Tool for SquashCommits {
11771177
<important_notes>
11781178
This tool allows you to squash a sequence of commits in a stack into a single commit with a new message.
11791179
Use this tool to clean up commit history before merging or sharing.
1180+
Always squash the commits down, meanding newer commits into their parents.
1181+
Remember that the commits listed in the project status are in reverse order, so the first commit in the list is the newest one.
11801182
</important_notes>
11811183
".to_string()
11821184
}

crates/but/src/command/claude.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::io::{self, Read};
22
use std::str::FromStr;
33

44
use anyhow::anyhow;
5+
use but_action::rename_branch::RenameBranchParams;
56
use but_action::{ActionHandler, OpenAiProvider, Source, reword::CommitEvent};
67
use but_db::ClaudeCodeSession;
78
use but_hunk_assignment::HunkAssignmentRequest;
@@ -111,6 +112,10 @@ pub(crate) async fn handle_stop() -> anyhow::Result<ClaudeHookOutput> {
111112
)?;
112113

113114
let stacks = crate::log::stacks(defer.ctx)?;
115+
let existing_branch_names = stacks
116+
.iter()
117+
.flat_map(|s| s.heads.iter().map(|h| h.name.clone().to_string()))
118+
.collect::<Vec<_>>();
114119

115120
// Trigger commit message generation for newly created commits
116121
// TODO: Maybe this can be done in the main app process i.e. the GitButler GUI, if avaialbe
@@ -140,17 +145,16 @@ pub(crate) async fn handle_stop() -> anyhow::Result<ClaudeHookOutput> {
140145

141146
match elegibility {
142147
RenameEligibility::Eligible(commit) => {
143-
but_action::rename_branch::rename_branch(
144-
defer.ctx,
145-
&openai_client,
146-
commit.id,
147-
commit.message,
148-
branch.stack_id,
149-
branch.branch_name.clone(),
150-
id,
151-
)
152-
.await
153-
.ok();
148+
let params = RenameBranchParams {
149+
commit_id: commit.id,
150+
commit_message: commit.message,
151+
stack_id: branch.stack_id,
152+
current_branch_name: branch.branch_name.clone(),
153+
existing_branch_names: existing_branch_names.clone(),
154+
};
155+
but_action::rename_branch::rename_branch(defer.ctx, &openai_client, params, id)
156+
.await
157+
.ok();
154158
}
155159
RenameEligibility::NotEligible => {
156160
// Do nothing, branch is not eligible for renaming

0 commit comments

Comments
 (0)