Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions crates/forge_api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub trait API: Sync + Send {
preview: bool,
max_diff_size: Option<usize>,
diff: Option<String>,
additional_context: Option<String>,
) -> Result<forge_app::CommitResult>;

/// Returns the current environment
Expand Down
5 changes: 4 additions & 1 deletion crates/forge_api/src/forge_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,12 @@ impl<A: Services, F: CommandInfra + EnvironmentInfra> API for ForgeAPI<A, F> {
preview: bool,
max_diff_size: Option<usize>,
diff: Option<String>,
additional_context: Option<String>,
) -> Result<forge_app::CommitResult> {
let git_app = GitApp::new(self.services.clone());
let result = git_app.commit_message(max_diff_size, diff).await?;
let result = git_app
.commit_message(max_diff_size, diff, additional_context)
.await?;

if preview {
Ok(result)
Expand Down
51 changes: 24 additions & 27 deletions crates/forge_app/src/git_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::sync::Arc;

use anyhow::{Context, Result};
use forge_domain::*;
use forge_template::Element;

use crate::{
AgentProviderResolver, AgentRegistry, AppConfigService, EnvironmentService,
Expand Down Expand Up @@ -47,10 +48,8 @@ struct DiffContext {
diff_content: String,
branch_name: String,
recent_commits: String,
was_truncated: bool,
max_diff_size: Option<usize>,
original_size: usize,
has_staged_files: bool,
additional_context: Option<String>,
}

impl<S> GitApp<S> {
Expand Down Expand Up @@ -99,6 +98,8 @@ where
/// unlimited.
/// * `diff` - Optional diff content provided via pipe. If provided, this
/// diff is used instead of fetching from git.
/// * `additional_context` - Optional additional text to help structure the
/// commit message
///
/// # Errors
///
Expand All @@ -107,9 +108,11 @@ where
&self,
max_diff_size: Option<usize>,
diff: Option<String>,
additional_context: Option<String>,
) -> Result<CommitResult> {
let CommitMessageDetails { message, has_staged_files } =
self.generate_commit_message(max_diff_size, diff).await?;
let CommitMessageDetails { message, has_staged_files } = self
.generate_commit_message(max_diff_size, diff, additional_context)
.await?;

Ok(CommitResult { message, committed: false, has_staged_files })
}
Expand Down Expand Up @@ -149,6 +152,7 @@ where
&self,
max_diff_size: Option<usize>,
diff: Option<String>,
additional_context: Option<String>,
) -> Result<CommitMessageDetails> {
// Get current working directory
let cwd = self.services.get_environment().cwd;
Expand All @@ -167,17 +171,14 @@ where
};

// Truncate diff if it exceeds max size
let (truncated_diff, was_truncated) =
self.truncate_diff(diff_content, max_diff_size, original_size);
let (truncated_diff, _) = self.truncate_diff(diff_content, max_diff_size, original_size);

self.generate_message_from_diff(DiffContext {
diff_content: truncated_diff,
branch_name,
recent_commits,
was_truncated,
max_diff_size,
original_size,
has_staged_files,
additional_context,
})
.await
}
Expand Down Expand Up @@ -250,27 +251,23 @@ where
agent_provider_resolver.get_model(agent_id)
)?;

// Create an context
let truncation_notice = if ctx.was_truncated {
format!(
"\n\n[Note: Diff truncated to {} bytes. Original size: {} bytes]",
ctx.max_diff_size.unwrap(),
ctx.original_size
)
} else {
String::new()
};
// Build git diff content with optional truncation notice
// Build user message using Element
let mut user_message = Element::new("user_message")
.append(Element::new("branch_name").text(&ctx.branch_name))
.append(Element::new("recent_commit_messages").text(&ctx.recent_commits))
.append(Element::new("git_diff").cdata(&ctx.diff_content));

// Add additional context if provided
if let Some(additional_context) = &ctx.additional_context {
user_message =
user_message.append(Element::new("additional_context").text(additional_context));
}

let context = forge_domain::Context::default()
.add_message(ContextMessage::system(rendered_prompt))
.add_message(ContextMessage::user(
format!(
"<branch_name>\n{}\n</branch_name>\n\n<recent_commit_messages>\n{}\n</recent_commit_messages>\n\n<git_diff>\n{}{}\n</git_diff>",
ctx.branch_name,
ctx.recent_commits,
ctx.diff_content,
truncation_notice
),
user_message.to_string(),
Some(model.clone()),
));

Expand Down
60 changes: 60 additions & 0 deletions crates/forge_main/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,13 @@ pub struct CommitCommandGroup {
/// diff content: `git diff | forge commit --preview`
#[arg(skip)]
pub diff: Option<String>,

/// Additional text to customize the commit message
///
/// Provide additional context or instructions for the AI to use when
/// generating the commit message. Multiple words can be provided without
/// quotes: `forge commit fix typo in readme`
pub text: Vec<String>,
}

#[cfg(test)]
Expand Down Expand Up @@ -981,4 +988,57 @@ mod tests {
let expected = true;
assert_eq!(actual, expected);
}

#[test]
fn test_commit_with_custom_text() {
let fixture = Cli::parse_from(["forge", "commit", "fix", "typo", "in", "readme"]);
let actual = match fixture.subcommands {
Some(TopLevelCommand::Commit(commit)) => commit.text,
_ => panic!("Expected Commit command"),
};
let expected = ["fix", "typo", "in", "readme"]
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>();
assert_eq!(actual, expected);
}

