Skip to content
Open
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
22 changes: 13 additions & 9 deletions crates/forge_app/src/git_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,12 @@ impl<S: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>> GitApp<
})
}

/// Commits changes with the provided commit message
/// Commits changes with the provided commit message.
///
/// Sets the user as the author (from git config) and ForgeCode as the
/// committer. This properly attributes the commit to both the user and
/// ForgeCode using Git's author/committer distinction.
/// When `use_forge_committer` is enabled, Forge uses the Git
/// author/committer distinction to keep the user as the author while
/// setting ForgeCode as the committer. When disabled, Git uses the local
/// configured committer identity.
///
/// # Arguments
///
Expand All @@ -143,14 +144,17 @@ impl<S: Services + EnvironmentInfra<Config = forge_config::ForgeConfig>> GitApp<
pub async fn commit(&self, message: String, has_staged_files: bool) -> Result<CommitResult> {
let cwd = self.services.get_environment().cwd;
let flags = if has_staged_files { "" } else { " -a" };
let use_forge_committer = self.services.get_config()?.use_forge_committer;

// Set ForgeCode as the committer while keeping the user as the author
// by prefixing the command with environment variables
// Escape single quotes in the message by replacing ' with '\''
let escaped_message = message.replace('\'', r"'\''");
let commit_command = format!(
"GIT_COMMITTER_NAME='ForgeCode' GIT_COMMITTER_EMAIL='noreply@forgecode.dev' git commit {flags} -m '{escaped_message}'"
);
let commit_command = if use_forge_committer {
format!(
"GIT_COMMITTER_NAME='ForgeCode' GIT_COMMITTER_EMAIL='noreply@forgecode.dev' git commit {flags} -m '{escaped_message}'"
)
} else {
format!("git commit {flags} -m '{escaped_message}'")
};

let commit_result = self
.services
Expand Down
1 change: 1 addition & 0 deletions crates/forge_config/.forge.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ research_subagent = false
currency_symbol = "$"
currency_conversion_rate = 1.0
subagents = true
use_forge_committer = true

[retry]
backoff_factor = 2
Expand Down
5 changes: 5 additions & 0 deletions crates/forge_config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ pub struct ForgeConfig {
/// Model and provider configuration used for commit message generation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit: Option<ModelConfig>,
/// Whether `forge commit` should override `GIT_COMMITTER_NAME` and
/// `GIT_COMMITTER_EMAIL` with the Forge identity. Defaults to `true` via
/// the embedded `.forge.toml` defaults.
#[serde(default)]
pub use_forge_committer: bool,
/// Maximum number of recent commits included as context for commit message
/// generation.
#[serde(default)]
Expand Down
5 changes: 5 additions & 0 deletions forge.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,11 @@
}
]
},
"use_forge_committer": {
"description": "Whether `forge commit` should override `GIT_COMMITTER_NAME` and\n`GIT_COMMITTER_EMAIL` with the Forge identity. Defaults to `true` via\nthe embedded `.forge.toml` defaults.",
"type": "boolean",
"default": false
},
"verify_todos": {
"description": "Enables the pending todos hook that checks for incomplete todo items\nwhen a task ends and reminds the LLM about them.",
"type": "boolean",
Expand Down
Loading