From ffc2d27ca39ab9a5df174b3d9b851d17b2d93b33 Mon Sep 17 00:00:00 2001 From: Fan Date: Thu, 7 Aug 2025 17:07:56 +0800 Subject: [PATCH] feat: add qwencoder as co-author (#207) * init Co-authored-by: Qwen-Coder * fix shell tool regex pattern for git commit messages Co-authored-by: Qwen-Coder --------- Co-authored-by: Qwen-Coder --- packages/core/src/config/config.ts | 17 ++++ packages/core/src/tools/shell.test.ts | 122 ++++++++++++++++++++++++++ packages/core/src/tools/shell.ts | 43 ++++++++- 3 files changed, 180 insertions(+), 2 deletions(-) diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index f17b37f9..c036b1f3 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -79,6 +79,12 @@ export interface TelemetrySettings { outfile?: string; } +export interface GitCoAuthorSettings { + enabled?: boolean; + name?: string; + email?: string; +} + export interface GeminiCLIExtension { name: string; version: string; @@ -164,6 +170,7 @@ export interface ConfigParameters { contextFileName?: string | string[]; accessibility?: AccessibilitySettings; telemetry?: TelemetrySettings; + gitCoAuthor?: GitCoAuthorSettings; usageStatisticsEnabled?: boolean; fileFiltering?: { respectGitIgnore?: boolean; @@ -227,6 +234,7 @@ export class Config { private readonly showMemoryUsage: boolean; private readonly accessibility: AccessibilitySettings; private readonly telemetrySettings: TelemetrySettings; + private readonly gitCoAuthor: GitCoAuthorSettings; private readonly usageStatisticsEnabled: boolean; private geminiClient!: GeminiClient; private readonly fileFiltering: { @@ -304,6 +312,11 @@ export class Config { logPrompts: params.telemetry?.logPrompts ?? true, outfile: params.telemetry?.outfile, }; + this.gitCoAuthor = { + enabled: params.gitCoAuthor?.enabled ?? true, + name: params.gitCoAuthor?.name ?? 'Qwen-Coder', + email: params.gitCoAuthor?.email ?? 'qwen-coder@alibabacloud.com', + }; this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true; this.fileFiltering = { @@ -571,6 +584,10 @@ export class Config { return this.telemetrySettings.outfile; } + getGitCoAuthor(): GitCoAuthorSettings { + return this.gitCoAuthor; + } + getGeminiClient(): GeminiClient { return this.geminiClient; } diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index 7f237e3d..1ed61759 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -56,6 +56,11 @@ describe('ShellTool', () => { getSummarizeToolOutputConfig: vi.fn().mockReturnValue(undefined), getWorkspaceContext: () => createMockWorkspaceContext('.'), getGeminiClient: vi.fn(), + getGitCoAuthor: vi.fn().mockReturnValue({ + enabled: true, + name: 'Qwen-Coder', + email: 'qwen-coder@alibabacloud.com', + }), } as unknown as Config; shellTool = new ShellTool(mockConfig); @@ -386,6 +391,123 @@ describe('ShellTool', () => { expect(confirmation).toBe(false); }); }); + + describe('addCoAuthorToGitCommit', () => { + it('should add co-author to git commit with double quotes', () => { + const command = 'git commit -m "Initial commit"'; + // Use public test method + const result = ( + shellTool as unknown as { + addCoAuthorToGitCommit: (command: string) => string; + } + ).addCoAuthorToGitCommit(command); + expect(result).toBe( + `git commit -m "Initial commit + +Co-authored-by: Qwen-Coder "`, + ); + }); + + it('should add co-author to git commit with single quotes', () => { + const command = "git commit -m 'Fix bug'"; + const result = ( + shellTool as unknown as { + addCoAuthorToGitCommit: (command: string) => string; + } + ).addCoAuthorToGitCommit(command); + expect(result).toBe( + `git commit -m 'Fix bug + +Co-authored-by: Qwen-Coder '`, + ); + }); + + it('should handle git commit with additional flags', () => { + const command = 'git commit -a -m "Add feature"'; + const result = ( + shellTool as unknown as { + addCoAuthorToGitCommit: (command: string) => string; + } + ).addCoAuthorToGitCommit(command); + expect(result).toBe( + `git commit -a -m "Add feature + +Co-authored-by: Qwen-Coder "`, + ); + }); + + it('should not modify non-git commands', () => { + const command = 'npm install'; + const result = ( + shellTool as unknown as { + addCoAuthorToGitCommit: (command: string) => string; + } + ).addCoAuthorToGitCommit(command); + expect(result).toBe('npm install'); + }); + + it('should not modify git commands without -m flag', () => { + const command = 'git commit'; + const result = ( + shellTool as unknown as { + addCoAuthorToGitCommit: (command: string) => string; + } + ).addCoAuthorToGitCommit(command); + expect(result).toBe('git commit'); + }); + + it('should handle git commit with escaped quotes in message', () => { + const command = 'git commit -m "Fix \\"quoted\\" text"'; + const result = ( + shellTool as unknown as { + addCoAuthorToGitCommit: (command: string) => string; + } + ).addCoAuthorToGitCommit(command); + expect(result).toBe( + `git commit -m "Fix \\"quoted\\" text + +Co-authored-by: Qwen-Coder "`, + ); + }); + + it('should not add co-author when disabled in config', () => { + // Mock config with disabled co-author + (mockConfig.getGitCoAuthor as Mock).mockReturnValue({ + enabled: false, + name: 'Qwen-Coder', + email: 'qwen-coder@alibabacloud.com', + }); + + const command = 'git commit -m "Initial commit"'; + const result = ( + shellTool as unknown as { + addCoAuthorToGitCommit: (command: string) => string; + } + ).addCoAuthorToGitCommit(command); + expect(result).toBe('git commit -m "Initial commit"'); + }); + + it('should use custom name and email from config', () => { + // Mock config with custom co-author details + (mockConfig.getGitCoAuthor as Mock).mockReturnValue({ + enabled: true, + name: 'Custom Bot', + email: 'custom@example.com', + }); + + const command = 'git commit -m "Test commit"'; + const result = ( + shellTool as unknown as { + addCoAuthorToGitCommit: (command: string) => string; + } + ).addCoAuthorToGitCommit(command); + expect(result).toBe( + `git commit -m "Test commit + +Co-authored-by: Custom Bot "`, + ); + }); + }); }); describe('validateToolParams', () => { diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 96423af1..0713585f 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -205,12 +205,15 @@ export class ShellTool extends BaseTool { const tempFilePath = path.join(os.tmpdir(), tempFileName); try { + // Add co-author to git commit commands + const processedCommand = this.addCoAuthorToGitCommit(strippedCommand); + // pgrep is not available on Windows, so we can't get background PIDs const commandToExecute = isWindows - ? strippedCommand + ? processedCommand : (() => { // wrap command to append subprocess pids (via pgrep) to temporary file - let command = strippedCommand.trim(); + let command = processedCommand.trim(); if (!command.endsWith('&')) command += ';'; return `{ ${command} }; __code=$?; pgrep -g 0 >${tempFilePath} 2>&1; exit $__code;`; })(); @@ -382,4 +385,40 @@ export class ShellTool extends BaseTool { } } } + + 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())) { + return command; + } + + // Define the co-author line using configuration + const coAuthor = ` + +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); + + if (match) { + const [fullMatch, prefix, quote, existingMessage, closingQuote] = match; + const newMessage = existingMessage + coAuthor; + const replacement = prefix + quote + newMessage + closingQuote; + + 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 + return command; + } }