Skip to content

Commit 4cf80d5

Browse files
feat(svn): 添加SVN提交搜索功能和相关组件支持
在Webview消息处理、扩展消息类型和UI组件中添加对SVN提交搜索的支持 - 新增SVN提交搜索功能及相关类型定义 - 扩展上下文菜单以支持SVN提交选项 - 添加SVN工具函数用于搜索和解析提交信息 - 在聊天文本区域集成SVN提交搜索功能
1 parent e692bef commit 4cf80d5

File tree

7 files changed

+444
-4
lines changed

7 files changed

+444
-4
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { fileExistsAtPath } from "../../utils/fs"
3737
import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
3838
import { singleCompletionHandler } from "../../utils/single-completion-handler"
3939
import { searchCommits } from "../../utils/git"
40+
import { searchSvnCommits } from "../../utils/svn"
4041
import { exportSettings, importSettingsWithFeedback } from "../config/importExport"
4142
import { getOpenAiModels } from "../../api/providers/openai"
4243
import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
@@ -1384,6 +1385,24 @@ export const webviewMessageHandler = async (
13841385
}
13851386
break
13861387
}
1388+
case "searchSvnCommits": {
1389+
const cwd = provider.cwd
1390+
if (cwd) {
1391+
try {
1392+
const svnCommits = await searchSvnCommits(message.query || "", cwd)
1393+
await provider.postMessageToWebview({
1394+
type: "svnCommitSearchResults",
1395+
svnCommits,
1396+
})
1397+
} catch (error) {
1398+
provider.log(
1399+
`Error searching SVN commits: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
1400+
)
1401+
vscode.window.showErrorMessage("Error searching SVN commits")
1402+
}
1403+
}
1404+
break
1405+
}
13871406
case "searchFiles": {
13881407
const workspacePath = getWorkspacePath()
13891408

src/shared/ExtensionMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
} from "@roo-code/types"
1414

1515
import { GitCommit } from "../utils/git"
16+
import { SvnCommit } from "../utils/svn"
1617

1718
import { McpServer } from "./mcp"
1819
import { Mode } from "./modes"
@@ -61,6 +62,7 @@ export interface ExtensionMessage {
6162
| "mcpServers"
6263
| "enhancedPrompt"
6364
| "commitSearchResults"
65+
| "svnCommitSearchResults"
6466
| "listApiConfig"
6567
| "routerModels"
6668
| "openAiModels"
@@ -137,6 +139,7 @@ export interface ExtensionMessage {
137139
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
138140
mcpServers?: McpServer[]
139141
commits?: GitCommit[]
142+
svnCommits?: SvnCommit[]
140143
listApiConfig?: ProviderSettingsEntry[]
141144
mode?: Mode
142145
customMode?: ModeConfig

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export interface WebviewMessage {
129129
| "mcpEnabled"
130130
| "enableMcpServerCreation"
131131
| "searchCommits"
132+
| "searchSvnCommits"
132133
| "alwaysApproveResubmit"
133134
| "requestDelaySeconds"
134135
| "setApiConfigPassword"

src/utils/svn.ts

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
import * as vscode from "vscode"
2+
import * as path from "path"
3+
import { promises as fs } from "fs"
4+
import { exec } from "child_process"
5+
import { promisify } from "util"
6+
import { truncateOutput } from "../integrations/misc/extract-text"
7+
8+
const execAsync = promisify(exec)
9+
const SVN_OUTPUT_LINE_LIMIT = 500
10+
11+
export interface SvnRepositoryInfo {
12+
repositoryUrl?: string
13+
repositoryName?: string
14+
workingCopyRoot?: string
15+
}
16+
17+
export interface SvnCommit {
18+
revision: string
19+
author: string
20+
date: string
21+
message: string
22+
}
23+
24+
/**
25+
* Extracts SVN repository information from the workspace's .svn directory
26+
* @param workspaceRoot The root path of the workspace
27+
* @returns SVN repository information or empty object if not an SVN repository
28+
*/
29+
export async function getSvnRepositoryInfo(workspaceRoot: string): Promise<SvnRepositoryInfo> {
30+
try {
31+
const svnDir = path.join(workspaceRoot, ".svn")
32+
33+
// Check if .svn directory exists
34+
try {
35+
await fs.access(svnDir)
36+
} catch {
37+
// Not an SVN repository
38+
return {}
39+
}
40+
41+
const svnInfo: SvnRepositoryInfo = {}
42+
43+
// Try to get SVN info using svn info command
44+
try {
45+
const { stdout } = await execAsync("svn info", { cwd: workspaceRoot })
46+
47+
// Parse SVN info output
48+
const urlMatch = stdout.match(/^URL:\s*(.+)$/m)
49+
if (urlMatch && urlMatch[1]) {
50+
const url = urlMatch[1].trim()
51+
svnInfo.repositoryUrl = url
52+
svnInfo.repositoryName = extractSvnRepositoryName(url)
53+
}
54+
55+
const rootMatch = stdout.match(/^Working Copy Root Path:\s*(.+)$/m)
56+
if (rootMatch && rootMatch[1]) {
57+
svnInfo.workingCopyRoot = rootMatch[1].trim()
58+
}
59+
} catch (error) {
60+
// Ignore SVN info errors
61+
}
62+
63+
return svnInfo
64+
} catch (error) {
65+
// Return empty object on any error
66+
return {}
67+
}
68+
}
69+
70+
/**
71+
* Extracts repository name from an SVN URL
72+
* @param url The SVN URL
73+
* @returns Repository name or undefined
74+
*/
75+
export function extractSvnRepositoryName(url: string): string {
76+
try {
77+
// Handle different SVN URL formats
78+
const patterns = [
79+
// Standard SVN: https://svn.example.com/repo/trunk -> repo
80+
/\/([^\/]+)\/(?:trunk|branches|tags)(?:\/.*)?$/,
81+
// Simple repo: https://svn.example.com/repo -> repo
82+
/\/([^\/]+)\/?$/,
83+
]
84+
85+
for (const pattern of patterns) {
86+
const match = url.match(pattern)
87+
if (match && match[1]) {
88+
return match[1]
89+
}
90+
}
91+
92+
// Fallback: use the last part of the URL
93+
const parts = url.split("/").filter(Boolean)
94+
return parts[parts.length - 1] || ""
95+
} catch {
96+
return ""
97+
}
98+
}
99+
100+
/**
101+
* Gets SVN repository information for the current VSCode workspace
102+
* @returns SVN repository information or empty object if not available
103+
*/
104+
export async function getWorkspaceSvnInfo(): Promise<SvnRepositoryInfo> {
105+
const workspaceFolders = vscode.workspace.workspaceFolders
106+
if (!workspaceFolders || workspaceFolders.length === 0) {
107+
return {}
108+
}
109+
110+
// Use the first workspace folder
111+
const workspaceRoot = workspaceFolders[0].uri.fsPath
112+
return getSvnRepositoryInfo(workspaceRoot)
113+
}
114+
115+
/**
116+
* Checks if the given directory is an SVN repository
117+
* @param cwd The directory to check
118+
* @returns True if it's an SVN repository, false otherwise
119+
*/
120+
export async function checkSvnRepo(cwd: string): Promise<boolean> {
121+
try {
122+
const svnDir = path.join(cwd, ".svn")
123+
await fs.access(svnDir)
124+
return true
125+
} catch {
126+
return false
127+
}
128+
}
129+
130+
/**
131+
* Checks if SVN is installed and available
132+
* @returns True if SVN is available, false otherwise
133+
*/
134+
export async function checkSvnInstalled(): Promise<boolean> {
135+
try {
136+
await execAsync("svn --version")
137+
return true
138+
} catch {
139+
return false
140+
}
141+
}
142+
143+
/**
144+
* Searches for SVN commits by revision number or message content
145+
* @param query The search query (revision number or message text)
146+
* @param cwd The working directory
147+
* @returns Array of matching SVN commits
148+
*/
149+
export async function searchSvnCommits(query: string, cwd: string): Promise<SvnCommit[]> {
150+
try {
151+
// Check if SVN is available
152+
if (!(await checkSvnInstalled()) || !(await checkSvnRepo(cwd))) {
153+
return []
154+
}
155+
156+
const commits: SvnCommit[] = []
157+
158+
// If query looks like a revision number, search for that specific revision
159+
if (/^\d+$/.test(query)) {
160+
try {
161+
const { stdout } = await execAsync(`svn log -r ${query} --xml`, { cwd })
162+
const revisionCommits = parseSvnLogXml(stdout)
163+
commits.push(...revisionCommits)
164+
} catch {
165+
// Revision might not exist, continue with general search
166+
}
167+
}
168+
169+
// Search in commit messages (get recent commits and filter)
170+
try {
171+
const { stdout } = await execAsync("svn log -l 100 --xml", { cwd })
172+
const allCommits = parseSvnLogXml(stdout)
173+
174+
// Filter commits by message content
175+
const messageMatches = allCommits.filter(
176+
(commit) => commit.message.toLowerCase().includes(query.toLowerCase()) || commit.revision === query,
177+
)
178+
179+
// Add unique commits (avoid duplicates from revision search)
180+
messageMatches.forEach((commit) => {
181+
if (!commits.some((c) => c.revision === commit.revision)) {
182+
commits.push(commit)
183+
}
184+
})
185+
} catch {
186+
// Ignore errors in message search
187+
}
188+
189+
return commits.slice(0, 20) // Limit results
190+
} catch (error) {
191+
console.error("Error searching SVN commits:", error)
192+
return []
193+
}
194+
}
195+
196+
/**
197+
* Parses SVN log XML output into commit objects
198+
* @param xmlOutput The XML output from svn log --xml
199+
* @returns Array of SVN commits
200+
*/
201+
function parseSvnLogXml(xmlOutput: string): SvnCommit[] {
202+
const commits: SvnCommit[] = []
203+
204+
try {
205+
// Simple XML parsing for SVN log entries
206+
const entryRegex = /<logentry[^>]*revision="(\d+)"[^>]*>([\s\S]*?)<\/logentry>/g
207+
const authorRegex = /<author>([\s\S]*?)<\/author>/
208+
const dateRegex = /<date>([\s\S]*?)<\/date>/
209+
const msgRegex = /<msg>([\s\S]*?)<\/msg>/
210+
211+
let match
212+
while ((match = entryRegex.exec(xmlOutput)) !== null) {
213+
const [, revision, entryContent] = match
214+
215+
const authorMatch = authorRegex.exec(entryContent)
216+
const dateMatch = dateRegex.exec(entryContent)
217+
const msgMatch = msgRegex.exec(entryContent)
218+
219+
commits.push({
220+
revision,
221+
author: authorMatch ? authorMatch[1].trim() : "Unknown",
222+
date: dateMatch ? new Date(dateMatch[1]).toISOString() : "",
223+
message: msgMatch ? msgMatch[1].trim() : "",
224+
})
225+
}
226+
} catch (error) {
227+
console.error("Error parsing SVN log XML:", error)
228+
}
229+
230+
return commits
231+
}
232+
233+
/**
234+
* Gets detailed information about a specific SVN revision
235+
* @param revision The revision number
236+
* @param cwd The working directory
237+
* @returns Detailed commit information including diff
238+
*/
239+
export async function getSvnCommitInfo(
240+
revision: string,
241+
cwd: string,
242+
): Promise<{
243+
commit: SvnCommit | null
244+
diff: string
245+
stats: string
246+
}> {
247+
try {
248+
if (!(await checkSvnInstalled()) || !(await checkSvnRepo(cwd))) {
249+
return { commit: null, diff: "", stats: "" }
250+
}
251+
252+
// Get commit info
253+
const { stdout: logOutput } = await execAsync(`svn log -r ${revision} --xml`, { cwd })
254+
const commits = parseSvnLogXml(logOutput)
255+
const commit = commits[0] || null
256+
257+
// Get diff
258+
let diff = ""
259+
try {
260+
const { stdout: diffOutput } = await execAsync(`svn diff -c ${revision}`, { cwd })
261+
diff = truncateOutput(diffOutput, SVN_OUTPUT_LINE_LIMIT)
262+
} catch {
263+
// Diff might not be available
264+
}
265+
266+
// Get stats (changed files)
267+
let stats = ""
268+
try {
269+
const { stdout: changedOutput } = await execAsync(`svn log -r ${revision} -v`, { cwd })
270+
const changedMatch = changedOutput.match(/Changed paths:([\s\S]*?)(?=\n\n|\n-{72}|\n$)/)
271+
if (changedMatch) {
272+
stats = changedMatch[1].trim()
273+
}
274+
} catch {
275+
// Stats might not be available
276+
}
277+
278+
return { commit, diff, stats }
279+
} catch (error) {
280+
console.error("Error getting SVN commit info:", error)
281+
return { commit: null, diff: "", stats: "" }
282+
}
283+
}
284+
285+
/**
286+
* Gets the current working state of the SVN repository
287+
* @param cwd The working directory
288+
* @returns Object containing status and diff information
289+
*/
290+
export async function getSvnWorkingState(cwd: string): Promise<{
291+
status: string
292+
diff: string
293+
}> {
294+
try {
295+
if (!(await checkSvnInstalled()) || !(await checkSvnRepo(cwd))) {
296+
return { status: "", diff: "" }
297+
}
298+
299+
// Get status
300+
let status = ""
301+
try {
302+
const { stdout: statusOutput } = await execAsync("svn status", { cwd })
303+
status = truncateOutput(statusOutput, SVN_OUTPUT_LINE_LIMIT)
304+
} catch {
305+
// Status might not be available
306+
}
307+
308+
// Get diff of working changes
309+
let diff = ""
310+
try {
311+
const { stdout: diffOutput } = await execAsync("svn diff", { cwd })
312+
diff = truncateOutput(diffOutput, SVN_OUTPUT_LINE_LIMIT)
313+
} catch {
314+
// Diff might not be available
315+
}
316+
317+
return { status, diff }
318+
} catch (error) {
319+
console.error("Error getting SVN working state:", error)
320+
return { status: "", diff: "" }
321+
}
322+
}

0 commit comments

Comments
 (0)