OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / scripts / migrate-v1-issues.mjs

166 lines · javascript ref:
#!/usr/bin/env node
/*
 * migrate-v1-issues.mjs — Phase 2 of the v1 → v3 migration.
 *
 * Reads the JSON dump produced by migrate-v1-extract.mjs (Phase 1,
 * which runs under bare) and writes into a v3 IssueRegistry.
 *
 * Usage:
 *   node scripts/migrate-v1-issues.mjs \
 *     --in /tmp/v1-migration.json \
 *     --v3-storage /home/fleeky/pgv3-storage \
 *     [--dry-run] \
 *     [--repos repo1,repo2]
 *
 * v3 daemon MUST be stopped (RocksDB exclusive lock on the corestore).
 * Idempotent — re-running skips already-migrated entries.
 */

import fs from 'fs'
import path from 'path'
import Corestore from 'corestore'
import { IssueRegistry } from '../lib/issue-registry.js'
import { RepoEventLog } from '../lib/services/repo-event-log.js'

function parseArgs (argv) {
  const args = { inPath: null, v3Storage: null, dryRun: false, repos: null }
  for (let i = 2; i < argv.length; i++) {
    const a = argv[i]
    if (a === '--dry-run') args.dryRun = true
    else if (a === '--in') args.inPath = argv[++i]
    else if (a === '--v3-storage') args.v3Storage = argv[++i]
    else if (a === '--repos') args.repos = argv[++i].split(',').map(s => s.trim()).filter(Boolean)
  }
  if (!args.inPath) throw new Error('--in is required')
  if (!args.v3Storage) throw new Error('--v3-storage is required')
  return args
}

async function main () {
  const args = parseArgs(process.argv)
  console.log('[migrate] input     :', args.inPath)
  console.log('[migrate] v3 storage:', args.v3Storage)
  console.log('[migrate] dry run   :', args.dryRun)
  if (args.repos) console.log('[migrate] repos     :', args.repos.join(', '))

  const dump = JSON.parse(fs.readFileSync(args.inPath, 'utf8'))
  const entries = Object.entries(dump).filter(([r]) => !args.repos || args.repos.includes(r))

  let totalIssues = 0, totalComments = 0
  for (const [repo, b] of entries) {
    totalIssues += b.issues.length
    totalComments += b.comments.length
    console.log(`[migrate] ${repo}: ${b.issues.length} issues, ${b.comments.length} comments`)
  }
  console.log(`[migrate] total: ${totalIssues} issues, ${totalComments} comments across ${entries.length} repos`)

  if (args.dryRun) {
    console.log('[migrate] DRY RUN — no writes performed')
    return
  }

  const v3Store = new Corestore(path.join(args.v3Storage, 'store'))
  await v3Store.ready()
  const registry = new IssueRegistry(v3Store, {
    indexPath: path.join(args.v3Storage, 'issue-registry-index.json')
  })
  console.log('[migrate] v3 IssueRegistry opened')

  let wroteIssues = 0, wroteComments = 0
  for (const [repo, b] of entries) {
    const tracker = await registry.getForRepo(repo)

    b.issues.sort((a, x) => a.id - x.id)
    for (const v1 of b.issues) {
      await tracker.createWithId(v1.id, {
        title: v1.title || '(no title)',
        body: v1.body || '',
        author: v1.author || 'unknown',
        status: v1.status === 'closed' ? 'closed' : 'open',
        labels: Array.isArray(v1.labels) ? v1.labels : [],
        images: Array.isArray(v1.images) ? v1.images : [],
        createdAt: v1.createdAt,
        updatedAt: v1.updatedAt
      })
      wroteIssues++
    }

    b.comments.sort((a, x) => {
      if (a.issueId !== x.issueId) return a.issueId - x.issueId
      return (a.createdAt || 0) - (x.createdAt || 0)
    })
    for (const c of b.comments) {
      await tracker.commentWithId(c.issueId, c.commentId, {
        body: c.body || '',
        author: c.author || 'unknown',
        createdAt: c.createdAt
      })
      wroteComments++
    }

    console.log(`[migrate] ${repo}: wrote ${b.issues.length} issues + ${b.comments.length} comments`)
  }

  // ── Emit repo-created events so mirrors learn about the new trackers.
  //
  // The migration writes trackers directly via IssueRegistry, bypassing
  // the normal `create-repo` path that appends to RepoEventLog. Mirrors
  // (e.g. stortron) subscribe to this log through RepoSync — without
  // events, they never learn about migrated trackers and can't
  // replicate them. Idempotent: we read existing events first and skip
  // repos already announced so re-running the migration doesn't dupe.
  const log = new RepoEventLog({ corestore: v3Store })
  await log.ready()
  const alreadyAnnounced = new Set()
  if (log.length > 0) {
    for await (const entry of log.entries({ start: 0 })) {
      if (entry && entry.name) alreadyAnnounced.add(entry.name)
    }
  }
  let emitted = 0
  for (const [repo] of entries) {
    if (alreadyAnnounced.has(repo)) continue
    const tracker = await registry.getForRepo(repo)
    const keyHex = tracker.keyHex
    if (!keyHex) {
      console.warn(`[migrate] ${repo}: tracker has no keyHex; skipping event`)
      continue
    }
    await log.append({
      op: 0, // created
      name: repo,
      head: '',
      issueBootstrapKey: Buffer.from(keyHex, 'hex')
    })
    emitted++
  }
  console.log(`[migrate] appended ${emitted} repo-created event(s) to RepoEventLog (${log.length} total)`)
  try { await log.close() } catch {}

  console.log('[migrate] verifying counts...')
  let verifyIssues = 0, verifyComments = 0
  for (const [repo] of entries) {
    const tracker = await registry.getForRepo(repo)
    const list = await tracker.list({ status: 'all' })
    verifyIssues += list.length
    for (const issue of list) {
      const comments = await tracker.getComments(issue.id)
      verifyComments += comments.length
    }
  }
  console.log(`[migrate] v3 has ${verifyIssues} issues, ${verifyComments} comments post-migration`)
  if (verifyIssues === wroteIssues && verifyComments === wroteComments) {
    console.log('[migrate] counts match')
  } else {
    console.error(`[migrate] MISMATCH wrote=${wroteIssues}/${wroteComments} v3=${verifyIssues}/${verifyComments}`)
  }

  try { await v3Store.close() } catch {}
}

main().catch(err => {
  console.error('[migrate] ERROR:', err.message)
  console.error(err.stack)
  process.exit(1)
})