Detailed changes
@@ -747,7 +747,8 @@
"context": "GitCommit > Editor",
"bindings": {
"enter": "editor::Newline",
- "ctrl-enter": "git::Commit"
+ "ctrl-enter": "git::Commit",
+ "alt-l": "git::GenerateCommitMessage"
}
},
{
@@ -769,7 +770,8 @@
"tab": "git_panel::FocusChanges",
"shift-tab": "git_panel::FocusChanges",
"ctrl-enter": "git::Commit",
- "alt-up": "git_panel::FocusChanges"
+ "alt-up": "git_panel::FocusChanges",
+ "alt-l": "git::GenerateCommitMessage"
}
},
{
@@ -786,7 +786,8 @@
"tab": "git_panel::FocusChanges",
"shift-tab": "git_panel::FocusChanges",
"alt-up": "git_panel::FocusChanges",
- "shift-escape": "git::ExpandCommitEditor"
+ "shift-escape": "git::ExpandCommitEditor",
+ "alt-tab": "git::GenerateCommitMessage"
}
},
{
@@ -794,7 +795,8 @@
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
- "cmd-enter": "git::Commit"
+ "cmd-enter": "git::Commit",
+ "alt-tab": "git::GenerateCommitMessage"
}
},
{
@@ -402,6 +402,7 @@ impl Server {
.add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
.add_request_handler(forward_mutating_project_request::<proto::SetIndexText>)
.add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
+ .add_request_handler(forward_mutating_project_request::<proto::GitDiff>)
.add_request_handler(forward_mutating_project_request::<proto::GitCreateBranch>)
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
@@ -49,7 +49,8 @@ actions!(
Pull,
Fetch,
Commit,
- ExpandCommitEditor
+ ExpandCommitEditor,
+ GenerateCommitMessage
]
);
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
@@ -217,6 +217,14 @@ pub trait GitRepository: Send + Sync {
/// returns a list of remote branches that contain HEAD
fn check_for_pushed_commit(&self) -> Result<Vec<SharedString>>;
+
+ /// Run git diff
+ fn diff(&self, diff: DiffType) -> Result<String>;
+}
+
+pub enum DiffType {
+ HeadToIndex,
+ HeadToWorktree,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
@@ -577,6 +585,28 @@ impl GitRepository for RealGitRepository {
)
}
+ fn diff(&self, diff: DiffType) -> Result<String> {
+ let working_directory = self.working_directory()?;
+ let args = match diff {
+ DiffType::HeadToIndex => Some("--staged"),
+ DiffType::HeadToWorktree => None,
+ };
+
+ let output = new_std_command(&self.git_binary_path)
+ .current_dir(&working_directory)
+ .args(["diff"])
+ .args(args)
+ .output()?;
+
+ if !output.status.success() {
+ return Err(anyhow!(
+ "Failed to run git diff:\n{}",
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
+ Ok(String::from_utf8_lossy(&output.stdout).to_string())
+ }
+
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
let working_directory = self.working_directory()?;
@@ -1048,6 +1078,10 @@ impl GitRepository for FakeGitRepository {
fn check_for_pushed_commit(&self) -> Result<Vec<SharedString>> {
unimplemented!()
}
+
+ fn diff(&self, _diff: DiffType) -> Result<String> {
+ unimplemented!()
+ }
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
@@ -1,4 +1,8 @@
-You are an expert at writing Git commits. Your job is to write a clear commit message that summarizes the changes.
+You are an expert at writing Git commits. Your job is to write a short clear commit message that summarizes the changes.
+
+If you can accurately express the change in just the subject line, don't include anything in the message body. Only use the body when it is providing *useful* information.
+
+Don't repeat information from the subject line in the message body.
Only return the commit message in your response. Do not include any additional meta-commentary about the task.
@@ -10,6 +14,6 @@ Follow good Git style:
- Do not end the subject line with any punctuation
- Use the imperative mood in the subject line
- Wrap the body at 72 characters
-- Use the body to explain *what* and *why* vs. *how*
+- Keep the body short and concise (omit it entirely if not useful)
Here are the changes in this commit:
@@ -234,7 +234,7 @@ impl CommitModal {
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let git_panel = self.git_panel.clone();
- let (branch, can_commit, tooltip, commit_label, co_authors) =
+ let (branch, can_commit, tooltip, commit_label, co_authors, generate_commit_message) =
self.git_panel.update(cx, |git_panel, cx| {
let branch = git_panel
.active_repository
@@ -249,7 +249,15 @@ impl CommitModal {
let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
let title = git_panel.commit_button_title();
let co_authors = git_panel.render_co_authors(cx);
- (branch, can_commit, tooltip, title, co_authors)
+ let generate_commit_message = git_panel.render_generate_commit_message_button(cx);
+ (
+ branch,
+ can_commit,
+ tooltip,
+ title,
+ co_authors,
+ generate_commit_message,
+ )
});
let branch_picker_button = panel_button(branch)
@@ -316,7 +324,13 @@ impl CommitModal {
.w_full()
.h(px(self.properties.footer_height))
.gap_1()
- .child(h_flex().gap_1().child(branch_picker).children(co_authors))
+ .child(
+ h_flex()
+ .gap_1()
+ .child(branch_picker)
+ .children(co_authors)
+ .child(generate_commit_message),
+ )
.child(div().flex_1())
.child(
h_flex()
@@ -18,8 +18,8 @@ use editor::{
};
use futures::StreamExt as _;
use git::repository::{
- Branch, CommitDetails, CommitSummary, PushOptions, Remote, RemoteCommandOutput, ResetMode,
- Upstream, UpstreamTracking, UpstreamTrackingStatus,
+ Branch, CommitDetails, CommitSummary, DiffType, PushOptions, Remote, RemoteCommandOutput,
+ ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
};
use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
@@ -1393,6 +1393,15 @@ impl GitPanel {
Some(format!("{} {}", action_text, file_name))
}
+ fn generate_commit_message_action(
+ &mut self,
+ _: &git::GenerateCommitMessage,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.generate_commit_message(cx);
+ }
+
/// Generates a commit message using an LLM.
fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
@@ -1406,38 +1415,50 @@ impl GitPanel {
return;
}
- const PROMPT: &str = include_str!("commit_message_prompt.txt");
-
- // TODO: We need to generate a diff from the actual Git state.
- //
- // It need not look exactly like the structure below, this is just an example generated by Claude.
- let diff_text = "diff --git a/src/main.rs b/src/main.rs
-index 1234567..abcdef0 100644
---- a/src/main.rs
-+++ b/src/main.rs
-@@ -10,7 +10,7 @@ fn main() {
- println!(\"Hello, world!\");
-- let unused_var = 42;
-+ let important_value = 42;
-
- // Do something with the value
-- // TODO: Implement this later
-+ println!(\"The answer is {}\", important_value);
- }";
-
- let request = LanguageModelRequest {
- messages: vec![LanguageModelRequestMessage {
- role: Role::User,
- content: vec![format!("{PROMPT}\n{diff_text}").into()],
- cache: false,
- }],
- tools: Vec::new(),
- stop: Vec::new(),
- temperature: None,
+ let Some(repo) = self.active_repository.as_ref() else {
+ return;
};
+ let diff = repo.update(cx, |repo, cx| {
+ if self.has_staged_changes() {
+ repo.diff(DiffType::HeadToIndex, cx)
+ } else {
+ repo.diff(DiffType::HeadToWorktree, cx)
+ }
+ });
+
self.generate_commit_message_task = Some(cx.spawn(|this, mut cx| {
async move {
+ let _defer = util::defer({
+ let mut cx = cx.clone();
+ let this = this.clone();
+ move || {
+ this.update(&mut cx, |this, _cx| {
+ this.generate_commit_message_task.take();
+ })
+ .ok();
+ }
+ });
+
+ let mut diff_text = diff.await??;
+ const ONE_MB: usize = 1_000_000;
+ if diff_text.len() > ONE_MB {
+ diff_text = diff_text.chars().take(ONE_MB).collect()
+ }
+
+ const PROMPT: &str = include_str!("commit_message_prompt.txt");
+
+ let request = LanguageModelRequest {
+ messages: vec![LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec![format!("{PROMPT}\n{diff_text}").into()],
+ cache: false,
+ }],
+ tools: Vec::new(),
+ stop: Vec::new(),
+ temperature: None,
+ };
+
let stream = model.stream_completion_text(request, &cx);
let mut messages = stream.await?;
@@ -2113,6 +2134,32 @@ index 1234567..abcdef0 100644
self.has_staged_changes()
}
+ pub(crate) fn render_generate_commit_message_button(&self, cx: &Context<Self>) -> AnyElement {
+ if self.generate_commit_message_task.is_some() {
+ return Icon::new(IconName::ArrowCircle)
+ .size(IconSize::XSmall)
+ .color(Color::Info)
+ .with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+ )
+ .into_any_element();
+ }
+
+ IconButton::new("generate-commit-message", IconName::ZedAssistant)
+ .shape(ui::IconButtonShape::Square)
+ .tooltip(Tooltip::for_action_title_in(
+ "Generate commit message",
+ &git::GenerateCommitMessage,
+ &self.commit_editor.focus_handle(cx),
+ ))
+ .on_click(cx.listener(move |this, _event, _window, cx| {
+ this.generate_commit_message(cx);
+ }))
+ .into_any_element()
+ }
+
pub(crate) fn render_co_authors(&self, cx: &Context<Self>) -> Option<AnyElement> {
let potential_co_authors = self.potential_co_authors(cx);
if potential_co_authors.is_empty() {
@@ -2201,8 +2248,6 @@ index 1234567..abcdef0 100644
let panel_editor_style = panel_editor_style(true, window, cx);
let enable_coauthors = self.render_co_authors(cx);
- // Note: This is hard-coded to `false` as it is not fully implemented.
- let show_generate_commit_message_button = false;
let title = self.commit_button_title();
let editor_focus_handle = self.commit_editor.focus_handle(cx);
@@ -2253,16 +2298,8 @@ index 1234567..abcdef0 100644
.gap_0p5()
.h(footer_size)
.flex_none()
- .when(show_generate_commit_message_button, |parent| {
- parent.child(
- panel_filled_button("Generate Commit Message").on_click(
- cx.listener(move |this, _event, _window, cx| {
- this.generate_commit_message(cx);
- }),
- ),
- )
- })
.children(enable_coauthors)
+ .child(self.render_generate_commit_message_button(cx))
.child(
panel_filled_button(title)
.tooltip(move |window, cx| {
@@ -2927,6 +2964,7 @@ impl Render for GitPanel {
.on_action(cx.listener(Self::restore_tracked_files))
.on_action(cx.listener(Self::clean_all))
.on_action(cx.listener(Self::expand_commit_editor))
+ .on_action(cx.listener(Self::generate_commit_message_action))
.when(has_write_access && has_co_authors, |git_panel| {
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
})
@@ -12,6 +12,7 @@ use futures::{
channel::{mpsc, oneshot},
StreamExt as _,
};
+use git::repository::DiffType;
use git::{
repository::{
Branch, CommitDetails, GitRepository, PushOptions, Remote, RemoteCommandOutput, RepoPath,
@@ -136,6 +137,7 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_set_index_text);
client.add_entity_request_handler(Self::handle_askpass);
client.add_entity_request_handler(Self::handle_check_for_pushed_commits);
+ client.add_entity_request_handler(Self::handle_git_diff);
}
pub fn active_repository(&self) -> Option<Entity<Repository>> {
@@ -807,6 +809,33 @@ impl GitStore {
})
}
+ async fn handle_git_diff(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::GitDiff>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::GitDiffResponse> {
+ let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+ let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+ let diff_type = match envelope.payload.diff_type() {
+ proto::git_diff::DiffType::HeadToIndex => DiffType::HeadToIndex,
+ proto::git_diff::DiffType::HeadToWorktree => DiffType::HeadToWorktree,
+ };
+
+ let mut diff = repository_handle
+ .update(&mut cx, |repository_handle, cx| {
+ repository_handle.diff(diff_type, cx)
+ })?
+ .await??;
+ const ONE_MB: usize = 1_000_000;
+ if diff.len() > ONE_MB {
+ diff = diff.chars().take(ONE_MB).collect()
+ }
+
+ Ok(proto::GitDiffResponse { diff })
+ }
+
fn repository_for_request(
this: &Entity<Self>,
worktree_id: WorktreeId,
@@ -1627,6 +1656,39 @@ impl Repository {
})
}
+ pub fn diff(&self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver<Result<String>> {
+ self.send_job(|repo| async move {
+ match repo {
+ GitRepo::Local(git_repository) => git_repository.diff(diff_type),
+ GitRepo::Remote {
+ project_id,
+ client,
+ worktree_id,
+ work_directory_id,
+ ..
+ } => {
+ let response = client
+ .request(proto::GitDiff {
+ project_id: project_id.0,
+ worktree_id: worktree_id.to_proto(),
+ work_directory_id: work_directory_id.to_proto(),
+ diff_type: match diff_type {
+ DiffType::HeadToIndex => {
+ proto::git_diff::DiffType::HeadToIndex.into()
+ }
+ DiffType::HeadToWorktree => {
+ proto::git_diff::DiffType::HeadToWorktree.into()
+ }
+ },
+ })
+ .await?;
+
+ Ok(response.diff)
+ }
+ }
+ })
+ }
+
pub fn create_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
self.send_job(|repo| async move {
match repo {
@@ -342,7 +342,10 @@ message Envelope {
CheckForPushedCommitsResponse check_for_pushed_commits_response = 316;
AskPassRequest ask_pass_request = 317;
- AskPassResponse ask_pass_response = 318; // current max
+ AskPassResponse ask_pass_response = 318;
+
+ GitDiff git_diff = 319;
+ GitDiffResponse git_diff_response = 320; // current max
}
reserved 87 to 88;
@@ -2907,3 +2910,19 @@ message CheckForPushedCommits {
message CheckForPushedCommitsResponse {
repeated string pushed_to = 1;
}
+
+message GitDiff {
+ uint64 project_id = 1;
+ uint64 worktree_id = 2;
+ uint64 work_directory_id = 3;
+ DiffType diff_type = 4;
+
+ enum DiffType {
+ HEAD_TO_WORKTREE = 0;
+ HEAD_TO_INDEX = 1;
+ }
+}
+
+message GitDiffResponse {
+ string diff = 1;
+}
@@ -458,6 +458,8 @@ messages!(
(GitChangeBranch, Background),
(CheckForPushedCommits, Background),
(CheckForPushedCommitsResponse, Background),
+ (GitDiff, Background),
+ (GitDiffResponse, Background),
);
request_messages!(
@@ -604,6 +606,7 @@ request_messages!(
(GitCreateBranch, Ack),
(GitChangeBranch, Ack),
(CheckForPushedCommits, CheckForPushedCommitsResponse),
+ (GitDiff, GitDiffResponse),
);
entity_messages!(
@@ -709,6 +712,7 @@ entity_messages!(
GitChangeBranch,
GitCreateBranch,
CheckForPushedCommits,
+ GitDiff,
);
entity_messages!(