OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / cli / commands / issue.js

266 lines · javascript ref:
/**
 * pgh issue commands
 *
 * Subcommands: list, create, view, edit, close, reopen, comment, label
 */

import { table, jsonOut, log, error } from '../output.js'
import { getRepo, getNamespace } from '../config.js'

function resolveRepo (flags) {
  const repo = getRepo(flags)
  if (!repo) error('Repository required. Use --repo <name> or set defaultRepo in config.')
  return repo
}

function resolveNs (flags) {
  return getNamespace(flags) || '_'
}

export async function issueCommand (client, sub, args, flags) {
  switch (sub) {
    case 'list':
    case 'ls':
      return issueList(client, flags)
    case 'create':
    case 'new':
      return issueCreate(client, args.join(' '), flags)
    case 'view':
    case 'show':
      return issueView(client, args[0], flags)
    case 'edit':
      return issueEdit(client, args[0], flags)
    case 'close':
      return issueUpdate(client, args[0], 'closed', flags)
    case 'reopen':
      return issueUpdate(client, args[0], 'open', flags)
    case 'comment':
      return issueComment(client, args[0], flags)
    case 'label':
      return issueLabel(client, args, flags)
    default:
      issueHelp()
  }
}

async function issueList (client, flags) {
  const repo = resolveRepo(flags)
  const ns = resolveNs(flags)

  const params = new URLSearchParams()
  if (flags.status) params.set('status', flags.status)
  if (flags.label) params.set('label', flags.label)
  if (flags.author) params.set('author', flags.author)

  const qs = params.toString()
  const data = await client.get(`/${ns}/${repo}/issues${qs ? '?' + qs : ''}`, {
    Accept: 'application/json'
  })

  if (flags.json) {
    jsonOut(data)
    return
  }

  const issues = data.issues || []
  if (issues.length === 0) {
    log('No issues.')
    if (data.counts) {
      log(`  Open: ${data.counts.open}  Closed: ${data.counts.closed}  Total: ${data.counts.total}`)
    }
    return
  }

  table(issues, [
    { key: 'number', label: '#', width: 5 },
    { key: 'title', label: 'TITLE', width: 50 },
    { key: 'status', label: 'STATUS', width: 8 },
    { key: 'labels', label: 'LABELS', width: 20 }
  ])

  if (data.counts) {
    log(`\nOpen: ${data.counts.open}  Closed: ${data.counts.closed}  Total: ${data.counts.total}`)
  }
}

async function issueCreate (client, title, flags) {
  if (!title && !flags.title) error('Usage: pgh issue create <title> [-d description]')
  const repo = resolveRepo(flags)
  const ns = resolveNs(flags)

  const body = {
    title: flags.title || title,
    description: flags.description || flags.d || ''
  }

  const data = await client.post(`/${ns}/${repo}/issues`, body)

  if (flags.json) {
    jsonOut(data)
    return
  }

  if (data.issue) {
    log(`Issue #${data.issue.number} created: ${data.issue.title}`)
  }
}

async function issueView (client, number, flags) {
  if (!number) error('Usage: pgh issue view <number>')
  const repo = resolveRepo(flags)
  const ns = resolveNs(flags)

  const data = await client.get(`/${ns}/${repo}/issues/${number}`, {
    Accept: 'application/json'
  })

  if (flags.json) {
    jsonOut(data)
    return
  }

  const issue = data.issue || data
  log(`#${issue.number}: ${issue.title}`)
  log(`Status: ${issue.status}`)
  if (issue.labels && issue.labels.length > 0) {
    log(`Labels: ${issue.labels.join(', ')}`)
  }
  log(`Created: ${new Date(issue.createdAt).toLocaleString()}`)
  if (issue.createdBy) log(`Author: ${issue.createdBy}`)
  log('')
  if (issue.description) {
    log(issue.description)
  } else {
    log('No description.')
  }

  if (issue.comments && issue.comments.length > 0) {
    log(`\n--- Comments (${issue.comments.length}) ---\n`)
    for (const c of issue.comments) {
      const author = c.author || c.userId || 'Anonymous'
      const date = new Date(c.createdAt).toLocaleString()
      log(`#${c.id} ${author} (${date}):`)
      log(`  ${c.text}`)
      log(`  Link: issues/${issue.number}#comment-${c.id}`)
      log('')
    }
  }
}

