Merge pull request #1228 from afarber/add-git-co-author

feat: expose gitCoAuthor setting in settings.json and document it
This commit is contained in:
tanzhenxin
2025-12-16 15:17:02 +08:00
committed by GitHub
9 changed files with 155 additions and 41 deletions

View File

@@ -21,7 +21,7 @@ You can update your `.qwenignore` file at any time. To apply the changes, you mu
## How to use `.qwenignore` ## How to use `.qwenignore`
| Step | Description | | Step | Description |
| ---------------------- | ------------------------------------------------------------ | | ---------------------- | -------------------------------------------------------------------------------------- |
| **Enable .qwenignore** | Create a file named `.qwenignore` in your project root directory | | **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` | | **Add ignore rules** | Open `.qwenignore` file and add paths to ignore, example: `/archive/` or `apikeys.txt` |

View File

@@ -51,11 +51,12 @@ Settings are organized into categories. All settings should be placed within the
#### general #### general
| Setting | Type | Description | Default | | Setting | Type | Description | Default |
| ------------------------------- | ------- | ------------------------------------------ | ----------- | | ------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- | ----------- |
| `general.preferredEditor` | string | The preferred editor to open files in. | `undefined` | | `general.preferredEditor` | string | The preferred editor to open files in. | `undefined` |
| `general.vimMode` | boolean | Enable Vim keybindings. | `false` | | `general.vimMode` | boolean | Enable Vim keybindings. | `false` |
| `general.disableAutoUpdate` | boolean | Disable automatic updates. | `false` | | `general.disableAutoUpdate` | boolean | Disable automatic updates. | `false` |
| `general.disableUpdateNag` | boolean | Disable update notification prompts. | `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` | | `general.checkpointing.enabled` | boolean | Enable session checkpointing for recovery. | `false` |
#### output #### output

View File

@@ -140,8 +140,6 @@ The theme file must be a valid JSON file that follows the same structure as a cu
### Example Custom Theme ### Example Custom Theme
<img src="https://gw.alicdn.com/imgextra/i1/O1CN01Em30Hc1jYXAdIgls3_!!6000000004560-2-tps-1009-629.png" alt=" " style="zoom:100%;text-align:center;margin: 0 auto;" /> <img src="https://gw.alicdn.com/imgextra/i1/O1CN01Em30Hc1jYXAdIgls3_!!6000000004560-2-tps-1009-629.png" alt=" " style="zoom:100%;text-align:center;margin: 0 auto;" />
### Using Your Custom Theme ### Using Your Custom Theme
@@ -150,12 +148,10 @@ 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`. - 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. - 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 ## Themes Preview
| Dark Theme | Preview | Light Theme | Preview | | Dark Theme | Preview | Light Theme | Preview |
| :-: | :-: | :-: | :-: | | :----------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| ANSI | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01ZInJiq1GdSZc9gHsI_!!6000000000645-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | ANSI Light | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01IiJQFC1h9E3MXQj6W_!!6000000004234-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | | ANSI | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01ZInJiq1GdSZc9gHsI_!!6000000000645-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | ANSI Light | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01IiJQFC1h9E3MXQj6W_!!6000000004234-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Atom OneDark | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01Zlx1SO1Sw21SkTKV3_!!6000000002310-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | Ayu Light | <img src="https://gw.alicdn.com/imgextra/i3/O1CN01zEUc1V1jeUJsnCgQb_!!6000000004573-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> | | Atom OneDark | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01Zlx1SO1Sw21SkTKV3_!!6000000002310-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | Ayu Light | <img src="https://gw.alicdn.com/imgextra/i3/O1CN01zEUc1V1jeUJsnCgQb_!!6000000004573-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Ayu | <img src="https://gw.alicdn.com/imgextra/i3/O1CN019upo6v1SmPhmRjzfN_!!6000000002289-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> | Default Light | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01RHjrEs1u7TXq3M6l3_!!6000000005990-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> | | Ayu | <img src="https://gw.alicdn.com/imgextra/i3/O1CN019upo6v1SmPhmRjzfN_!!6000000002289-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> | Default Light | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01RHjrEs1u7TXq3M6l3_!!6000000005990-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |

View File

@@ -85,7 +85,7 @@ 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. 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 | | Exit Code | Error Type | Description |
| --------- | -------------------------- | ------------------------------------------------------------ | | --------- | -------------------------- | --------------------------------------------------------------------------------------------------- |
| 41 | `FatalAuthenticationError` | An error occurred during the authentication process. | | 41 | `FatalAuthenticationError` | An error occurred during the authentication process. |
| 42 | `FatalInputError` | Invalid or missing input was provided to the CLI. (non-interactive mode only) | | 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). | | 44 | `FatalSandboxError` | An error occurred with the sandboxing environment (e.g. Docker, Podman, or Seatbelt). |

