├── .gitignore ├── readme-assets ├── buymeacoffee.png ├── changelog-md.png ├── il_570xN.3603423963_tuhu.xcf ├── issues-missing-criteria-warning.png └── snitch-text-markdown-side-by-side.png ├── core ├── lib │ ├── escape.mjs │ ├── reportTypes.mjs │ ├── renderInteractive.mjs │ ├── formatIssue.mjs │ ├── constants.mjs │ ├── urls.mjs │ ├── reportAndExit.mjs │ ├── reportUnreportables.mjs │ └── showState.mjs ├── bin │ └── cli.mjs ├── services │ ├── gh.mjs │ ├── reports │ │ ├── issuesByLabelReport.mjs │ │ ├── issuesByMilestoneReport.mjs │ │ ├── issuesByAssigneeReport.mjs │ │ ├── issuesReport.mjs │ │ └── issuesByMilestoneAndLabelReport.mjs │ └── configure.mjs └── index.mjs ├── .eslintrc.cjs ├── package.json ├── CHANGELOG.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | examples 2 | node_modules 3 | test.* 4 | *.txt 5 | *.md 6 | !README.md 7 | !CHANGELOG.txt 8 | -------------------------------------------------------------------------------- /readme-assets/buymeacoffee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4awpawz/snitch/HEAD/readme-assets/buymeacoffee.png -------------------------------------------------------------------------------- /readme-assets/changelog-md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4awpawz/snitch/HEAD/readme-assets/changelog-md.png -------------------------------------------------------------------------------- /core/lib/escape.mjs: -------------------------------------------------------------------------------- 1 | export function escape(s) { 2 | return s.replaceAll("<", "<").replaceAll(">", ">") 3 | } 4 | -------------------------------------------------------------------------------- /readme-assets/il_570xN.3603423963_tuhu.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4awpawz/snitch/HEAD/readme-assets/il_570xN.3603423963_tuhu.xcf -------------------------------------------------------------------------------- /readme-assets/issues-missing-criteria-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4awpawz/snitch/HEAD/readme-assets/issues-missing-criteria-warning.png -------------------------------------------------------------------------------- /readme-assets/snitch-text-markdown-side-by-side.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4awpawz/snitch/HEAD/readme-assets/snitch-text-markdown-side-by-side.png -------------------------------------------------------------------------------- /core/lib/reportTypes.mjs: -------------------------------------------------------------------------------- 1 | export const reportTypes = [ 2 | "list", 3 | "milestone", 4 | "milestone-label", 5 | "label", 6 | "assignee" 7 | ] 8 | -------------------------------------------------------------------------------- /core/lib/renderInteractive.mjs: -------------------------------------------------------------------------------- 1 | export function renderInteractive(config, interactive, nonInteractive) { 2 | return config.nonInteractive ? nonInteractive : interactive 3 | } 4 | -------------------------------------------------------------------------------- /core/lib/formatIssue.mjs: -------------------------------------------------------------------------------- 1 | export function formatIssue({ state, number, title, labels, assignees, milestone }) { 2 | return `${state} ${number} ${title} ${labels} ${assignees} ${milestone}` 3 | } 4 | -------------------------------------------------------------------------------- /core/bin/cli.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import open from "open" 4 | import { snitch } from "../index.mjs" 5 | 6 | const args = process.argv.slice(2) 7 | const wantsHelp = ["help", "h", "-h", "--help"].includes(args[0]) 8 | wantsHelp && open("https://github.com/4awpawz/snitch") 9 | !wantsHelp && await snitch(args) 10 | -------------------------------------------------------------------------------- /core/lib/constants.mjs: -------------------------------------------------------------------------------- 1 | export const noMilestone = "No Milestone" 2 | export const noLabels = "No Labels" 3 | export const noAssignees = "No Assignees" 4 | export const noIssuesToReport = "No issues to report" 5 | export const attributionText = "This report was created with Snitch 👉" 6 | export const snitchUrl = "https://github.com/4awpawz/snitch" 7 | -------------------------------------------------------------------------------- /core/lib/urls.mjs: -------------------------------------------------------------------------------- 1 | export function labelUrl(config, label) { 2 | return `${config.repo}/labels/${label.name}` 3 | } 4 | 5 | export function milestoneUrl(config, milestone) { 6 | return `${config.repo}/milestone/${milestone.number}` 7 | } 8 | 9 | export function assigneeUrl(config, assignee) { 10 | return `${config.repo}/issues/${assignee.login}` 11 | } 12 | -------------------------------------------------------------------------------- /core/lib/reportAndExit.mjs: -------------------------------------------------------------------------------- 1 | import { chalkStderr } from "chalk" 2 | 3 | export function reportAndExit(message, level = "warn") { 4 | const msgLevel = level === "warn" && chalkStderr.bgYellowBright.black || level === "error" && chalkStderr.bgRed.black 5 | let msgOut = (level === "warn" ? "Attention: " : "Error: ") + message 6 | console.error(msgLevel(msgOut)) 7 | process.exit(1) 8 | } 9 | -------------------------------------------------------------------------------- /core/lib/reportUnreportables.mjs: -------------------------------------------------------------------------------- 1 | import { chalkStderr } from "chalk" 2 | 3 | export function reportUnreportables(config, issues, filter) { 4 | const count = issues.filter(issue => filter(issue)).length 5 | if (count === 0) return 6 | const preface = count === 1 ? "1 issue" : `${count} issues` 7 | console.error(chalkStderr.black.bgYellow(`${preface} out of a total of ${issues.length} do not meet the requirements for this ${config.reportName} report!`)) 8 | } 9 | -------------------------------------------------------------------------------- /core/lib/showState.mjs: -------------------------------------------------------------------------------- 1 | 2 | export function showState(config, issueState) { 3 | if (config.asText && issueState === "CLOSED") 4 | return "✓" 5 | if (config.asText && issueState === "OPEN") 6 | return "x" 7 | if (!config.asText && issueState === "CLOSED") 8 | return "" 9 | if (!config.asText && issueState === "OPEN") 10 | return "🆇" 11 | throw new TypeError(`invalid issue state, found ${issueState}`) 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "overrides": [ 8 | { 9 | "env": { 10 | "node": true 11 | }, 12 | "files": [ 13 | ".eslintrc.{js,cjs}" 14 | ], 15 | "parserOptions": { 16 | "sourceType": "script" 17 | } 18 | } 19 | ], 20 | "parserOptions": { 21 | "ecmaVersion": "latest", 22 | "sourceType": "module" 23 | }, 24 | "rules": { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@4awpawz/snitch", 3 | "version": "3.2.0", 4 | "description": "Snitch is a terminal utility that automates the reporting of GitHub repository issues via interactive and informative reports in both markdown and plain text.", 5 | "main": "core/index.mjs", 6 | "bin": { 7 | "snitch": "core/bin/cli.mjs" 8 | }, 9 | "type": "module", 10 | "scripts": {}, 11 | "keywords": [ 12 | "reports", 13 | "github", 14 | "cli", 15 | "gh", 16 | "issues", 17 | "list", 18 | "format", 19 | "markdown" 20 | ], 21 | "author": "4awpawz, aka Jeff Schwartz", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/4awpawz/snitch" 25 | }, 26 | "license": "MIT", 27 | "dependencies": { 28 | "chalk": "^5.3.0", 29 | "marked": "^12.0.2", 30 | "open": "^10.1.0" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^8.57.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/services/gh.mjs: -------------------------------------------------------------------------------- 1 | import cp from "node:child_process" 2 | 3 | const execSync = cp.execSync; 4 | 5 | /* 6 | * Get repo info for current project. 7 | * Only called when repo url isn't supplied by user. 8 | */ 9 | export function ghGetRepoInfo() { 10 | const command = "gh repo view --json 'url,name'" 11 | const stdout = execSync(command) 12 | return stdout 13 | } 14 | 15 | export function ghGetIssues(config) { 16 | let command = "gh" 17 | let args = `issue list -L ${config.maxIssues} --state ${config.state} --json 'number,title,labels,milestone,state,assignees,url'` 18 | args = config.repo ? args + ` -R ${config.repo}` : args 19 | args = config.label ? args + ` --label '${config.label}'` : args 20 | args = config.assignee ? args + ` --assignee '${config.assignee}'` : args 21 | args = config.milestone ? args + ` --milestone '${config.milestone}'` : args 22 | if (config.debug) { 23 | console.log("debug gh command: ", `${command} ${args}`) 24 | return 25 | } 26 | const gh = cp.spawnSync(command, [args], { shell: true }) 27 | return gh.stdout.toString() 28 | } 29 | -------------------------------------------------------------------------------- /core/index.mjs: -------------------------------------------------------------------------------- 1 | import { ghGetIssues } from "./services/gh.mjs" 2 | import { configure } from "./services/configure.mjs" 3 | import { issuesReport } from "./services/reports/issuesReport.mjs" 4 | import { issuesByMilestoneReport } from "./services/reports/issuesByMilestoneReport.mjs" 5 | import { issuesByMilestoneAndLabelReport } from "./services/reports/issuesByMilestoneAndLabelReport.mjs" 6 | import { issuesByLabelReport } from "./services/reports/issuesByLabelReport.mjs" 7 | import { issuesByAssigneeReport } from "./services/reports/issuesByAssigneeReport.mjs" 8 | import { renderInteractive } from "./lib/renderInteractive.mjs" 9 | import { attributionText, snitchUrl } from "./lib/constants.mjs" 10 | 11 | export async function snitch(args) { 12 | const config = await configure(args) 13 | if (config.debug) console.error("debug config: ", config) 14 | const result = ghGetIssues(config) 15 | if (args.includes("--debug")) process.exit(0) 16 | const issues = JSON.parse(result) 17 | let output = "" 18 | if (!config.noHeading && config.heading.length) output += 19 | config.asText ? `${config.heading}` : 20 | renderInteractive(config, 21 | `

${config.heading}

`, 22 | `

${config.heading}

`) 23 | switch (config.reportName) { 24 | case "list": 25 | output += issuesReport(config, issues) 26 | break 27 | case "milestone": 28 | output += issuesByMilestoneReport(config, issues) 29 | break 30 | case "milestone-label": 31 | output += issuesByMilestoneAndLabelReport(config, issues) 32 | break 33 | case "label": 34 | output += issuesByLabelReport(config, issues) 35 | break 36 | case "assignee": 37 | output += issuesByAssigneeReport(config, issues) 38 | break 39 | default: 40 | throw new TypeError(`invalid report type, you entered ${config.reportName}`) 41 | } 42 | if (!config.noAttribution && config.asText) output += "\n\n" 43 | if (!config.noAttribution) output += 44 | config.asText ? `| ${attributionText} @ ${snitchUrl}` : 45 | `
${attributionText}
` 46 | process.stdout.write(output) 47 | process.exit(0) 48 | } 49 | -------------------------------------------------------------------------------- /core/services/reports/issuesByLabelReport.mjs: -------------------------------------------------------------------------------- 1 | import { issuesReport } from "./issuesReport.mjs" 2 | import { reportAndExit } from "../../lib/reportAndExit.mjs" 3 | import { labelUrl } from "../../lib/urls.mjs" 4 | import { noIssuesToReport } from "../../lib/constants.mjs" 5 | import { renderInteractive } from "../../lib/renderInteractive.mjs" 6 | import { reportUnreportables } from "../../lib/reportUnreportables.mjs" 7 | 8 | function label(config, label) { 9 | return config.asText ? `\n\n${label.name}` : 10 | renderInteractive(config, 11 | `

${label.name}

`, 12 | `

${label.name}

`) 13 | } 14 | 15 | /* 16 | * Compose a reportable milestone with issues. 17 | */ 18 | function getReportableIssues(config, label, issues) { 19 | const selector = { showState: true, showLabels: false, showAssignees: true, showMilestones: true } 20 | const matches = issues.filter(issue => issue.labels.some(lbl => lbl.name === label.name)) 21 | const lss = issuesReport(config, matches, selector) 22 | return lss 23 | } 24 | 25 | /* 26 | * Compose a reportable milestone object from milestone properties. 27 | */ 28 | function mapReportableLabel(config, label, issues) { 29 | const lbl = {} 30 | lbl.name = label.name 31 | lbl.color = label.color 32 | lbl.issues = getReportableIssues(config, label, issues) 33 | return lbl 34 | } 35 | 36 | /* 37 | * Compose report from reportable milestone objects. 38 | */ 39 | function getReportableLabels(config, issues) { 40 | let labels = new Map() 41 | issues.forEach(issue => issue.labels.forEach(label => !labels.get(label.id) && labels.set(label.id, label))) 42 | labels = [...labels.values()] 43 | labels = labels.sort((a, b) => { 44 | if (a.name.toUpperCase() > b.name.toUpperCase()) return 1 45 | if (a.name.toUpperCase() < b.name.toUpperCase()) return -1 46 | return 0 47 | }) 48 | if (config.sortLabelsDescending) labels = labels.reverse(); 49 | return labels.map(label => mapReportableLabel(config, label, issues)) 50 | } 51 | 52 | /* 53 | * Generate report from reportable label objects. 54 | */ 55 | export function issuesByLabelReport(config, issues) { 56 | if (issues.length === 0) reportAndExit(noIssuesToReport) 57 | reportUnreportables(config, issues, issue => issue.labels.length === 0) 58 | const reportableLabels = getReportableLabels(config, issues) 59 | if (reportableLabels.length === 0) reportAndExit(noIssuesToReport) 60 | let output = "" 61 | for (let i = 0; i < reportableLabels.length; i++) { 62 | const reportableLabel = reportableLabels[i] 63 | let formattedOutput = "" 64 | formattedOutput += label(config, reportableLabel) 65 | formattedOutput += reportableLabel.issues 66 | output += formattedOutput 67 | } 68 | return output 69 | } 70 | -------------------------------------------------------------------------------- /core/services/configure.mjs: -------------------------------------------------------------------------------- 1 | import { ghGetRepoInfo } from "./gh.mjs" 2 | import { reportTypes } from "../lib/reportTypes.mjs" 3 | import { reportAndExit } from "../lib/reportAndExit.mjs" 4 | 5 | function repoURL(repo) { 6 | const protocolHost = "https://github.com/" 7 | if (repo.startsWith(protocolHost)) return repo 8 | return protocolHost + repo 9 | } 10 | 11 | export async function configure(args) { 12 | let config = {} 13 | const reportName = args.find(arg => arg.startsWith("--name=")) 14 | config.reportName = reportName?.split("=")[1] || "list" 15 | const repo = args.find(arg => arg.startsWith("--repo=")) 16 | config.repo = repo?.split("=")[1] || JSON.parse(ghGetRepoInfo()).url 17 | config.repo = repoURL(config.repo) 18 | const state = args.find(arg => arg.startsWith("--state=")) 19 | config.state = state?.split("=")[1] || "all" 20 | !["open", "closed", "all"].includes(config.state) && 21 | reportAndExit(`open, closed, all are the only valid states, you entered ${config.state}`, "error") 22 | const maxIssues = args.find(arg => arg.startsWith("--max-issues=")) 23 | config.maxIssues = maxIssues && parseInt(maxIssues.split("=")[1]) || 10000 24 | !Number.isInteger(config.maxIssues) && reportAndExit("max-issues must be an integer", "error") 25 | config.nonInteractive = args.includes("--non-interactive") 26 | config.noHeading = args.includes("--no-heading") 27 | const heading = args.find(arg => arg.startsWith("--heading=")) 28 | config.heading = heading?.split("=")[1] || (new URL(config.repo)).pathname.slice(1) 29 | config.debug = args.includes("--debug") 30 | config.noAttribution = args.includes("--no-attribution") 31 | config.asText = args.includes("--as-text") 32 | config.blankLines = args.includes("--blank-lines") 33 | // filtering 34 | const label = args.find(arg => arg.startsWith("--label=")) 35 | config.label = label?.split("=")[1] || "" 36 | const assignee = args.find(arg => arg.startsWith("--assignee=")) 37 | config.assignee = assignee?.split("=")[1] || "" 38 | const milestone = args.find(arg => arg.startsWith("--milestone=")) 39 | config.milestone = milestone?.split("=")[1] || "" 40 | // sorting 41 | config.sortIssuesAscending = args.includes("--sort-issues-ascending") 42 | config.sortMilestonesDescending = (config.reportName === "milestone" || config.reportName === "milestone-label") && args.includes("--sort-milestones-descending") 43 | config.sortAssigneesDescending = config.reportName === "assignee" && args.includes("--sort-assignees-descending") 44 | config.sortLabelsDescending = (config.reportName === "milestone-label" || config.reportName === "label") && args.includes("--sort-labels-descending") 45 | // prompt user when they enter an invalid report type 46 | if (!config.debug && !reportTypes.includes(config.reportName)) { 47 | console.error("------------------") 48 | console.error("Pick A Report Type") 49 | console.error("------------------") 50 | console.error(reportTypes.join("\n")) 51 | reportAndExit("invalid or missing report type, please provide one from the list above", "error") 52 | } 53 | return config 54 | } 55 | -------------------------------------------------------------------------------- /core/services/reports/issuesByMilestoneReport.mjs: -------------------------------------------------------------------------------- 1 | import { issuesReport } from "./issuesReport.mjs" 2 | import { reportAndExit } from "../../lib/reportAndExit.mjs" 3 | import { noIssuesToReport } from "../../lib/constants.mjs" 4 | import { reportUnreportables } from "../../lib/reportUnreportables.mjs" 5 | import { renderInteractive } from "../../lib/renderInteractive.mjs" 6 | import { milestoneUrl } from "../../lib/urls.mjs" 7 | 8 | function milestone(config, _milestone) { 9 | let title = _milestone.title 10 | title = _milestone.dueOn ? `${title} (${_milestone.dueOn.substring(0, 10)})` : title 11 | return config.asText ? `\n\n${title}` : 12 | renderInteractive(config, 13 | `

${title}

`, 14 | `

${title}

`) 15 | } 16 | 17 | /* 18 | * Compose a reportable milestone with issues. 19 | */ 20 | function getReportableIssues(config, milestone, issues) { 21 | const selector = { showState: true, showLabels: true, showAssignees: true, showMilestones: false } 22 | const iss = issues.filter(issue => issue.milestone?.number === milestone.number) 23 | const lss = issuesReport(config, iss, selector) 24 | return lss 25 | } 26 | 27 | /* 28 | * Compose a reportable milestone object from milestone properties. 29 | */ 30 | function mapReportableMilestone(config, _milestone, issues) { 31 | const ms = {} 32 | ms.title = milestone(config, _milestone) 33 | ms.number = _milestone.number 34 | ms.dueOn = Object.hasOwn(_milestone, "dueOn") && _milestone.dueOn || null 35 | ms.issues = getReportableIssues(config, ms, issues) 36 | return ms 37 | } 38 | 39 | /* 40 | * Compose report from reportable milestone objects. 41 | */ 42 | function getReportableMilestones(config, issues) { 43 | let ms = new Map() 44 | issues.forEach(issue => !ms.has(issue.milestone.number) && ms.set(issue.milestone.number, issue.milestone)) 45 | ms = [...ms.values()].sort((a, b) => { 46 | if (a.title.toUpperCase() > b.title.toUpperCase()) return 1 47 | if (a.title.toUpperCase() < b.title.toUpperCase()) return -1 48 | return 0 49 | }) 50 | if (config.sortMilestonesDescending) ms = ms.reverse() 51 | return ms.map(milestone => mapReportableMilestone(config, milestone, issues)) 52 | } 53 | 54 | /* 55 | * Generate report from reportable milestone objects. 56 | */ 57 | export function issuesByMilestoneReport(config, issues) { 58 | if (issues.length === 0) reportAndExit(noIssuesToReport) 59 | reportUnreportables(config, issues, issue => issue.milestone === null) 60 | const reportableIssues = issues.filter(issue => issue.milestone !== null) 61 | if (reportableIssues.length === 0) reportAndExit(noIssuesToReport) 62 | const reportableMilestones = getReportableMilestones(config, reportableIssues) 63 | if (reportableMilestones.length === 0) reportAndExit(noIssuesToReport) 64 | let output = "" 65 | for (let i = 0; i < reportableMilestones.length; i++) { 66 | const reportableMilestone = reportableMilestones[i] 67 | let formattedOutput = "" 68 | formattedOutput += reportableMilestone.title 69 | formattedOutput += reportableMilestone.issues 70 | // formattedOutput += !config.blankLines && config.asText && "\n" || "" 71 | output += formattedOutput 72 | } 73 | return output 74 | } 75 | -------------------------------------------------------------------------------- /core/services/reports/issuesByAssigneeReport.mjs: -------------------------------------------------------------------------------- 1 | import { issuesReport } from "./issuesReport.mjs" 2 | import { reportAndExit } from "../../lib/reportAndExit.mjs" 3 | import { noIssuesToReport } from "../../lib/constants.mjs" 4 | import { reportUnreportables } from "../../lib/reportUnreportables.mjs" 5 | import { renderInteractive } from "../../lib/renderInteractive.mjs" 6 | import { assigneeUrl } from "../../lib/urls.mjs" 7 | 8 | function assignee(config, _assignee) { 9 | return config.asText ? `\n\n${_assignee.name}` : renderInteractive(config, 10 | `

${_assignee.name}

`, 11 | `

${_assignee.name}

` 12 | ) 13 | } 14 | 15 | /* 16 | * Compose a reportable milestone with issues. 17 | */ 18 | function getReportableIssues(config, _assignee, issues) { 19 | const selector = { showState: true, showLabels: true, showAssignees: false, showMilestones: true } 20 | const matches = issues.filter(issue => issue.assignees.some(assignee => assignee.id === _assignee.id)) 21 | const lss = issuesReport(config, matches, selector) 22 | return lss 23 | } 24 | 25 | /* 26 | * Compose a reportable milestone object from milestone properties. 27 | */ 28 | function mapReportableAssignee(config, assignee, issues) { 29 | const asgn = {} 30 | asgn.id = assignee.id 31 | asgn.login = assignee.login 32 | asgn.name = assignee.name 33 | asgn.issues = getReportableIssues(config, asgn, issues) 34 | return asgn 35 | } 36 | 37 | /* 38 | * Compose report from reportable milestone objects. 39 | */ 40 | function getReportableAssignees(config, issues) { 41 | let assignees = new Map() 42 | issues.forEach(issue => issue.assignees.forEach(assignee => assignee.name !== "" && !assignees.get(assignee.id) && assignees.set(assignee.id, assignee))) 43 | assignees = [...assignees.values()].sort((a, b) => { 44 | if (a.name.toUpperCase() > b.name.toUpperCase()) return 1 45 | if (a.name.toUpperCase() < b.name.toUpperCase()) return -1 46 | return 0 47 | }) 48 | if (config.sortAssigneesDescending) assignees = assignees.reverse(); 49 | return assignees.map(assignee => mapReportableAssignee(config, assignee, issues)) 50 | } 51 | 52 | /* 53 | * Generate report from reportable label objects. 54 | */ 55 | export function issuesByAssigneeReport(config, issues) { 56 | if (issues.length === 0) reportAndExit(noIssuesToReport) 57 | reportUnreportables(config, issues, issue => issue.assignees.length === 0 || 58 | issue.assignees.length === issue.assignees.filter(assignee => assignee.name === "").length) 59 | const reportableIssues = issues.filter(issue => issue.assignees.length !== 0 && issue.assignees.some(assignee => assignee.name !== "")) 60 | if (reportableIssues.length === 0) reportAndExit(noIssuesToReport) 61 | const reportableAssignees = getReportableAssignees(config, reportableIssues) 62 | if (reportableAssignees.length === 0) reportAndExit(noIssuesToReport) 63 | let output = "" 64 | for (let i = 0; i < reportableAssignees.length; i++) { 65 | const reportableAssignee = reportableAssignees[i] 66 | let formattedOutput = "" 67 | formattedOutput += assignee(config, reportableAssignee) 68 | formattedOutput += reportableAssignee.issues 69 | output += formattedOutput 70 | } 71 | return output 72 | } 73 | -------------------------------------------------------------------------------- /core/services/reports/issuesReport.mjs: -------------------------------------------------------------------------------- 1 | import { reportAndExit } from "../../lib/reportAndExit.mjs" 2 | import { showState } from "../../lib/showState.mjs" 3 | import { labelUrl, milestoneUrl, assigneeUrl } from "../../lib/urls.mjs" 4 | import { noIssuesToReport, noMilestone, noLabels, noAssignees } from "../../lib/constants.mjs" 5 | import { formatIssue } from "../../lib/formatIssue.mjs" 6 | import { marked } from "marked" 7 | import { escape } from "../../lib/escape.mjs" 8 | import { renderInteractive } from "../../lib/renderInteractive.mjs" 9 | 10 | function assignees(config, assignees) { 11 | if (assignees.length === 0) return `[ ${noAssignees} ]` 12 | let asgns = assignees.map(assignee => 13 | config.asText ? assignee.name : 14 | renderInteractive(config, 15 | `${assignee.name}`, 16 | assignee.name)).join(", ") 17 | return `[ ${asgns} ]` 18 | } 19 | 20 | function labels(config, labels) { 21 | if (!labels.length) return `[ ${noLabels} ]` 22 | let lbls = labels.map(label => 23 | config.asText ? label.name : 24 | renderInteractive(config, 25 | `${label.name}`, 26 | `${label.name}`)) 27 | return `[ ${lbls.join(", ")} ]` 28 | } 29 | 30 | function milestone(config, milestone) { 31 | if (!milestone) return `${noMilestone}` 32 | let msName = milestone.title 33 | msName += milestone.dueOn ? 34 | ` (${milestone.dueOn.substring(0, 10)})` : 35 | "" 36 | return config.asText ? msName : renderInteractive(config, 37 | `${msName}`, 38 | msName) 39 | } 40 | 41 | function number(number) { 42 | return `#${number.toString()}:` 43 | } 44 | 45 | function title(config, title, url, number) { 46 | const ttl = config.asText ? title : marked.parseInline(escape(title)) 47 | return config.asText ? ttl : renderInteractive(config, `${ttl}`, title) 48 | } 49 | 50 | function state(config, state) { 51 | return showState(config, state) 52 | } 53 | 54 | /* 55 | * Compose a reportable issue object from issue properties. 56 | */ 57 | function mapReportableIssue(config, issue) { 58 | const is = {} 59 | // issue state 60 | is.state = state(config, issue.state) 61 | // issue number 62 | is.number = number(issue.number) 63 | // issue title 64 | is.title = title(config, issue.title, issue.url, issue.number) 65 | // labels 66 | is.labels = labels(config, issue.labels) 67 | // assignees 68 | is.assignees = assignees(config, issue.assignees) 69 | // milestone 70 | is.milestone = milestone(config, issue.milestone) 71 | return is 72 | } 73 | 74 | /* 75 | * Compose report from reportable issue objects. 76 | */ 77 | function getReportableIssues(config, issues) { 78 | return issues.map(issue => mapReportableIssue(config, issue)) 79 | } 80 | 81 | /* 82 | * Generate report from reportable issue objects. 83 | */ 84 | export function issuesReport(config, issues, opts = { showState: true, showLabels: true, showAssignees: true, showMilestones: true }) { 85 | if (issues.length === 0) reportAndExit(noIssuesToReport) 86 | let reportableIssues = getReportableIssues(config, issues) 87 | if (reportableIssues.length === 0) reportAndExit(noIssuesToReport) 88 | if (config.sortIssuesAscending) reportableIssues = reportableIssues.reverse() 89 | let output = "\n\n" 90 | for (let i = 0; i < reportableIssues.length; i++) { 91 | const reportableIssue = reportableIssues[i] 92 | output += formatIssue({ 93 | state: opts.showState ? reportableIssue.state : "", 94 | number: reportableIssue.number, 95 | title: reportableIssue.title, 96 | labels: opts.showLabels ? reportableIssue.labels : "", 97 | assignees: opts.showAssignees ? reportableIssue.assignees : "", 98 | milestone: opts.showMilestones ? reportableIssue.milestone : "" 99 | }) 100 | if (i < reportableIssues.length - 1 && config.blankLines && config.asText) output += "\n\n" 101 | if (i < reportableIssues.length - 1 && config.blankLines && !config.asText) output += "
\n
" 102 | if (i < reportableIssues.length - 1 && !config.blankLines && config.asText) output += "\n" 103 | if (i < reportableIssues.length - 1 && !config.blankLines && !config.asText) output += "
" 104 | } 105 | return output 106 | } 107 | -------------------------------------------------------------------------------- /core/services/reports/issuesByMilestoneAndLabelReport.mjs: -------------------------------------------------------------------------------- 1 | import { issuesReport } from "./issuesReport.mjs" 2 | import { reportAndExit } from "../../lib/reportAndExit.mjs" 3 | import { noIssuesToReport } from "../../lib/constants.mjs" 4 | import { labelUrl, milestoneUrl } from "../../lib/urls.mjs" 5 | import { reportUnreportables } from "../../lib/reportUnreportables.mjs" 6 | import { renderInteractive } from "../../lib/renderInteractive.mjs" 7 | 8 | function milestone(config, _milestone) { 9 | let title = _milestone.title 10 | title = _milestone.dueOn ? `${title} (${_milestone.dueOn.substring(0, 10)})` : title 11 | return config.asText ? `\n\n${title}` : 12 | renderInteractive(config, 13 | `

${title}

`, 14 | `

${title}

`) 15 | } 16 | 17 | function label(config, label) { 18 | return config.asText ? `\n\n ${label.name}` : 19 | renderInteractive(config, 20 | `

${label.name}

`, 21 | `

${label.name}

`) 22 | } 23 | 24 | /* 25 | * Compose a reportable milestone's labels with issues. 26 | */ 27 | function getReportableIssues(config, milestone, label, issues) { 28 | const selector = { showState: true, showLabels: false, showAssignees: true, showMilestones: false } 29 | const milestoneLabelIssues = [] 30 | issues.forEach(issue => 31 | issue.milestone && issue.milestone.number === milestone.number && issue.labels.some(issueLabel => issueLabel.id === label.id && milestoneLabelIssues.push(issue))) 32 | const lss = issuesReport(config, milestoneLabelIssues, selector) 33 | return lss 34 | } 35 | 36 | /* 37 | * Compose a reportable milestone with labels and their matching issues. 38 | */ 39 | function getReportableLabels(config, milestone, issues) { 40 | let labels = new Map() 41 | issues.forEach(issue => issue.milestone.number === milestone.number && 42 | issue.labels.forEach(label => !labels.get(label.id) && labels.set(label.id, label))) 43 | labels = [...labels.values()].sort((a, b) => { 44 | if (a.name.toUpperCase() > b.name.toUpperCase()) return 1 45 | if (a.name.toUpperCase() < b.name.toUpperCase()) return -1 46 | return 0 47 | }) 48 | if (config.sortLabelsDescending) labels = labels.reverse(); 49 | labels.forEach(label => label.issues = getReportableIssues(config, milestone, label, issues)) 50 | return labels 51 | } 52 | 53 | /* 54 | * Compose a reportable milestone object from milestone properties. 55 | */ 56 | function mapReportableMilestone(config, _milestone, issues) { 57 | const ms = {} 58 | ms.title = milestone(config, _milestone) 59 | ms.number = _milestone.number 60 | ms.dueOn = Object.hasOwn(_milestone, "dueOn") && _milestone.dueOn || null 61 | ms.labels = getReportableLabels(config, ms, issues) 62 | return ms 63 | } 64 | 65 | /* 66 | * Compose report from reportable milestone objects. 67 | */ 68 | function getReportableMilestones(config, issues) { 69 | let ms = new Map() 70 | issues.forEach(issue => !ms.has(issue.milestone.number) && ms.set(issue.milestone.number, issue.milestone)) 71 | ms = [...ms.values()].sort((a, b) => { 72 | if (a.title.toUpperCase() > b.title.toUpperCase()) return 1 73 | if (a.title.toUpperCase() < b.title.toUpperCase()) return -1 74 | return 0 75 | }) 76 | if (config.sortMilestonesDescending) ms = ms.reverse() 77 | return ms.map(milestone => mapReportableMilestone(config, milestone, issues)) 78 | } 79 | 80 | /* 81 | * Generate report from reportable milestone objects. 82 | */ 83 | export function issuesByMilestoneAndLabelReport(config, issues) { 84 | if (issues.length === 0) reportAndExit(noIssuesToReport) 85 | reportUnreportables(config, issues, issue => issue.milestone === null || issue.labels.length === 0) 86 | const reportableIssues = issues.filter(issue => issue.milestone !== null && issue.labels.length > 0) 87 | if (reportableIssues.length === 0) reportAndExit(noIssuesToReport) 88 | const reportableMilestones = getReportableMilestones(config, reportableIssues) 89 | let output = "" 90 | for (let i = 0; i < reportableMilestones.length; i++) { 91 | const reportableMilestone = reportableMilestones[i] 92 | let formattedOutput = "" 93 | formattedOutput += reportableMilestone.title 94 | for (let ii = 0; ii < reportableMilestone.labels.length; ii++) { 95 | const reportableMilestoneLabel = reportableMilestone.labels[ii] 96 | formattedOutput += label(config, reportableMilestoneLabel) 97 | formattedOutput += reportableMilestoneLabel.issues 98 | } 99 | output += formattedOutput 100 | } 101 | return output 102 | } 103 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | 3 | ✓ #69: Update docs for v3.1.0. [ documentation, enhancement ] [ Jeff Schwartz ] v3.1.0 4 | ✓ #67: Support previewing reports in the browser. [ enhancement, wontfix, requires further analysis ] [ Jeff Schwartz ] Next Release 5 | ✓ #66: Issues are not rendered properly by tne Vim/NeoVim "markdown preview" plugin. [ wontfix ] [ Jeff Schwartz ] v3.1.0 6 | ✓ #65: Update docs to reflect v3.0.0. [ documentation ] [ Jeff Schwartz ] v3.0.0 (2024-06-28) 7 | ✓ #64: Make blank lines between issues optional. [ enhancement, breaking change ] [ Jeff Schwartz ] v3.0.0 (2024-06-28) 8 | ✓ #62: Open link to snitch repo from cli help using default browser. [ enhancement ] [ Jeff Schwartz ] v3.0.0 (2024-06-28) 9 | ✓ #61: Text in warning messages is hard to read. [ bug ] [ Jeff Schwartz ] v3.0.0 (2024-06-28) 10 | ✓ #59: Support text-based reporting. [ enhancement ] [ Jeff Schwartz ] v3.0.0 (2024-06-28) 11 | ✓ #58: "love" in 'If Using Snitch Provides You Value Then Please Show Some love' should be capitalized. [ bug, documentation ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 12 | ✓ #57: Documentation for debug mode has conflicting state. [ bug, documentation ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 13 | ✓ #56: npm badge broken. [ bug ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 14 | ✓ #55: Release badge broken. [ bug ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 15 | ✓ #54: Link image to YouTube video demo in README.md. [ enhancement ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 16 | ✓ #53: Add attribution to bottom of reports with opt out option. [ enhancement ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 17 | ✓ #52: Add title attribute to all anchor tags. [ enhancement ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 18 | ✓ #51: Sort milestone report in ascending order. [ enhancement ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 19 | ✓ #50: Support reports with non interactive issues. [ enhancement ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 20 | ✓ #49: Support reports with no heading. [ enhancement ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 21 | ✓ #48: Issues should be fully responsive when rendered. [ enhancement ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 22 | ✓ #47: Drop support for text based reports. [ refactor ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 23 | ✓ #45: When reporting issues by assignee, asignees whose names are blank (i.e., "") should be ignored. [ enhancement ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 24 | ✓ #44: Assignees are not sorted in the assignees report. [ bug ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 25 | ✓ #43: Replace exec with spawn when calling gh to retrieve a list of issues. [ enhancement ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 26 | ✓ #42: Rename and rebrand project to 'snitch'. [ refactor, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 27 | ✓ #41: Titles with HTML and code blocks break formatting. [ bug ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 28 | ✓ #40: Create 2 new reports, issues by label and issues by assignee. [ requires further analysis, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 29 | ✓ #39: Refactor project according to new specifications. [ refactor, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 30 | ✓ #38: Indent labels in markdown to align with title. [ enhancement ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 31 | ✓ #37: Preview reports in the browser after generation. [ wontfix ] [ No Assignees ] Next Release 32 | ✓ #36: Provide list of available reports when user enters invalid report type. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 33 | ✓ #35: Extraneous blank lines have resurfaced. [ bug, regression, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 34 | ✓ #34: Issue titles are still causing wrapping and need to be cropped more. [ bug, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 35 | ✓ #32: Replace "--report-" with "--issues-" for all report types. [ documentation, enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 36 | ✓ #31: Issues with long titles cause lines to wrap. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 37 | ✓ #30: Add option to force line break after title. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 38 | ✓ #28: The issue title in text files is wrapped in an anchor tag. [ bug, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 39 | ✓ #27: Default to --report-list-txt when user supplies an unrecognized report type. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 40 | ✓ #26: Optionally support colorized labels and marks in markdown with a single option. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 41 | ✓ #24: In md reports, optionally make the issue title a link to the Github issue. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 42 | ✓ #22: Named reports. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 43 | ✓ #20: Optionally show marks for issue state. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 44 | ✓ #18: Filter issues by label, assignee and milestone. [ enhancement ] [ Jeff Schwartz ] v3.1.0 45 | ✓ #17: Sanitize issues that are missing critical information. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 46 | ✓ #16: Optionally report issue state and assignees. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 47 | ✓ #15: Rewrite README.md to reflect the state of v2.0.0. [ documentation, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 48 | ✓ #14: A blank line should only be added if it is configured and only for txt-based reports. [ bug, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 49 | ✓ #13: More than one blank line surrounds milestones and labels and blank lines appear at the end of reports. [ bug, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 50 | ✓ #12: Labels aren't colorized in the milestone-label report when the option to colorize labels is provided. [ bug, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 51 | ✓ #11: Restructure application. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 52 | ✓ #10: list-report issue items should include milestone title and date due if available in gh payload. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 53 | ✓ #9: Run gh in a child process. [ documentation, enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 54 | ✓ #7: Check for missing milestones only when the selected report includes milestones. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 55 | ✓ #6: If no heading is supplied a blank line is generated at the top of the report. [ bug, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 56 | ✓ #4: All reports and options to be supported in v2.0.0. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 57 | ✓ #3: Optional handling of missing issue data for labels and milestones. [ enhancement, v2.0.0 ] [ Jeff Schwartz ] v2.0.0 (2024-03-31) 58 | 59 | | This report was created with Snitch 👉 @ https://github.com/4awpawz/snitch -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snitch 👉 2 | 3 | ![GitHub Release](https://img.shields.io/github/release/4awpawz/snitch/all.svg) 4 | [![npm version](https://badge.fury.io/js/@4awpawz%2Fsnitch.svg)](https://badge.fury.io/js/@4awpawz%2Fsnitch) 5 | [![License](https://img.shields.io/badge/license-MIT-%230172ad)](https://github.com/picocss/pico/blob/master/LICENSE.md) 6 | [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/4awpawz.svg?style=social&label=Follow%20%404awpawz)](https://twitter.com/4awpawz) 7 | 8 | ## Automated GitHub Issues Reporting 9 | 10 | Five different report styles to chose from in either markdown or plain text. 11 | 12 | ![Snitch markdown and text reports](./readme-assets/snitch-text-markdown-side-by-side.png) 13 | 14 | ⚠️ This project was formerly named _ghif_ but as of v2 has diverged significantly enough from that codebase to warrant rebranding while maintaining all its previous git history. 15 | 16 | ## What's New 17 | 18 | - Snitch v3.2.0 adds support for overriding how issues, labels, assignees, and milestones are sorted, giving you even greater control over how you tailor your reports. See [Options](#options) below for details. 19 | - Snitch v3.1.0 added support for filtering issues by label, by assignee and by milestone. See [Options](#options) below for details. 20 | 21 | ## Installation 22 | 23 | ⚠️ Snitch requires [GitHub CLI ](https://cli.github.com) and [Node.js](https://nodejs.org/en). 24 | 25 | To install Snitch with NPM, please run the following command in your terminal: 26 | 27 | ```shell 28 | > npm i -g 4awpawz/snitch 29 | ``` 30 | 31 | ## 5 Reports To Chose From 32 | 33 | | Report Name | Description | Example | 34 | | :-- | :-- | :-- | 35 | | assignee | a list of issues by assignee | `> snitch --name=assignee > snitch-report.md` | 36 | | label | a list of issues by label | `> snitch --name=label > snitch-report.md` | 37 | | list | a list of issues | `> snitch --name=list > snitch-report.md` | 38 | | milestone | a list of issues by milestone | `> snitch --name=milestone > snitch-report.md` | 39 | | milestone-label | a list of issues by milestone and label | `> snitch --name=milestone-label > snitch-report.md` | 40 | 41 | ## Options 42 | 43 | | Option | Description | Default (if omitted)| Example | 44 | | :-- | :-- | :-- | :-- | 45 | | --name=[list \| milestone \| milestone-label \| label \| assignee] | name of report to generate | list | `--name=milestone-label` | 46 | | --repo=[path to repository] | path to Github repository | the GitHub repository associated with the current project determined by git remote origin | `--repo=4awpawz/snitch` | 47 | | --as-text (v3.0.0) | output report as plain text | output report as markdown | `--as-text` | 48 | | --heading=[report heading] | the heading for the report | repository name | `--heading=CHANGELOG` | 49 | | --no-heading (v3.0.0) | omit heading | include heading | `--no-heading` | 50 | | --blank-lines (v3.0.0) | seperate issues with a blank line | no seperating blank line | `--blank-lines` | 51 | | --non-interactive | for markdown reports only, generate non interactive issues | generate interactive issues | `--non-interactive` | 52 | | --no-attribution | attribution is jnot appended to the report | attribution is appended to the report | `--no-attribution` | 53 | | --max-issues=integer | maximum number of issues to report | 10000 | `--max-issues=100000` | 54 | | --state=[all \| open \| closed] | filter issues by state | all | `--state=closed` | 55 | | --label=\ (v3.1.0) | filter issues by one or more labels | no filtering by label | `--label=bug` | 56 | | --assignee=\ (v3.1.0) | filter issues by assignee | no filtering by assignee | `--assignee=supercoder` | 57 | | --milestone=\ (v3.1.0) | filter issues by milestone | no filtering by milestone | `--milestone=v10.6.20` | 58 | | --sort-issues-ascending (v3.2.0) | sort issues in reverse order, see [below](#sorting) for details | the default sort order is descending | `--sort-issues-ascending` | 59 | | --sort-labels-descending (v3.2.0) | sort labels in reverse order, see [below](#sorting) for details | the default sort order is ascending | `--sort-labels-descending` | 60 | | --sort-assignees-descending (v3.2.0) | sort assigness in reverse order, see [below](#sorting) for details | the default sort order is ascending | `--sort-assignees-descending` | 61 | | --sort-milestones-descending (v3.2.0) | sort milestones in reverse order, see [below](#sorting) for details | the default sort order is ascending | `--sort-milestones-descending` | 62 | | --debug | run in debug mode, see [below](#debug-mode) for details | run in normal mode | `--debug` | 63 | 64 | ## Saving output to a file 65 | 66 | Use redirection (i.e., `>`) to save output to a file: 67 | 68 | ```shell 69 | > snitch --name=list > list.md 70 | ``` 71 | ## Sorting 72 | 73 | The table below lists the sort ordering options available for each report: 74 | 75 | | Report | Applicable | 76 | | :-- | :-- | 77 | | list | --sort-issues-ascending | 78 | | milestone | --sort-issues-ascending, --sort-milestones-descending | 79 | | milestone-label | --sort-issues-ascending, --sort-milestones-descending, --sort-labels-descending | 80 | | assignee | --sort-issues-ascending, --sort-assignees-descending | 81 | | label | --sort-issues-ascending, --sort-labels-descending | 82 | 83 | ## Debug mode 84 | 85 | You can run Snitch in __debug mode__ to expose the dynamically generated configuration data that would be used during the processing of the payload returned from __GitHub's CLI__ utility as well as the command line that would be used to invoke __GitHub CLI__ itself. This information would be useful when submitting an issue or for your own problem resolution. 86 | 87 | To invoke debug mode, append `--debug` to the command line that you would use to generate your desired report, such as the __list report__ in the command below: 88 | 89 | ```shell 90 | > snitch --name=list --debug 91 | ``` 92 | 93 | The output from running Snitch in debug mode would look similar to the following: 94 | 95 | ```shell 96 | debug config: { 97 | reportName: 'list', 98 | repo: 'https://github.com/4awpawz/snitch', 99 | state: 'all', 100 | maxIssues: 10000, 101 | nonInteractive: false, 102 | noHeading: false, 103 | heading: '4awpawz/snitch', 104 | debug: true, 105 | noAttribution: false, 106 | asText: false, 107 | blankLines: false, 108 | label: '', 109 | assignee: '', 110 | milestone: '', 111 | sortIssuesAscending: false, 112 | sortMilestonesDescending: false, 113 | sortAssigneesDescending: false, 114 | sortLabelsDescending: false 115 | } 116 | debug gh command: gh issue list -L 10000 --state all --json 'number,title,labels,milestone,state,assignees,url' -R https://github.com/4awpawz/snitch 117 | ``` 118 | 119 | You can also run the _debug gh command_ to examine the JSON payload returned by GitHub's _gh_ utility: 120 | 121 | ```shell 122 | > gh issue list -L 10000 --state all --json 'number,title,labels,milestone,state,assignees,url' -R https://github.com/4awpawz/snitch 123 | ``` 124 | 125 | ## Report Sensitivity 126 | 127 | When generating a report other than the list report you might see a warning message like the one below. It is informing you that some issues were excluded from the report because they didn't meet the report's requirements. For example, if you generate a milestone report and there are issues that haven't been assigned a milestone then those issues will be excluded from the report. 128 | 129 | missing criteria warning message 130 | 131 | ## Screencasts & Tutorials 132 | 133 | [![New features introduced in Snitch v3.1 and v3.2](https://img.youtube.com/vi/CgvFShjjClY/0.jpg)](https://www.youtube.com/watch?v=CgvFShjjClY) 134 | [![Introducing Snitch v3.0.0](https://img.youtube.com/vi/_vUUfBxtSFE/0.jpg)](https://www.youtube.com/watch?v=_vUUfBxtSFE) 135 | [![Snitch milestone report video](https://img.youtube.com/vi/u-7oJJUUdGs/0.jpg)](https://www.youtube.com/watch?v=u-7oJJUUdGs) 136 | 137 | ### Example - Easily Create Your Project's Changelog 138 | 139 | `> snitch --name=list --state=closed --heading=CHANGELOG` 140 | 141 | changelog report image 142 |
143 |
144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | ## Request a new report format 187 | 188 | Have an idea for a report format that is not yet supported? Then by all means [please submit a request](https://github.com/4awpawz/snitch/issues) and provide a detailed description of the report you are seeking. 189 | 190 | ## Known Issues 191 | 192 | If you are a Vim or a Neovim user and you are using the _markdown-preview_ plugin to preview markdown then please be aware that it can render markdown incorrectly. Unfortunately, the plugin doesn't currently seem to be actively maintained. For more information, [please see this issue](https://github.com/iamcco/markdown-preview.nvim/issues/681). 193 | 194 | ## License 195 | 196 | MIT 197 | 198 | ## If Using Snitch Provides You Value Then Please Show Some Love ❤️ 199 | 200 | image 201 | 202 | Please 👀 watch and leave us a 🌟 star :) 203 | --------------------------------------------------------------------------------