import * as vscode from "vscode"import * as path from "path"import { promises as fs } from "fs"import { exec } from "child_process"import { promisify } from "util"import type { GitRepositoryInfo, GitCommit } from "@roo-code/types"import { truncateOutput } from "../integrations/misc/extract-text"const execAsync = promisify(exec)const GIT_OUTPUT_LINE_LIMIT = 500/*** Extracts git repository information from the workspace's .git directory* @param workspaceRoot The root path of the workspace* @returns Git repository information or empty object if not a git repository*/export async function getGitRepositoryInfo(workspaceRoot: string): Promise<GitRepositoryInfo> {try {const gitDir = path.join(workspaceRoot, ".git")// Check if .git directory existstry {await fs.access(gitDir)} catch {// Not a git repositoryreturn {}}const gitInfo: GitRepositoryInfo = {}// Try to read git config filetry {const configPath = path.join(gitDir, "config")const configContent = await fs.readFile(configPath, "utf8")// Very simple approach - just find any URL lineconst urlMatch = configContent.match(/url\s*=\s*(.+?)(?:\r?\n|$)/m)if (urlMatch && urlMatch[1]) {const url = urlMatch[1].trim()// Sanitize the URL and convert to HTTPS formatgitInfo.repositoryUrl = convertGitUrlToHttps(sanitizeGitUrl(url))const repositoryName = extractRepositoryName(url)if (repositoryName) {gitInfo.repositoryName = repositoryName}}// Extract default branch (if available)const branchMatch = configContent.match(/\[branch "([^"]+)"\]/i)if (branchMatch && branchMatch[1]) {gitInfo.defaultBranch = branchMatch[1]}} catch (error) {// Ignore config reading errors}// Try to read HEAD file to get current branchif (!gitInfo.defaultBranch) {try {const headPath = path.join(gitDir, "HEAD")const headContent = await fs.readFile(headPath, "utf8")const branchMatch = headContent.match(/ref: refs\/heads\/(.+)/)if (branchMatch && branchMatch[1]) {gitInfo.defaultBranch = branchMatch[1].trim()}} catch (error) {// Ignore HEAD reading errors}}return gitInfo} catch (error) {// Return empty object on any errorreturn {}}}/*** Converts a git URL to HTTPS format* @param url The git URL to convert* @returns The URL in HTTPS format, or the original URL if conversion is not possible*/export function convertGitUrlToHttps(url: string): string {try {// Already HTTPS, just return itif (url.startsWith("https://")) {return url}// Handle SSH format: git@github.com:user/repo.git -> https://github.com/user/repo.gitif (url.startsWith("git@")) {const match = url.match(/git@([^:]+):(.+)/)if (match && match.length === 3) {const [, host, path] = matchreturn `https://${host}/${path}`}}// Handle SSH with protocol: ssh://git@github.com/user/repo.git -> https://github.com/user/repo.gitif (url.startsWith("ssh://")) {const match = url.match(/ssh:\/\/(?:git@)?([^\/]+)\/(.+)/)if (match && match.length === 3) {const [, host, path] = matchreturn `https://${host}/${path}`}}// Return original URL if we can't convert itreturn url} catch {// If parsing fails, return originalreturn url}}/*** Sanitizes a git URL to remove sensitive information like tokens* @param url The original git URL* @returns Sanitized URL*/export function sanitizeGitUrl(url: string): string {try {// Remove credentials from HTTPS URLsif (url.startsWith("https://")) {const urlObj = new URL(url)// Remove username and passwordurlObj.username = ""urlObj.password = ""return urlObj.toString()}// For SSH URLs, return as-is (they don't contain sensitive tokens)if (url.startsWith("git@") || url.startsWith("ssh://")) {return url}// For other formats, return as-is but remove any potential tokensreturn url.replace(/:[a-f0-9]{40,}@/gi, "@")} catch {// If URL parsing fails, return original (might be SSH format)return url}}/*** Extracts repository name from a git URL* @param url The git URL* @returns Repository name or undefined*/export function extractRepositoryName(url: string): string {try {// Handle different URL formatsconst patterns = [// HTTPS: https://github.com/user/repo.git -> user/repo/https:\/\/[^\/]+\/([^\/]+\/[^\/]+?)(?:\.git)?$/,// SSH: git@github.com:user/repo.git -> user/repo/git@[^:]+:([^\/]+\/[^\/]+?)(?:\.git)?$/,// SSH with user: ssh://git@github.com/user/repo.git -> user/repo/ssh:\/\/[^\/]+\/([^\/]+\/[^\/]+?)(?:\.git)?$/,]for (const pattern of patterns) {const match = url.match(pattern)if (match && match[1]) {return match[1].replace(/\.git$/, "")}}return ""} catch {return ""}}/*** Gets git repository information for the current VSCode workspace* @returns Git repository information or empty object if not available*/export async function getWorkspaceGitInfo(): Promise<GitRepositoryInfo> {const workspaceFolders = vscode.workspace.workspaceFoldersif (!workspaceFolders || workspaceFolders.length === 0) {return {}}// Use the first workspace folder.const workspaceRoot = workspaceFolders[0].uri.fsPathreturn getGitRepositoryInfo(workspaceRoot)}async function checkGitRepo(cwd: string): Promise<boolean> {try {await execAsync("git rev-parse --git-dir", { cwd })return true} catch (error) {return false}}/*** Checks if Git is installed on the system by attempting to run git --version* @returns {Promise<boolean>} True if Git is installed and accessible, false otherwise* @example* const isGitInstalled = await checkGitInstalled();* if (!isGitInstalled) {* console.log("Git is not installed");* }*/export async function checkGitInstalled(): Promise<boolean> {try {await execAsync("git --version")return true} catch (error) {return false}}export async function searchCommits(query: string, cwd: string): Promise<GitCommit[]> {try {const isInstalled = await checkGitInstalled()if (!isInstalled) {console.error("Git is not installed")return []}const isRepo = await checkGitRepo(cwd)if (!isRepo) {console.error("Not a git repository")return []}// Search commits by hash or message, limiting to 10 resultsconst { stdout } = await execAsync(`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--grep="${query}" --regexp-ignore-case`,{ cwd },)let output = stdoutif (!output.trim() && /^[a-f0-9]+$/i.test(query)) {// If no results from grep search and query looks like a hash, try searching by hashconst { stdout: hashStdout } = await execAsync(`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--author-date-order ${query}`,{ cwd },).catch(() => ({ stdout: "" }))if (!hashStdout.trim()) {return []}output = hashStdout}const commits: GitCommit[] = []const lines = output.trim().split("\n").filter((line) => line !== "--")for (let i = 0; i < lines.length; i += 5) {commits.push({hash: lines[i],shortHash: lines[i + 1],subject: lines[i + 2],author: lines[i + 3],date: lines[i + 4],})}return commits} catch (error) {console.error("Error searching commits:", error)return []}}export async function getCommitInfo(hash: string, cwd: string): Promise<string> {try {const isInstalled = await checkGitInstalled()if (!isInstalled) {return "Git is not installed"}const isRepo = await checkGitRepo(cwd)if (!isRepo) {return "Not a git repository"}// Get commit info, stats, and diff separatelyconst { stdout: info } = await execAsync(`git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch ${hash}`, {cwd,})const [fullHash, shortHash, subject, author, date, body] = info.trim().split("\n")const { stdout: stats } = await execAsync(`git show --stat --format="" ${hash}`, { cwd })const { stdout: diff } = await execAsync(`git show --format="" ${hash}`, { cwd })const summary = [`Commit: ${shortHash} (${fullHash})`,`Author: ${author}`,`Date: ${date}`,`\nMessage: ${subject}`,body ? `\nDescription:\n${body}` : "","\nFiles Changed:",stats.trim(),"\nFull Changes:",].join("\n")const output = summary + "\n\n" + diff.trim()return truncateOutput(output, GIT_OUTPUT_LINE_LIMIT)} catch (error) {console.error("Error getting commit info:", error)return `Failed to get commit info: ${error instanceof Error ? error.message : String(error)}`}}export async function getWorkingState(cwd: string): Promise<string> {try {const isInstalled = await checkGitInstalled()if (!isInstalled) {return "Git is not installed"}const isRepo = await checkGitRepo(cwd)if (!isRepo) {return "Not a git repository"}// Get status of working directoryconst { stdout: status } = await execAsync("git status --short", { cwd })if (!status.trim()) {return "No changes in working directory"}// Get all changes (both staged and unstaged) compared to HEADconst { stdout: diff } = await execAsync("git diff HEAD", { cwd })const lineLimit = GIT_OUTPUT_LINE_LIMITconst output = `Working directory changes:\n\n${status}\n\n${diff}`.trim()return truncateOutput(output, lineLimit)} catch (error) {console.error("Error getting working state:", error)return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}`}}/*** Gets git status output with configurable file limit* @param cwd The working directory to check git status in* @param maxFiles Maximum number of file entries to include (0 = disabled)* @returns Git status string or null if not a git repository*/export async function getGitStatus(cwd: string, maxFiles: number = 20): Promise<string | null> {try {const isInstalled = await checkGitInstalled()if (!isInstalled) {return null}const isRepo = await checkGitRepo(cwd)if (!isRepo) {return null}// Use porcelain v1 format with branch infoconst { stdout } = await execAsync("git status --porcelain=v1 --branch", { cwd })if (!stdout.trim()) {return null}const lines = stdout.trim().split("\n")// First line is always branch info (e.g., "## main...origin/main")const branchLine = lines[0]const fileLines = lines.slice(1)// Build output with branch info and limited file entriesconst output: string[] = [branchLine]if (maxFiles > 0 && fileLines.length > 0) {const filesToShow = fileLines.slice(0, maxFiles)output.push(...filesToShow)// Add truncation notice if neededif (fileLines.length > maxFiles) {output.push(`... ${fileLines.length - maxFiles} more files`)}}return output.join("\n")} catch (error) {console.error("Error getting git status:", error)return null}}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。