diff --git a/docs/users/configuration/qwen-ignore.md b/docs/users/configuration/qwen-ignore.md index 25087657..052eadf0 100644 --- a/docs/users/configuration/qwen-ignore.md +++ b/docs/users/configuration/qwen-ignore.md @@ -20,9 +20,9 @@ You can update your `.qwenignore` file at any time. To apply the changes, you mu ## How to use `.qwenignore` -| Step | Description | -| ---------------------- | ------------------------------------------------------------ | -| **Enable .qwenignore** | Create a file named `.qwenignore` in your project root directory | +| Step | Description | +| ---------------------- | -------------------------------------------------------------------------------------- | +| **Enable .qwenignore** | Create a file named `.qwenignore` in your project root directory | | **Add ignore rules** | Open `.qwenignore` file and add paths to ignore, example: `/archive/` or `apikeys.txt` | ### `.qwenignore` examples diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index a62ef2fe..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 diff --git a/docs/users/configuration/themes.md b/docs/users/configuration/themes.md index d17498ea..7175d17c 100644 --- a/docs/users/configuration/themes.md +++ b/docs/users/configuration/themes.md @@ -140,8 +140,6 @@ The theme file must be a valid JSON file that follows the same structure as a cu ### Example Custom Theme - -  ### Using Your Custom Theme @@ -150,15 +148,13 @@ The theme file must be a valid JSON file that follows the same structure as a cu - Or, set it as the default by adding `"theme": "MyCustomTheme"` to the `ui` object in your `settings.json`. - Custom themes can be set at the user, project, or system level, and follow the same [configuration precedence](./configuration.md) as other settings. - - ## Themes Preview -| Dark Theme | Preview | Light Theme | Preview | -| :-: | :-: | :-: | :-: | -| ANSI | | ANSI Light | | -| Atom OneDark | | Ayu Light |  | -| Ayu |  | Default Light |  | -| Default | | GitHub Light |  | -| Dracula | | Google Code |  | -| GitHub |  | Xcode |  | +| Dark Theme | Preview | Light Theme | Preview | +| :----------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| ANSI | | ANSI Light | | +| Atom OneDark | | Ayu Light |  | +| Ayu |  | Default Light |  | +| Default | | GitHub Light |  | +| Dracula | | Google Code |  | +| GitHub |  | Xcode |  | diff --git a/docs/users/support/troubleshooting.md b/docs/users/support/troubleshooting.md index f5129300..5f9452d4 100644 --- a/docs/users/support/troubleshooting.md +++ b/docs/users/support/troubleshooting.md @@ -84,12 +84,12 @@ This guide provides solutions to common issues and debugging tips, including top The Qwen Code uses specific exit codes to indicate the reason for termination. This is especially useful for scripting and automation. -| Exit Code | Error Type | Description | -| --------- | -------------------------- | ------------------------------------------------------------ | -| 41 | `FatalAuthenticationError` | An error occurred during the authentication process. | -| 42 | `FatalInputError` | Invalid or missing input was provided to the CLI. (non-interactive mode only) | -| 44 | `FatalSandboxError` | An error occurred with the sandboxing environment (e.g. Docker, Podman, or Seatbelt). | -| 52 | `FatalConfigError` | A configuration file (`settings.json`) is invalid or contains errors. | +| Exit Code | Error Type | Description | +| --------- | -------------------------- | --------------------------------------------------------------------------------------------------- | +| 41 | `FatalAuthenticationError` | An error occurred during the authentication process. | +| 42 | `FatalInputError` | Invalid or missing input was provided to the CLI. (non-interactive mode only) | +| 44 | `FatalSandboxError` | An error occurred with the sandboxing environment (e.g. Docker, Podman, or Seatbelt). | +| 52 | `FatalConfigError` | A configuration file (`settings.json`) is invalid or contains errors. | | 53 | `FatalTurnLimitedError` | The maximum number of conversational turns for the session was reached. (non-interactive mode only) | ## Debugging Tips diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 99d0c0ed..07ac1967 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -1002,6 +1002,7 @@ export async function loadCliConfig( enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation, eventEmitter: appEvents, useSmartEdit: argv.useSmartEdit ?? settings.useSmartEdit, + gitCoAuthor: settings.general?.gitCoAuthor, output: { format: outputSettingsFormat, }, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index bff0cc52..5b888c29 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', 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.test.ts b/packages/core/src/tools/shell.test.ts index 043ab0c6..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 }); @@ -768,6 +798,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..5354f925 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -334,13 +334,14 @@ export class ShellToolInvocation extends BaseToolInvocation< private addCoAuthorToGitCommit(command: string): string { // Check if co-author feature is enabled const gitCoAuthorSettings = this.config.getGitCoAuthor(); + if (!gitCoAuthorSettings.enabled) { 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; } @@ -349,15 +350,27 @@ 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)/; - const match = command.match(messagePattern); + // 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 doubleMatch = command.match(doubleQuotePattern); + const singleMatch = command.match(singleQuotePattern); + const match = doubleMatch ?? singleMatch; + const quote = doubleMatch ? '"' : "'"; 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; return command.replace(fullMatch, replacement); }