#[test]
fn test_commit_without_custom_text() {
let fixture = Cli::parse_from(["forge", "commit", "--preview"]);
let actual = match fixture.subcommands {
Some(TopLevelCommand::Commit(commit)) => commit.text,
_ => panic!("Expected Commit command"),
};
let expected: Vec<String> = vec![];
assert_eq!(actual, expected);
}

#[test]
fn test_commit_with_text_and_flags() {
let fixture = Cli::parse_from([
"forge",
"commit",
"--preview",
"--max-diff",
"50000",
"update",
"docs",
]);
let actual = match fixture.subcommands {
Some(TopLevelCommand::Commit(commit)) => {
(commit.preview, commit.max_diff_size, commit.text)
}
_ => panic!("Expected Commit command"),
};
let expected = (
true,
Some(50000),
["update", "docs"]
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>(),
);
assert_eq!(actual, expected);
}
}
11 changes: 10 additions & 1 deletion crates/forge_main/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,14 @@ impl<A: API + 'static, F: Fn() -> A> UI<A, F> {
&mut self,
commit_group: CommitCommandGroup,
) -> anyhow::Result<CommitResult> {
self.spinner.start(Some("Generating commit message"))?;
self.spinner.start(Some("Creating commit"))?;

// Convert Vec<String> to Option<String> by joining with spaces
let additional_context = if commit_group.text.is_empty() {
None
} else {
Some(commit_group.text.join(" "))
};

// Handle the commit command
let result = self
Expand All @@ -740,6 +747,7 @@ impl<A: API + 'static, F: Fn() -> A> UI<A, F> {
commit_group.preview,
commit_group.max_diff_size,
commit_group.diff,
additional_context,
)
.await;

Expand Down Expand Up @@ -1395,6 +1403,7 @@ impl<A: API + 'static, F: Fn() -> A> UI<A, F> {
preview: true,
max_diff_size: max_diff_size.or(Some(100_000)),
diff: None,
text: Vec::new(),
};
let result = self.handle_commit_command(args).await?;
let flags = if result.has_staged_files { "" } else { " -a" };
Expand Down
14 changes: 11 additions & 3 deletions shell-plugin/forge.plugin.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# Using typeset to keep variables local to plugin scope and prevent public exposure
typeset -h _FORGE_BIN="${FORGE_BIN:-forge}"
typeset -h _FORGE_CONVERSATION_PATTERN=":"
typeset -h _FORGE_MAX_COMMIT_DIFF="${FORGE_MAX_COMMIT_DIFF:-5000}"
typeset -h _FORGE_MAX_COMMIT_DIFF="${FORGE_MAX_COMMIT_DIFF:-100000}"
typeset -h _FORGE_DELIMITER='\s\s+'

# Detect fd command - Ubuntu/Debian use 'fdfind', others use 'fd'
Expand Down Expand Up @@ -437,14 +437,22 @@ function _forge_action_model() {
}

# Action handler: Commit changes with AI-generated message
# Usage: :commit [additional context]
function _forge_action_commit() {
local additional_context="$1"
local commit_message
# Generate AI commit message
echo
# Force color output even when not connected to TTY
# FORCE_COLOR: for indicatif spinner colors
# CLICOLOR_FORCE: for colored crate text colors
commit_message=$(FORCE_COLOR=true CLICOLOR_FORCE=1 $_FORGE_BIN commit --preview --max-diff "$_FORGE_MAX_COMMIT_DIFF")

# Build commit command with optional additional context
if [[ -n "$additional_context" ]]; then
commit_message=$(FORCE_COLOR=true CLICOLOR_FORCE=1 $_FORGE_BIN commit --preview --max-diff "$_FORGE_MAX_COMMIT_DIFF" $additional_context)
else
commit_message=$(FORCE_COLOR=true CLICOLOR_FORCE=1 $_FORGE_BIN commit --preview --max-diff "$_FORGE_MAX_COMMIT_DIFF")
fi

# Proceed only if command succeeded
if [[ -n "$commit_message" ]]; then
Expand Down Expand Up @@ -689,7 +697,7 @@ function forge-accept-line() {
_forge_action_tools
;;
commit)
_forge_action_commit
_forge_action_commit "$input_text"
;;
suggest|s)
_forge_action_suggest "$input_text"
Expand Down
9 changes: 5 additions & 4 deletions templates/forge-commit-message-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ Structure: `type(scope): description`
1. **Single line only** - never use multiple lines or bullet points
2. **Focus on what changed** - describe the primary change, not implementation details
3. **Be specific** - mention the affected component/module when relevant
4. **Preserve issue/PR references** - if recent commits use `(#1234)` format, include the relevant issue number at the end
5. **Match project style** - analyze recent_commit_messages for patterns (scope usage, verbosity, issue references)
4. **Exclude issue/PR references** - never include issue or PR numbers like `(#1234)` in the commit message
5. **Match project style** - analyze recent_commit_messages for patterns (scope usage, verbosity), but ignore any issue/PR references
6. **Imperative mood** - use "add" not "adds" or "added"
7. **Conciseness** - shorter is better; avoid redundant words like "improve", "update", "enhance" unless necessary

# Input Analysis Priority
1. **git_diff** - primary source for understanding the actual changes
2. **recent_commit_messages** - reference for project's commit message style and conventions
3. **branch_name** - additional context hint (feature/, fix/, etc.)
2. **additional_context** - user-provided context to help structure the commit message (if provided, use this information to guide the commit message structure and focus)
3. **recent_commit_messages** - reference for project's commit message style and conventions
4. **branch_name** - additional context hint (feature/, fix/, etc.)

# Examples
Good:
Expand Down
Loading