git.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import { exec } from "child_process"
  2. import { promisify } from "util"
  3. import { truncateOutput } from "../integrations/misc/extract-text"
  4. const execAsync = promisify(exec)
  5. const GIT_OUTPUT_LINE_LIMIT = 500
  6. export interface GitCommit {
  7. hash: string
  8. shortHash: string
  9. subject: string
  10. author: string
  11. date: string
  12. }
  13. async function checkGitRepo(cwd: string): Promise<boolean> {
  14. try {
  15. await execAsync("git rev-parse --git-dir", { cwd })
  16. return true
  17. } catch (error) {
  18. return false
  19. }
  20. }
  21. async function checkGitInstalled(): Promise<boolean> {
  22. try {
  23. await execAsync("git --version")
  24. return true
  25. } catch (error) {
  26. return false
  27. }
  28. }
  29. export async function searchCommits(query: string, cwd: string): Promise<GitCommit[]> {
  30. try {
  31. const isInstalled = await checkGitInstalled()
  32. if (!isInstalled) {
  33. console.error("Git is not installed")
  34. return []
  35. }
  36. const isRepo = await checkGitRepo(cwd)
  37. if (!isRepo) {
  38. console.error("Not a git repository")
  39. return []
  40. }
  41. // Search commits by hash or message, limiting to 10 results
  42. const { stdout } = await execAsync(
  43. `git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--grep="${query}" --regexp-ignore-case`,
  44. { cwd },
  45. )
  46. let output = stdout
  47. if (!output.trim() && /^[a-f0-9]+$/i.test(query)) {
  48. // If no results from grep search and query looks like a hash, try searching by hash
  49. const { stdout: hashStdout } = await execAsync(
  50. `git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--author-date-order ${query}`,
  51. { cwd },
  52. ).catch(() => ({ stdout: "" }))
  53. if (!hashStdout.trim()) {
  54. return []
  55. }
  56. output = hashStdout
  57. }
  58. const commits: GitCommit[] = []
  59. const lines = output
  60. .trim()
  61. .split("\n")
  62. .filter((line) => line !== "--")
  63. for (let i = 0; i < lines.length; i += 5) {
  64. commits.push({
  65. hash: lines[i],
  66. shortHash: lines[i + 1],
  67. subject: lines[i + 2],
  68. author: lines[i + 3],
  69. date: lines[i + 4],
  70. })
  71. }
  72. return commits
  73. } catch (error) {
  74. console.error("Error searching commits:", error)
  75. return []
  76. }
  77. }
  78. export async function getCommitInfo(hash: string, cwd: string): Promise<string> {
  79. try {
  80. const isInstalled = await checkGitInstalled()
  81. if (!isInstalled) {
  82. return "Git is not installed"
  83. }
  84. const isRepo = await checkGitRepo(cwd)
  85. if (!isRepo) {
  86. return "Not a git repository"
  87. }
  88. // Get commit info, stats, and diff separately
  89. const { stdout: info } = await execAsync(`git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch ${hash}`, {
  90. cwd,
  91. })
  92. const [fullHash, shortHash, subject, author, date, body] = info.trim().split("\n")
  93. const { stdout: stats } = await execAsync(`git show --stat --format="" ${hash}`, { cwd })
  94. const { stdout: diff } = await execAsync(`git show --format="" ${hash}`, { cwd })
  95. const summary = [
  96. `Commit: ${shortHash} (${fullHash})`,
  97. `Author: ${author}`,
  98. `Date: ${date}`,
  99. `\nMessage: ${subject}`,
  100. body ? `\nDescription:\n${body}` : "",
  101. "\nFiles Changed:",
  102. stats.trim(),
  103. "\nFull Changes:",
  104. ].join("\n")
  105. const output = summary + "\n\n" + diff.trim()
  106. return truncateOutput(output, GIT_OUTPUT_LINE_LIMIT)
  107. } catch (error) {
  108. console.error("Error getting commit info:", error)
  109. return `Failed to get commit info: ${error instanceof Error ? error.message : String(error)}`
  110. }
  111. }
  112. export async function getWorkingState(cwd: string): Promise<string> {
  113. try {
  114. const isInstalled = await checkGitInstalled()
  115. if (!isInstalled) {
  116. return "Git is not installed"
  117. }
  118. const isRepo = await checkGitRepo(cwd)
  119. if (!isRepo) {
  120. return "Not a git repository"
  121. }
  122. // Get status of working directory
  123. const { stdout: status } = await execAsync("git status --short", { cwd })
  124. if (!status.trim()) {
  125. return "No changes in working directory"
  126. }
  127. // Get all changes (both staged and unstaged) compared to HEAD
  128. const { stdout: diff } = await execAsync("git diff HEAD", { cwd })
  129. const lineLimit = GIT_OUTPUT_LINE_LIMIT
  130. const output = `Working directory changes:\n\n${status}\n\n${diff}`.trim()
  131. return truncateOutput(output, lineLimit)
  132. } catch (error) {
  133. console.error("Error getting working state:", error)
  134. return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}`
  135. }
  136. }