git-remote-tree.mjs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import { mkdtempSync, rmSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { spawnSync } from 'node:child_process'
  5. import { normalizeReleaseError } from './release-error-normalizer.mjs'
  6. function runGit(args, options = {}) {
  7. const result = spawnSync('git', args, {
  8. cwd: options.cwd,
  9. encoding: 'utf8',
  10. stdio: 'pipe',
  11. timeout: options.timeout ?? 20_000
  12. })
  13. const error = result.error instanceof Error ? result.error.message : ''
  14. return {
  15. ok: result.status === 0,
  16. status: result.status,
  17. signal: result.signal || '',
  18. stdout: (result.stdout || '').trim(),
  19. stderr: (result.stderr || '').trim(),
  20. error
  21. }
  22. }
  23. function commandError(result) {
  24. return normalizeReleaseError(result.error || result.stderr || result.stdout || result.signal || 'unknown error')
  25. }
  26. export function localHeadTree(repoDir) {
  27. const head = runGit(['rev-parse', 'HEAD'], { cwd: repoDir })
  28. const tree = runGit(['rev-parse', 'HEAD^{tree}'], { cwd: repoDir })
  29. return {
  30. ok: head.ok && tree.ok && Boolean(head.stdout) && Boolean(tree.stdout),
  31. hash: head.stdout,
  32. tree: tree.stdout,
  33. error: head.ok && tree.ok ? '' : commandError(head.ok ? tree : head)
  34. }
  35. }
  36. export function remoteHeadTree(url, branch = 'main', options = {}) {
  37. const workdir = mkdtempSync(join(tmpdir(), 'file-viewer-remote-tree-'))
  38. try {
  39. const init = runGit(['init', '-q'], { cwd: workdir, timeout: options.timeout })
  40. if (!init.ok) {
  41. return {
  42. ok: false,
  43. tree: '',
  44. error: commandError(init)
  45. }
  46. }
  47. const fetch = runGit(
  48. [
  49. '-c',
  50. 'protocol.version=2',
  51. 'fetch',
  52. '--quiet',
  53. '--depth=1',
  54. '--filter=blob:none',
  55. '--no-tags',
  56. url,
  57. `refs/heads/${branch}`
  58. ],
  59. { cwd: workdir, timeout: options.timeout }
  60. )
  61. if (!fetch.ok) {
  62. return {
  63. ok: false,
  64. tree: '',
  65. error: commandError(fetch)
  66. }
  67. }
  68. const tree = runGit(['rev-parse', 'FETCH_HEAD^{tree}'], { cwd: workdir, timeout: options.timeout })
  69. return {
  70. ok: tree.ok && Boolean(tree.stdout),
  71. tree: tree.stdout,
  72. error: tree.ok && tree.stdout ? '' : commandError(tree)
  73. }
  74. } finally {
  75. rmSync(workdir, { recursive: true, force: true })
  76. }
  77. }
  78. export function comparePublicMirrorTrees({ publicRepoDir, githubHead, giteeHead, giteeUrl, branch = 'main', timeout }) {
  79. if (!githubHead.ok || !giteeHead.ok) {
  80. return {
  81. inSync: false,
  82. mode: 'missing',
  83. checked: false,
  84. referenceTree: '',
  85. giteeTree: '',
  86. error: 'missing public repository head'
  87. }
  88. }
  89. if (githubHead.hash === giteeHead.hash) {
  90. return {
  91. inSync: true,
  92. mode: 'commit',
  93. checked: false,
  94. referenceTree: '',
  95. giteeTree: '',
  96. error: ''
  97. }
  98. }
  99. const reference = localHeadTree(publicRepoDir)
  100. if (!reference.ok || reference.hash !== githubHead.hash) {
  101. return {
  102. inSync: false,
  103. mode: 'unchecked',
  104. checked: false,
  105. referenceTree: reference.tree,
  106. giteeTree: '',
  107. error: reference.ok
  108. ? `local public repository ${reference.hash.slice(0, 12)} does not match GitHub ${githubHead.hash.slice(0, 12)}`
  109. : reference.error
  110. }
  111. }
  112. const gitee = remoteHeadTree(giteeUrl, branch, { timeout })
  113. if (!gitee.ok) {
  114. return {
  115. inSync: false,
  116. mode: 'unchecked',
  117. checked: false,
  118. referenceTree: reference.tree,
  119. giteeTree: '',
  120. error: gitee.error
  121. }
  122. }
  123. return {
  124. inSync: reference.tree === gitee.tree,
  125. mode: reference.tree === gitee.tree ? 'tree' : 'stale',
  126. checked: true,
  127. referenceTree: reference.tree,
  128. giteeTree: gitee.tree,
  129. error: ''
  130. }
  131. }