├── .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 | `
`,
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 | ``,
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 | ``,
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 | ``,
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 | ``,
14 | `${title}
`)
15 | }
16 |
17 | function label(config, label) {
18 | return config.asText ? `\n\n ${label.name}` :
19 | renderInteractive(config,
20 | ``,
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 | 
4 | [](https://badge.fury.io/js/@4awpawz%2Fsnitch)
5 | [](https://github.com/picocss/pico/blob/master/LICENSE.md)
6 | [](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 | 
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 |
130 |
131 | ## Screencasts & Tutorials
132 |
133 | [](https://www.youtube.com/watch?v=CgvFShjjClY)
134 | [](https://www.youtube.com/watch?v=_vUUfBxtSFE)
135 | [](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 |
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 |
201 |
202 | Please 👀 watch and leave us a 🌟 star :)
203 |
--------------------------------------------------------------------------------