From 3b9d38a325be8b87968a914feb0190741544a87f Mon Sep 17 00:00:00 2001 From: Alexander Farber Date: Thu, 11 Dec 2025 21:33:03 +0100 Subject: [PATCH 1/7] Expose gitCoAuthor setting in settings.json and document it --- docs/cli/configuration.md | 19 +++++++++ packages/cli/src/config/config.ts | 1 + packages/cli/src/config/settingsSchema.ts | 52 +++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index aef6bc4f..11c6400a 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -306,6 +306,20 @@ Settings are organized into categories. All settings should be placed within the - **Default:** `1000` - **Requires restart:** Yes +#### `git` + +- **`git.gitCoAuthor.enabled`** (boolean): + - **Description:** Automatically add a Co-authored-by trailer to git commit messages when commits are made through Qwen Code. + - **Default:** `true` + +- **`git.gitCoAuthor.name`** (string): + - **Description:** The name to use in the Co-authored-by trailer. + - **Default:** `"Qwen-Coder"` + +- **`git.gitCoAuthor.email`** (string): + - **Description:** The email to use in the Co-authored-by trailer. + - **Default:** `"qwen-coder@alibabacloud.com"` + #### `mcp` - **`mcp.serverCommand`** (string): @@ -418,6 +432,11 @@ Here is an example of a `settings.json` file with the nested structure, new as o "callCommand": "bin/call_tool", "exclude": ["write_file"] }, + "git": { + "gitCoAuthor": { + "enabled": false + } + }, "mcpServers": { "mainServer": { "command": "bin/mcp_server.py" diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index ab4f087d..988bec17 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -992,6 +992,7 @@ export async function loadCliConfig( enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation, eventEmitter: appEvents, useSmartEdit: argv.useSmartEdit ?? settings.useSmartEdit, + gitCoAuthor: settings.git?.gitCoAuthor, output: { format: outputSettingsFormat, }, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 439bc5d9..097ff6d4 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -933,6 +933,58 @@ const SETTINGS_SCHEMA = { }, }, + git: { + type: 'object', + label: 'Git', + category: 'Git', + requiresRestart: false, + default: {}, + description: 'Git-related settings.', + showInDialog: false, + properties: { + gitCoAuthor: { + type: 'object', + label: 'Git Co-Author', + category: 'Git', + requiresRestart: false, + default: {}, + description: + 'Settings for automatic Co-authored-by trailer in git commits.', + showInDialog: false, + properties: { + enabled: { + type: 'boolean', + label: 'Enable Git Co-Author', + category: 'Git', + requiresRestart: false, + default: true, + description: + 'Automatically add Co-authored-by trailer to git commit messages.', + showInDialog: true, + }, + name: { + type: 'string', + label: 'Co-Author Name', + category: 'Git', + requiresRestart: false, + default: 'Qwen-Coder' as string | undefined, + description: 'The name to use in the Co-authored-by trailer.', + showInDialog: true, + }, + email: { + type: 'string', + label: 'Co-Author Email', + category: 'Git', + requiresRestart: false, + default: 'qwen-coder@alibabacloud.com' as string | undefined, + description: 'The email to use in the Co-authored-by trailer.', + showInDialog: true, + }, + }, + }, + }, + }, + mcp: { type: 'object', label: 'MCP', From 65392a057de429e2d1933c32f9d04673ca5b52d5 Mon Sep 17 00:00:00 2001 From: Alexander Farber Date: Fri, 12 Dec 2025 11:02:29 +0100 Subject: [PATCH 2/7] Detect git commit anywhere in command, not just at start --- packages/core/src/tools/shell.test.ts | 63 +++++++++++++++++++++++++++ packages/core/src/tools/shell.ts | 6 +-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index 043ab0c6..3760266a 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -768,6 +768,69 @@ describe('ShellTool', () => { {}, ); }); + + it('should add co-author when git commit is prefixed with cd command', async () => { + const command = 'cd /tmp/test && git commit -m "Test commit"'; + const invocation = shellTool.build({ command, is_background: false }); + const promise = invocation.execute(mockAbortSignal); + + resolveExecutionPromise({ + rawOutput: Buffer.from(''), + output: '', + exitCode: 0, + signal: null, + error: null, + aborted: false, + pid: 12345, + executionMethod: 'child_process', + }); + + await promise; + + expect(mockShellExecutionService).toHaveBeenCalledWith( + expect.stringContaining( + 'Co-authored-by: Qwen-Coder ', + ), + expect.any(String), + expect.any(Function), + mockAbortSignal, + false, + {}, + ); + }); + + it('should add co-author to git commit with multi-line message', async () => { + const command = `git commit -m "Fix bug + +This is a detailed description +spanning multiple lines"`; + const invocation = shellTool.build({ command, is_background: false }); + const promise = invocation.execute(mockAbortSignal); + + resolveExecutionPromise({ + rawOutput: Buffer.from(''), + output: '', + exitCode: 0, + signal: null, + error: null, + aborted: false, + pid: 12345, + executionMethod: 'child_process', + }); + + await promise; + + expect(mockShellExecutionService).toHaveBeenCalledWith( + expect.stringContaining( + 'Co-authored-by: Qwen-Coder ', + ), + expect.any(String), + expect.any(Function), + mockAbortSignal, + false, + {}, + ); + }); }); }); diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 8ff3047e..6e92954e 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -338,9 +338,9 @@ export class ShellToolInvocation extends BaseToolInvocation< return command; } - // Check if this is a git commit command - const gitCommitPattern = /^git\s+commit/; - if (!gitCommitPattern.test(command.trim())) { + // Check if this is a git commit command (anywhere in the command, e.g., after "cd /path &&") + const gitCommitPattern = /\bgit\s+commit\b/; + if (!gitCommitPattern.test(command)) { return command; } From 5bd1822b7d740da689695ab0fe22f7a8eac558b4 Mon Sep 17 00:00:00 2001 From: Alexander Farber Date: Mon, 15 Dec 2025 11:00:21 +0100 Subject: [PATCH 3/7] Fix gitCoAuthor not added for combined flags like -am --- packages/core/src/tools/shell.test.ts | 30 +++++++++++++++++++++++++++ packages/core/src/tools/shell.ts | 4 ++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index 3760266a..0b34b8c1 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -608,6 +608,36 @@ describe('ShellTool', () => { ); }); + it('should handle git commit with combined short flags like -am', async () => { + const command = 'git commit -am "Add feature"'; + const invocation = shellTool.build({ command, is_background: false }); + const promise = invocation.execute(mockAbortSignal); + + resolveExecutionPromise({ + rawOutput: Buffer.from(''), + output: '', + exitCode: 0, + signal: null, + error: null, + aborted: false, + pid: 12345, + executionMethod: 'child_process', + }); + + await promise; + + expect(mockShellExecutionService).toHaveBeenCalledWith( + expect.stringContaining( + 'Co-authored-by: Qwen-Coder ', + ), + expect.any(String), + expect.any(Function), + mockAbortSignal, + false, + {}, + ); + }); + it('should not modify non-git commands', async () => { const command = 'npm install'; const invocation = shellTool.build({ command, is_background: false }); diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 6e92954e..077bc693 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -350,8 +350,8 @@ export class ShellToolInvocation extends BaseToolInvocation< Co-authored-by: ${gitCoAuthorSettings.name} <${gitCoAuthorSettings.email}>`; // Handle different git commit patterns - // Match -m "message" or -m 'message' - const messagePattern = /(-m\s+)(['"])((?:\\.|[^\\])*?)(\2)/; + // Match -m "message" or -m 'message', including combined flags like -am + const messagePattern = /(-[a-zA-Z]*m\s+)(['"])((?:\\.|[^\\])*?)(\2)/; const match = command.match(messagePattern); if (match) { From 5d94763581360b8fe1762753c879fb5804b63e9b Mon Sep 17 00:00:00 2001 From: Alexander Farber Date: Mon, 15 Dec 2025 11:06:09 +0100 Subject: [PATCH 4/7] Add logs (TODO remove later) --- packages/core/src/tools/shell.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 077bc693..4deb02a7 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -334,13 +334,24 @@ export class ShellToolInvocation extends BaseToolInvocation< private addCoAuthorToGitCommit(command: string): string { // Check if co-author feature is enabled const gitCoAuthorSettings = this.config.getGitCoAuthor(); + + // Debug logging for gitCoAuthor feature + // TODO: Remove after debugging is complete + console.error( + '[gitCoAuthor] Settings:', + JSON.stringify(gitCoAuthorSettings), + ); + console.error('[gitCoAuthor] Command:', command); + if (!gitCoAuthorSettings.enabled) { + console.error('[gitCoAuthor] Feature disabled, skipping'); return command; } // Check if this is a git commit command (anywhere in the command, e.g., after "cd /path &&") const gitCommitPattern = /\bgit\s+commit\b/; if (!gitCommitPattern.test(command)) { + console.error('[gitCoAuthor] Not a git commit command, skipping'); return command; } @@ -354,16 +365,20 @@ Co-authored-by: ${gitCoAuthorSettings.name} <${gitCoAuthorSettings.email}>`; const messagePattern = /(-[a-zA-Z]*m\s+)(['"])((?:\\.|[^\\])*?)(\2)/; const match = command.match(messagePattern); + console.error('[gitCoAuthor] Message pattern match:', match ? 'YES' : 'NO'); + if (match) { const [fullMatch, prefix, quote, existingMessage, closingQuote] = match; const newMessage = existingMessage + coAuthor; const replacement = prefix + quote + newMessage + closingQuote; + console.error('[gitCoAuthor] Adding co-author trailer'); return command.replace(fullMatch, replacement); } // If no -m flag found, the command might open an editor // In this case, we can't easily modify it, so return as-is + console.error('[gitCoAuthor] No -m flag found, skipping'); return command; } } From 1956507d9014b7fdc558d5f9b88f48461b848869 Mon Sep 17 00:00:00 2001 From: Alexander Farber Date: Mon, 15 Dec 2025 16:23:17 +0100 Subject: [PATCH 5/7] Avoid ReDoS by using better regexes --- packages/core/src/tools/shell.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 4deb02a7..b78c6729 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -362,15 +362,19 @@ Co-authored-by: ${gitCoAuthorSettings.name} <${gitCoAuthorSettings.email}>`; // Handle different git commit patterns // Match -m "message" or -m 'message', including combined flags like -am - const messagePattern = /(-[a-zA-Z]*m\s+)(['"])((?:\\.|[^\\])*?)(\2)/; - const match = command.match(messagePattern); + // Use separate patterns to avoid ReDoS (catastrophic backtracking) + const doubleQuotePattern = /(-[a-zA-Z]*m\s+)"((?:[^"\\]|\\.)*)"/; + const singleQuotePattern = /(-[a-zA-Z]*m\s+)'((?:[^'\\]|\\.)*)'/; + const match = + command.match(doubleQuotePattern) || command.match(singleQuotePattern); + const quote = command.match(doubleQuotePattern) ? '"' : "'"; console.error('[gitCoAuthor] Message pattern match:', match ? 'YES' : 'NO'); if (match) { - const [fullMatch, prefix, quote, existingMessage, closingQuote] = match; + const [fullMatch, prefix, existingMessage] = match; const newMessage = existingMessage + coAuthor; - const replacement = prefix + quote + newMessage + closingQuote; + const replacement = prefix + quote + newMessage + quote; console.error('[gitCoAuthor] Adding co-author trailer'); return command.replace(fullMatch, replacement); From 07fb6faf5fb00002815aa7430b29b8f5cd23ef29 Mon Sep 17 00:00:00 2001 From: Alexander Farber Date: Mon, 15 Dec 2025 16:26:52 +0100 Subject: [PATCH 6/7] Add comments explaining regexes --- packages/core/src/tools/shell.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index b78c6729..1a32e6b1 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -360,9 +360,16 @@ export class ShellToolInvocation extends BaseToolInvocation< Co-authored-by: ${gitCoAuthorSettings.name} <${gitCoAuthorSettings.email}>`; - // Handle different git commit patterns + // Handle different git commit patterns: // Match -m "message" or -m 'message', including combined flags like -am // Use separate patterns to avoid ReDoS (catastrophic backtracking) + // + // Pattern breakdown: + // -[a-zA-Z]*m matches -m, -am, -nm, etc. (combined short flags) + // \s+ matches whitespace after the flag + // [^"\\] matches any char except double-quote and backslash + // \\. matches escape sequences like \" or \\ + // (?:...|...)* matches normal chars or escapes, repeated const doubleQuotePattern = /(-[a-zA-Z]*m\s+)"((?:[^"\\]|\\.)*)"/; const singleQuotePattern = /(-[a-zA-Z]*m\s+)'((?:[^'\\]|\\.)*)'/; const match = From 61e378644e1b5c835c6d93c75cb0bfe792928ea9 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Tue, 16 Dec 2025 15:02:59 +0800 Subject: [PATCH 7/7] feat: update configuration and shell tool implementations Co-authored-by: Qwen-Coder --- docs/users/configuration/settings.md | 23 +++------ packages/cli/src/config/config.ts | 2 +- packages/cli/src/config/settingsSchema.ts | 62 ++++------------------- packages/core/src/config/config.ts | 8 +-- packages/core/src/tools/shell.ts | 21 ++------ 5 files changed, 27 insertions(+), 89 deletions(-) diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index de710d27..9fecc6d3 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -50,13 +50,14 @@ Settings are organized into categories. All settings should be placed within the #### general -| Setting | Type | Description | Default | -| ------------------------------- | ------- | ------------------------------------------ | ----------- | -| `general.preferredEditor` | string | The preferred editor to open files in. | `undefined` | -| `general.vimMode` | boolean | Enable Vim keybindings. | `false` | -| `general.disableAutoUpdate` | boolean | Disable automatic updates. | `false` | -| `general.disableUpdateNag` | boolean | Disable update notification prompts. | `false` | -| `general.checkpointing.enabled` | boolean | Enable session checkpointing for recovery. | `false` | +| Setting | Type | Description | Default | +| ------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- | ----------- | +| `general.preferredEditor` | string | The preferred editor to open files in. | `undefined` | +| `general.vimMode` | boolean | Enable Vim keybindings. | `false` | +| `general.disableAutoUpdate` | boolean | Disable automatic updates. | `false` | +| `general.disableUpdateNag` | boolean | Disable update notification prompts. | `false` | +| `general.gitCoAuthor` | boolean | Automatically add a Co-authored-by trailer to git commit messages when commits are made through Qwen Code. | `true` | +| `general.checkpointing.enabled` | boolean | Enable session checkpointing for recovery. | `false` | #### output @@ -175,14 +176,6 @@ If you are experiencing performance issues with file searching (e.g., with `@` c | `tools.truncateToolOutputLines` | number | Maximum lines or entries kept when truncating tool output. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `1000` | Requires restart: Yes | | `tools.autoAccept` | boolean | Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe. | `false` | | -#### git - -| Setting | Type | Description | Default | -| ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------- | -| `git.gitCoAuthor.enabled` | boolean | Automatically add a Co-authored-by trailer to git commit messages when commits are made through Qwen Code. | `true` | -| `git.gitCoAuthor.name` | string | The name to use in the Co-authored-by trailer. | `"Qwen-Coder"` | -| `git.gitCoAuthor.email` | string | The email to use in the Co-authored-by trailer. | `"qwen-coder@alibabacloud.com"` | - #### mcp | Setting | Type | Description | Default | diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 16915558..07ac1967 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -1002,7 +1002,7 @@ export async function loadCliConfig( enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation, eventEmitter: appEvents, useSmartEdit: argv.useSmartEdit ?? settings.useSmartEdit, - gitCoAuthor: settings.git?.gitCoAuthor, + gitCoAuthor: settings.general?.gitCoAuthor, output: { format: outputSettingsFormat, }, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index e3a792b3..d653d85b 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -147,6 +147,16 @@ const SETTINGS_SCHEMA = { description: 'Disable update notification prompts.', showInDialog: false, }, + gitCoAuthor: { + type: 'boolean', + label: 'Git Co-Author', + category: 'General', + requiresRestart: false, + default: true, + description: + 'Automatically add a Co-authored-by trailer to git commit messages when commits are made through Qwen Code.', + showInDialog: false, + }, checkpointing: { type: 'object', label: 'Checkpointing', @@ -943,58 +953,6 @@ const SETTINGS_SCHEMA = { }, }, - git: { - type: 'object', - label: 'Git', - category: 'Git', - requiresRestart: false, - default: {}, - description: 'Git-related settings.', - showInDialog: false, - properties: { - gitCoAuthor: { - type: 'object', - label: 'Git Co-Author', - category: 'Git', - requiresRestart: false, - default: {}, - description: - 'Settings for automatic Co-authored-by trailer in git commits.', - showInDialog: false, - properties: { - enabled: { - type: 'boolean', - label: 'Enable Git Co-Author', - category: 'Git', - requiresRestart: false, - default: true, - description: - 'Automatically add Co-authored-by trailer to git commit messages.', - showInDialog: true, - }, - name: { - type: 'string', - label: 'Co-Author Name', - category: 'Git', - requiresRestart: false, - default: 'Qwen-Coder' as string | undefined, - description: 'The name to use in the Co-authored-by trailer.', - showInDialog: true, - }, - email: { - type: 'string', - label: 'Co-Author Email', - category: 'Git', - requiresRestart: false, - default: 'qwen-coder@alibabacloud.com' as string | undefined, - description: 'The email to use in the Co-authored-by trailer.', - showInDialog: true, - }, - }, - }, - }, - }, - mcp: { type: 'object', label: 'MCP', diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index d3c9b14a..d5b7f4be 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -287,7 +287,7 @@ export interface ConfigParameters { contextFileName?: string | string[]; accessibility?: AccessibilitySettings; telemetry?: TelemetrySettings; - gitCoAuthor?: GitCoAuthorSettings; + gitCoAuthor?: boolean; usageStatisticsEnabled?: boolean; fileFiltering?: { respectGitIgnore?: boolean; @@ -534,9 +534,9 @@ export class Config { useCollector: params.telemetry?.useCollector, }; this.gitCoAuthor = { - enabled: params.gitCoAuthor?.enabled ?? true, - name: params.gitCoAuthor?.name ?? 'Qwen-Coder', - email: params.gitCoAuthor?.email ?? 'qwen-coder@alibabacloud.com', + enabled: params.gitCoAuthor ?? true, + name: 'Qwen-Coder', + email: 'qwen-coder@alibabacloud.com', }; this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true; diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 1a32e6b1..5354f925 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -335,23 +335,13 @@ export class ShellToolInvocation extends BaseToolInvocation< // Check if co-author feature is enabled const gitCoAuthorSettings = this.config.getGitCoAuthor(); - // Debug logging for gitCoAuthor feature - // TODO: Remove after debugging is complete - console.error( - '[gitCoAuthor] Settings:', - JSON.stringify(gitCoAuthorSettings), - ); - console.error('[gitCoAuthor] Command:', command); - if (!gitCoAuthorSettings.enabled) { - console.error('[gitCoAuthor] Feature disabled, skipping'); return command; } // Check if this is a git commit command (anywhere in the command, e.g., after "cd /path &&") const gitCommitPattern = /\bgit\s+commit\b/; if (!gitCommitPattern.test(command)) { - console.error('[gitCoAuthor] Not a git commit command, skipping'); return command; } @@ -372,24 +362,21 @@ Co-authored-by: ${gitCoAuthorSettings.name} <${gitCoAuthorSettings.email}>`; // (?:...|...)* matches normal chars or escapes, repeated const doubleQuotePattern = /(-[a-zA-Z]*m\s+)"((?:[^"\\]|\\.)*)"/; const singleQuotePattern = /(-[a-zA-Z]*m\s+)'((?:[^'\\]|\\.)*)'/; - const match = - command.match(doubleQuotePattern) || command.match(singleQuotePattern); - const quote = command.match(doubleQuotePattern) ? '"' : "'"; - - console.error('[gitCoAuthor] Message pattern match:', match ? 'YES' : 'NO'); + const doubleMatch = command.match(doubleQuotePattern); + const singleMatch = command.match(singleQuotePattern); + const match = doubleMatch ?? singleMatch; + const quote = doubleMatch ? '"' : "'"; if (match) { const [fullMatch, prefix, existingMessage] = match; const newMessage = existingMessage + coAuthor; const replacement = prefix + quote + newMessage + quote; - console.error('[gitCoAuthor] Adding co-author trailer'); return command.replace(fullMatch, replacement); } // If no -m flag found, the command might open an editor // In this case, we can't easily modify it, so return as-is - console.error('[gitCoAuthor] No -m flag found, skipping'); return command; } }