async function issueUpdate (client, number, status, flags) {
  if (!number) error(`Usage: pgh issue ${status === 'closed' ? 'close' : 'reopen'} <number>`)
  const repo = resolveRepo(flags)
  const ns = resolveNs(flags)

  const data = await client.patch(`/${ns}/${repo}/issues/${number}`, { status })

  if (flags.json) {
    jsonOut(data)
    return
  }

  log(`Issue #${number} ${status === 'closed' ? 'closed' : 'reopened'}`)
}

async function issueEdit (client, number, flags) {
  if (!number) error('Usage: pgh issue edit <number> --title "..." --description "..."')
  const title = flags.title || flags.t
  const description = flags.description || flags.d
  if (!title && !description) error('Provide --title and/or --description to edit')
  const repo = resolveRepo(flags)
  const ns = resolveNs(flags)

  const body = {}
  if (title) body.title = title
  if (description) body.description = description

  const data = await client.patch(`/${ns}/${repo}/issues/${number}`, body)

  if (flags.json) {
    jsonOut(data)
    return
  }

  const parts = []
  if (title) parts.push('title')
  if (description) parts.push('description')
  log(`Issue #${number} updated (${parts.join(', ')})`)
}

async function issueComment (client, number, flags) {
  if (!number) error('Usage: pgh issue comment <number> -m <message>')
  const message = flags.m || flags.message
  if (!message) error('Comment message required. Use -m <message>')
  const repo = resolveRepo(flags)
  const ns = resolveNs(flags)

  const data = await client.post(`/${ns}/${repo}/issues/${number}/comments`, {
    text: message
  })

  if (flags.json) {
    jsonOut(data)
    return
  }

  const commentId = data.comment && data.comment.id
  log(`Comment added to issue #${number}`)
  if (commentId) {
    log(`Link: issues/${number}#comment-${commentId}`)
  }
}

async function issueLabel (client, args, flags) {
  const action = args[0]
  const number = args[1]
  const labelName = args[2]

  if (!action || !number || !labelName) {
    error('Usage: pgh issue label <add|rm> <number> <label>')
  }

  const repo = resolveRepo(flags)
  const ns = resolveNs(flags)

  if (action === 'add') {
    await client.post(`/${ns}/${repo}/issues/${number}/labels`, {
      labels: [labelName]
    })
    log(`Label "${labelName}" added to issue #${number}`)
  } else if (action === 'rm' || action === 'remove') {
    await client.del(`/${ns}/${repo}/issues/${number}/labels/${encodeURIComponent(labelName)}`)
    log(`Label "${labelName}" removed from issue #${number}`)
  } else {
    error('Unknown label action. Use "add" or "rm".')
  }
}

export function issueHelp () {
  log(`Usage: pgh issue <subcommand> [args] [flags]

Subcommands:
  list                       List issues
  create <title>             Create a new issue
  view <number>              View issue details
  close <number>             Close an issue
  reopen <number>            Reopen an issue
  comment <number> -m <msg>  Add comment to issue
  label add <number> <name>  Add label to issue
  label rm <number> <name>   Remove label from issue

Flags:
  --repo <name>              Repository name
  --namespace <ns>           Namespace (default: _)
  --status <open|closed|all> Filter by status
  --label <name>             Filter by label
  --author <id>              Filter by author
  -d, --description <text>   Issue description (for create)
  -m, --message <text>       Comment text
  --title <text>             Issue title (for create)
  --json                     Output as JSON

Comment linking:
  Each comment has a unique ID shown as #N in view output.
  Direct link: /issues/<number>#comment-<id>`)
}