View File

@@ -1002,6 +1002,7 @@ export async function loadCliConfig(
enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation, enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation,
eventEmitter: appEvents, eventEmitter: appEvents,
useSmartEdit: argv.useSmartEdit ?? settings.useSmartEdit, useSmartEdit: argv.useSmartEdit ?? settings.useSmartEdit,
gitCoAuthor: settings.general?.gitCoAuthor,
output: { output: {
format: outputSettingsFormat, format: outputSettingsFormat,
}, },

View File

@@ -147,6 +147,16 @@ const SETTINGS_SCHEMA = {
description: 'Disable update notification prompts.', description: 'Disable update notification prompts.',
showInDialog: false, 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: { checkpointing: {
type: 'object', type: 'object',
label: 'Checkpointing', label: 'Checkpointing',

View File

@@ -287,7 +287,7 @@ export interface ConfigParameters {
contextFileName?: string | string[]; contextFileName?: string | string[];
accessibility?: AccessibilitySettings; accessibility?: AccessibilitySettings;
telemetry?: TelemetrySettings; telemetry?: TelemetrySettings;
gitCoAuthor?: GitCoAuthorSettings; gitCoAuthor?: boolean;
usageStatisticsEnabled?: boolean; usageStatisticsEnabled?: boolean;
fileFiltering?: { fileFiltering?: {
respectGitIgnore?: boolean; respectGitIgnore?: boolean;
@@ -534,9 +534,9 @@ export class Config {
useCollector: params.telemetry?.useCollector, useCollector: params.telemetry?.useCollector,
}; };
this.gitCoAuthor = { this.gitCoAuthor = {
enabled: params.gitCoAuthor?.enabled ?? true, enabled: params.gitCoAuthor ?? true,
name: params.gitCoAuthor?.name ?? 'Qwen-Coder', name: 'Qwen-Coder',
email: params.gitCoAuthor?.email ?? 'qwen-coder@alibabacloud.com', email: 'qwen-coder@alibabacloud.com',
}; };
this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true; this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true;

View File

@@ -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 <qwen-coder@alibabacloud.com>',
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
false,
{},
);
});
it('should not modify non-git commands', async () => { it('should not modify non-git commands', async () => {
const command = 'npm install'; const command = 'npm install';
const invocation = shellTool.build({ command, is_background: false }); 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 <qwen-coder@alibabacloud.com>',
),
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 <qwen-coder@alibabacloud.com>',
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
false,
{},
);
});
}); });
}); });

View File

@@ -334,13 +334,14 @@ export class ShellToolInvocation extends BaseToolInvocation<
private addCoAuthorToGitCommit(command: string): string { private addCoAuthorToGitCommit(command: string): string {
// Check if co-author feature is enabled // Check if co-author feature is enabled
const gitCoAuthorSettings = this.config.getGitCoAuthor(); const gitCoAuthorSettings = this.config.getGitCoAuthor();
if (!gitCoAuthorSettings.enabled) { if (!gitCoAuthorSettings.enabled) {
return command; return command;
} }
// Check if this is a git commit command // Check if this is a git commit command (anywhere in the command, e.g., after "cd /path &&")
const gitCommitPattern = /^git\s+commit/; const gitCommitPattern = /\bgit\s+commit\b/;
if (!gitCommitPattern.test(command.trim())) { if (!gitCommitPattern.test(command)) {
return command; return command;
} }
@@ -349,15 +350,27 @@ export class ShellToolInvocation extends BaseToolInvocation<
Co-authored-by: ${gitCoAuthorSettings.name} <${gitCoAuthorSettings.email}>`; Co-authored-by: ${gitCoAuthorSettings.name} <${gitCoAuthorSettings.email}>`;
// Handle different git commit patterns // Handle different git commit patterns:
// Match -m "message" or -m 'message' // Match -m "message" or -m 'message', including combined flags like -am
const messagePattern = /(-m\s+)(['"])((?:\\.|[^\\])*?)(\2)/; // Use separate patterns to avoid ReDoS (catastrophic backtracking)
const match = command.match(messagePattern); //
// 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) { if (match) {
const [fullMatch, prefix, quote, existingMessage, closingQuote] = match; const [fullMatch, prefix, existingMessage] = match;
const newMessage = existingMessage + coAuthor; const newMessage = existingMessage + coAuthor;
const replacement = prefix + quote + newMessage + closingQuote; const replacement = prefix + quote + newMessage + quote;
return command.replace(fullMatch, replacement); return command.replace(fullMatch, replacement);
} }