mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Include project rules in commit message generation (#44921)
Closes #38027 Release Notes: - AI-generated commit messages now respect rules files (e.g. `AGENTS.md`) if present --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b17b097204
commit
c7d248329b
5 changed files with 90 additions and 20 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -7077,6 +7077,7 @@ dependencies = [
|
|||
"picker",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.9.2",
|
||||
"recent_projects",
|
||||
"remote",
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ use gpui::{
|
|||
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{
|
||||
ProjectContext, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext,
|
||||
ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
|
||||
WorktreeContext,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{LanguageModelSelection, update_settings_file};
|
||||
|
|
@ -51,18 +52,6 @@ pub struct ProjectSnapshot {
|
|||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
const RULES_FILE_NAMES: [&str; 9] = [
|
||||
".rules",
|
||||
".cursorrules",
|
||||
".windsurfrules",
|
||||
".clinerules",
|
||||
".github/copilot-instructions.md",
|
||||
"CLAUDE.md",
|
||||
"AGENT.md",
|
||||
"AGENTS.md",
|
||||
"GEMINI.md",
|
||||
];
|
||||
|
||||
pub struct RulesLoadingError {
|
||||
pub message: SharedString,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ notifications.workspace = true
|
|||
panel.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
recent_projects.workspace = true
|
||||
remote.workspace = true
|
||||
schemars.workspace = true
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ use project::{
|
|||
git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},
|
||||
project_settings::{GitPathStyle, ProjectSettings},
|
||||
};
|
||||
use prompt_store::RULES_FILE_NAMES;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore, StatusStyle};
|
||||
use std::future::Future;
|
||||
|
|
@ -71,7 +72,7 @@ use ui::{
|
|||
prelude::*,
|
||||
};
|
||||
use util::paths::PathStyle;
|
||||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use util::{ResultExt, TryFutureExt, maybe, rel_path::RelPath};
|
||||
use workspace::SERIALIZATION_THROTTLE_TIME;
|
||||
use workspace::{
|
||||
Workspace,
|
||||
|
|
@ -2325,6 +2326,56 @@ impl GitPanel {
|
|||
compressed
|
||||
}
|
||||
|
||||
async fn load_project_rules(
|
||||
project: &Entity<Project>,
|
||||
repo_work_dir: &Arc<Path>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<String> {
|
||||
let rules_path = cx
|
||||
.update(|cx| {
|
||||
for worktree in project.read(cx).worktrees(cx) {
|
||||
let worktree_abs_path = worktree.read(cx).abs_path();
|
||||
if !worktree_abs_path.starts_with(&repo_work_dir) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let worktree_snapshot = worktree.read(cx).snapshot();
|
||||
for rules_name in RULES_FILE_NAMES {
|
||||
if let Ok(rel_path) = RelPath::unix(rules_name) {
|
||||
if let Some(entry) = worktree_snapshot.entry_for_path(rel_path) {
|
||||
if entry.is_file() {
|
||||
return Some(ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: entry.path.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.ok()??;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(rules_path, cx))
|
||||
.ok()?
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let content = buffer
|
||||
.read_with(cx, |buffer, _| buffer.text())
|
||||
.ok()?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if content.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(content)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a commit message using an LLM.
|
||||
pub fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
|
||||
if !self.can_commit() || !AgentSettings::get_global(cx).enabled(cx) {
|
||||
|
|
@ -2352,8 +2403,10 @@ impl GitPanel {
|
|||
});
|
||||
|
||||
let temperature = AgentSettings::temperature_for_model(&model, cx);
|
||||
let project = self.project.clone();
|
||||
let repo_work_dir = repo.read(cx).work_directory_abs_path.clone();
|
||||
|
||||
self.generate_commit_message_task = Some(cx.spawn(async move |this, cx| {
|
||||
self.generate_commit_message_task = Some(cx.spawn(async move |this, mut cx| {
|
||||
async move {
|
||||
let _defer = cx.on_drop(&this, |this, _cx| {
|
||||
this.generate_commit_message_task.take();
|
||||
|
|
@ -2386,19 +2439,33 @@ impl GitPanel {
|
|||
const MAX_DIFF_BYTES: usize = 20_000;
|
||||
diff_text = Self::compress_commit_diff(&diff_text, MAX_DIFF_BYTES);
|
||||
|
||||
let rules_content = Self::load_project_rules(&project, &repo_work_dir, &mut cx).await;
|
||||
|
||||
let subject = this.update(cx, |this, cx| {
|
||||
this.commit_editor.read(cx).text(cx).lines().next().map(ToOwned::to_owned).unwrap_or_default()
|
||||
})?;
|
||||
|
||||
let text_empty = subject.trim().is_empty();
|
||||
|
||||
let content = if text_empty {
|
||||
format!("{PROMPT}\nHere are the changes in this commit:\n{diff_text}")
|
||||
} else {
|
||||
format!("{PROMPT}\nHere is the user's subject line:\n{subject}\nHere are the changes in this commit:\n{diff_text}\n")
|
||||
const PROMPT: &str = include_str!("commit_message_prompt.txt");
|
||||
|
||||
let rules_section = match &rules_content {
|
||||
Some(rules) => format!(
|
||||
"\n\nThe user has provided the following project rules that you should follow when writing the commit message:\n\
|
||||
<project_rules>\n{rules}\n</project_rules>\n"
|
||||
),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
const PROMPT: &str = include_str!("commit_message_prompt.txt");
|
||||
let subject_section = if text_empty {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\nHere is the user's subject line:\n{subject}")
|
||||
};
|
||||
|
||||
let content = format!(
|
||||
"{PROMPT}{rules_section}{subject_section}\nHere are the changes in this commit:\n{diff_text}"
|
||||
);
|
||||
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,18 @@ use util::{
|
|||
|
||||
use crate::UserPromptId;
|
||||
|
||||
pub const RULES_FILE_NAMES: &[&str] = &[
|
||||
".rules",
|
||||
".cursorrules",
|
||||
".windsurfrules",
|
||||
".clinerules",
|
||||
".github/copilot-instructions.md",
|
||||
"CLAUDE.md",
|
||||
"AGENT.md",
|
||||
"AGENTS.md",
|
||||
"GEMINI.md",
|
||||
];
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize)]
|
||||
pub struct ProjectContext {
|
||||
pub worktrees: Vec<WorktreeContext>,
|
||||
|
|
|
|||
Loading…
Reference in a new issue