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

@@ -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

View File

@@ -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

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
<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
@@ -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 | <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;" /> |
| 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;" /> |
| Default | <img src="https://gw.alicdn.com/imgextra/i4/O1CN016pIeXz1pFC8owmR4Q_!!6000000005330-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | GitHub Light | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01US2b0g1VETCPAVWLA_!!6000000002621-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Dracula | <img src="https://gw.alicdn.com/imgextra/i4/O1CN016htnWH20c3gd2LpUR_!!6000000006869-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | Google Code | <img src="https://gw.alicdn.com/imgextra/i1/O1CN01Ng29ab23iQ2BuYKz8_!!6000000007289-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| GitHub | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01fFCRda1IQIQ9qDNqv_!!6000000000887-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> | Xcode | <img src="https://gw.alicdn.com/imgextra/i1/O1CN010E3QAi1Huh5o1E9LN_!!6000000000818-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| 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;" /> |
| 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;" /> |
| Default | <img src="https://gw.alicdn.com/imgextra/i4/O1CN016pIeXz1pFC8owmR4Q_!!6000000005330-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | GitHub Light | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01US2b0g1VETCPAVWLA_!!6000000002621-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Dracula | <img src="https://gw.alicdn.com/imgextra/i4/O1CN016htnWH20c3gd2LpUR_!!6000000006869-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | Google Code | <img src="https://gw.alicdn.com/imgextra/i1/O1CN01Ng29ab23iQ2BuYKz8_!!6000000007289-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| GitHub | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01fFCRda1IQIQ9qDNqv_!!6000000000887-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> | Xcode | <img src="https://gw.alicdn.com/imgextra/i1/O1CN010E3QAi1Huh5o1E9LN_!!6000000000818-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |

View File

@@ -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

View File

@@ -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,
},

View File

@@ -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',

View File

@@ -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;

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 () => {
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 <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 {
// 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);
}