damus-github-export

Damus issue data exported from github
git clone git://jb55.com/damus-github-export
Log | Files | Refs | README | LICENSE

commit cc8bbeacb49fbb72d4cbffa68e4f5ef5a7c5ba31
parent 9da15c658bf6b36cac9deeefc1345eee19513264
Author: Gavin Rehkemper <gavinr@users.noreply.github.com>
Date:   Sat, 18 Apr 2020 21:09:41 -0500

Export Issues (#13)

export functionality
Diffstat:
MCHANGELOG.md | 8+++++++-
MREADME.md | 22+++++++++++++++++++---
Aexport.js | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aimport.js | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mindex.js | 125+++++++++++++------------------------------------------------------------------
Mpackage-lock.json | 19+++++++++++++++++++
Mpackage.json | 5+++--
7 files changed, 336 insertions(+), 111 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -4,6 +4,11 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [1.0.0] - 2020-04-18 + +## Added +- Exporting issues is now supported. Just call `githubCsvTools` with no file input. See `githubCsvTools --help` for more info. + ## [0.4.0] - 2020-04-18 ## Changed @@ -36,7 +41,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Basic CSV import functionality. - A few basic tests -[Unreleased]: https://github.com/gavinr/github-csv-tools/compare/v0.4.0...HEAD +[Unreleased]: https://github.com/gavinr/github-csv-tools/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/gavinr/github-csv-tools/compare/v0.4.0...v1.0.0 [0.4.0]: https://github.com/gavinr/github-csv-tools/compare/V0.3.0...v0.4.0 [0.3.0]: https://github.com/gavinr/github-csv-tools/compare/v0.2.0...V0.3.0 [0.2.0]: https://github.com/gavinr/github-csv-tools/compare/v0.1.0...v0.2.0 diff --git a/README.md b/README.md @@ -5,10 +5,26 @@ Currently imports title, description, labels, and milestones. ## Usage - 1. `npm install -g github-csv-tools` - 2. `githubCsvTools myFile.csv` +`githubCsvTools --help` for info on how to use the command line tool. -`githubCsvTools --help` for info on how to use the command line tool +First run `npm install -g github-csv-tools` to install. Then: + +### To Import Issues + +`githubCsvTools myFile.csv` + +### To Export Issues + +`githubCsvTools` + +### Tokens + +For all actions, you'll need a GitHub token: + +1. Go to https://github.com/settings/tokens +2. Click "Generate New Token" +3. Check on `repo` +4. Copy the token provided - this tool will ask for it. ## Development diff --git a/export.js b/export.js @@ -0,0 +1,166 @@ +// const csv = require("csv"); +const fs = require("fs"); +const converter = require("json-2-csv"); + +// Gets a single comment +const getComment = async (octokit, values, issueNumber) => { + return new Promise((resolve, reject) => { + const issueOptions = octokit.issues.listComments.endpoint.merge({ + owner: values.userOrOrganization, + repo: values.repo, + issue_number: issueNumber, + }); + octokit.paginate(issueOptions).then( + (commentsData) => { + resolve(commentsData); + }, + (err) => { + console.error(err); + reject(err); + } + ); + }); +}; + +// Given the full list of issues, returns back an array of all comments, +// each with the issue data also included. +const getFullCommentData = async (octokit, values, data) => { + const fullComments = []; + for (let i = 0; i < data.length; i++) { + const issueObject = data[i]; + fullComments.push({ + issue: issueObject, + }); + const commentsData = await getComment(octokit, values, issueObject.number); + commentsData.forEach((comment) => { + fullComments.push({ + issue: issueObject, + comment: { + user: comment.user.login, + created_at: comment.created_at, + updated_at: comment.updated_at, + body: comment.body, + }, + }); + }); + } + return fullComments; +}; + +const twoPadNumber = (number) => { + return String(number).padStart(2, "0"); +}; + +const exportIssues = (octokit, values, includeComments = false) => { + // Getting all the issues: + const options = octokit.issues.listForRepo.endpoint.merge({ + owner: values.userOrOrganization, + repo: values.repo, + state: "all", + }); + octokit.paginate(options).then( + async (data) => { + // Customized columns: + data.forEach(async (issueObject) => { + if (issueObject.user) { + issueObject.user = issueObject.user.login; + } + if (issueObject.assignee) { + issueObject.assignee = issueObject.assignee.login; + } + if (issueObject.labels) { + issueObject.labels = issueObject.labels + .map((labelObject) => { + return labelObject.name; + }) + .join(","); + } + if (issueObject.assignees) { + issueObject.assignees = issueObject.assignees + .map((assigneeObject) => { + return assigneeObject.login; + }) + .join(","); + } + }); + + // Data from the API that we're removing: + const columnsToRemove = [ + "url", + "repository_url", + "labels_url", + "comments_url", + "events_url", + "html_url", + "id", + "node_id", + // "number", + // "title", + + // "labels", + // "state", + "locked", + "assignee", + // "assignees", + // "milestone", + // "comments", + // "created_at", + // "updated_at", + // "closed_at", + "author_association", + "body", + "pull_request", + "milestone", + ]; + data.forEach((issueObject) => { + columnsToRemove.forEach((param) => { + delete issueObject[param]; + }); + }); + + let csvData = data; + if (values.exportComments === true) { + // If we want comments, replace the data that will get pushed into + // the CSV with our full comments data: + csvData = await getFullCommentData(octokit, values, data); + } + + converter.json2csv(csvData, (err, csvString) => { + if (err) { + console.error("error converting!"); + process.exit(0); + } + // console.log("csvString:", csvString); + const now = new Date(); + let fileName = `${now.getFullYear()}-${twoPadNumber( + now.getMonth() + )}-${twoPadNumber(now.getDate())}-${twoPadNumber( + now.getHours() + )}-${twoPadNumber(now.getMinutes())}-${twoPadNumber( + now.getSeconds() + )}-issues.csv`; + if (values.exportFileName) { + fileName = values.exportFileName; + } + fs.writeFile(fileName, csvString, "utf8", function (err) { + if (err) { + console.error("error writing csv!"); + process.exit(0); + } else { + console.log(`Success! check ${fileName}`); + console.log( + "❤ ❗ If this project has provided you value, please ⭐ star the repo to show your support: ➡ https://github.com/gavinr/github-csv-tools" + ); + process.exit(0); + } + }); + }); + }, + (err) => { + console.log("error", err); + process.exit(0); + } + ); +}; + +module.exports = { exportIssues }; diff --git a/import.js b/import.js @@ -0,0 +1,102 @@ +const csv = require("csv"); +const fs = require("fs"); + +const { createIssue } = require("./helpers.js"); + +const importFile = (octokit, file, values) => { + fs.readFile(file, "utf8", (err, data) => { + if (err) { + console.error("Error reading file."); + process.exit(1); + } + csv.parse( + data, + { + trim: true, + }, + (err, csvRows) => { + if (err) throw err; + var cols = csvRows[0]; + csvRows.shift(); + + // get indexes of the fields we need + var titleIndex = cols.indexOf("title"); + var bodyIndex = cols.indexOf("description"); + var labelsIndex = cols.indexOf("labels"); + var milestoneIndex = cols.indexOf("milestone"); + var assigneeIndex = cols.indexOf("assignee"); + var stateIndex = cols.indexOf("state"); + + if (titleIndex === -1) { + console.error("Title required by GitHub, but not found in CSV."); + process.exit(1); + } + const createPromises = csvRows.map((row) => { + var sendObj = { + owner: values.userOrOrganization, + repo: values.repo, + title: row[titleIndex], + }; + + // if we have a body column, pass that. + if (bodyIndex > -1) { + sendObj.body = row[bodyIndex]; + } + + // if we have a labels column, pass that. + if (labelsIndex > -1 && row[labelsIndex] !== "") { + sendObj.labels = row[labelsIndex].split(","); + } + + // if we have a milestone column, pass that. + if (milestoneIndex > -1 && row[milestoneIndex] !== "") { + sendObj.milestone = row[milestoneIndex]; + } + + // if we have an assignee column, pass that. + if (assigneeIndex > -1 && row[assigneeIndex] !== "") { + sendObj.assignees = row[assigneeIndex].replace(/ /g, "").split(","); + } + + // console.log("sendObj", sendObj); + let state = false; + if (stateIndex > -1 && row[stateIndex] === "closed") { + state = row[stateIndex]; + } + return createIssue(octokit, sendObj, state); + }); + + Promise.all(createPromises).then( + (res) => { + const successes = res.filter((cr) => { + return cr.status === 200 || cr.status === 201; + }); + const fails = res.filter((cr) => { + return cr.status !== 200 && cr.status !== 201; + }); + + console.log( + `Created ${successes.length} issues, and had ${fails.length} failures.` + ); + console.log( + "❤ ❗ If this project has provided you value, please ⭐ star the repo to show your support: ➡ https://github.com/gavinr/github-csv-tools" + ); + + if (fails.length > 0) { + console.log(fails); + } + + process.exit(0); + }, + (err) => { + console.error("Error"); + console.error(err); + process.exit(0); + } + ); + } + ); + }); +}; + +module.exports = { importFile }; diff --git a/index.js b/index.js @@ -7,22 +7,25 @@ const prompt = require("co-prompt"); const { Octokit } = require("@octokit/rest"); const { throttling } = require("@octokit/plugin-throttling"); -const { createIssue } = require("./helpers.js"); - -const csv = require("csv"); -const fs = require("fs"); +const { importFile } = require("./import.js"); +const { exportIssues } = require("./export.js"); program - .version("0.4.0") - .arguments("<file>") + .version("1.0.0") + .arguments("[file]") .option( - "--github_enterprise [https://api.github.my-company.com]", + "-g, --github_enterprise [https://api.github.my-company.com]", "Your GitHub Enterprise URL." ) .option( - "--token [token]", + "-t, --token [token]", "The GitHub token. https://github.com/settings/tokens" ) + .option( + "-f, --exportFileName [export.csv]", + "The name of the CSV you'd like to export to." + ) + .option("-c, --exportComments", "Include comments in the export.") .action(function (file, options) { co(function* () { var retObject = {}; @@ -34,6 +37,8 @@ program "token (get from https://github.com/settings/tokens): " ); } + retObject.exportFileName = options.exportFileName || false; + retObject.exportComments = options.exportComments || false; retObject.userOrOrganization = yield prompt("user or organization: "); retObject.repo = yield prompt("repo: "); return retObject; @@ -65,103 +70,13 @@ program }, }); - fs.readFile(file, "utf8", (err, data) => { - if (err) { - console.error("Error reading file."); - process.exit(1); - } - csv.parse( - data, - { - trim: true, - }, - (err, csvRows) => { - if (err) throw err; - var cols = csvRows[0]; - csvRows.shift(); - - // get indexes of the fields we need - var titleIndex = cols.indexOf("title"); - var bodyIndex = cols.indexOf("description"); - var labelsIndex = cols.indexOf("labels"); - var milestoneIndex = cols.indexOf("milestone"); - var assigneeIndex = cols.indexOf("assignee"); - var stateIndex = cols.indexOf("state"); - - if (titleIndex === -1) { - console.error( - "Title required by GitHub, but not found in CSV." - ); - process.exit(1); - } - const createPromises = csvRows.map((row) => { - var sendObj = { - owner: values.userOrOrganization, - repo: values.repo, - title: row[titleIndex], - }; - - // if we have a body column, pass that. - if (bodyIndex > -1) { - sendObj.body = row[bodyIndex]; - } - - // if we have a labels column, pass that. - if (labelsIndex > -1 && row[labelsIndex] !== "") { - sendObj.labels = row[labelsIndex].split(","); - } - - // if we have a milestone column, pass that. - if (milestoneIndex > -1 && row[milestoneIndex] !== "") { - sendObj.milestone = row[milestoneIndex]; - } - - // if we have an assignee column, pass that. - if (assigneeIndex > -1 && row[assigneeIndex] !== "") { - sendObj.assignees = row[assigneeIndex] - .replace(/ /g, "") - .split(","); - } - - // console.log("sendObj", sendObj); - let state = false; - if (stateIndex > -1 && row[stateIndex] === "closed") { - state = row[stateIndex]; - } - return createIssue(octokit, sendObj, state); - }); - - Promise.all(createPromises).then( - (res) => { - const successes = res.filter((cr) => { - return cr.status === 200 || cr.status === 201; - }); - const fails = res.filter((cr) => { - return cr.status !== 200 && cr.status !== 201; - }); - - console.log( - `Created ${successes.length} issues, and had ${fails.length} failures.` - ); - console.log( - "❤ ❗ If this project has provided you value, please ⭐ star the repo to show your support: ➡ https://github.com/gavinr/github-csv-tools" - ); - - if (fails.length > 0) { - console.log(fails); - } - - process.exit(0); - }, - (err) => { - console.error("Error"); - console.error(err); - process.exit(0); - } - ); - } - ); - }); + if (file) { + // This is an import! + importFile(octokit, file, values); + } else { + // this is an export! + exportIssues(octokit, values); + } }, function (err) { console.error("ERROR", err); diff --git a/package-lock.json b/package-lock.json @@ -415,6 +415,11 @@ "ms": "^2.1.1" } }, + "deeks": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/deeks/-/deeks-2.2.4.tgz", + "integrity": "sha512-T8HtfilZeYvVRmTx9J6Nk4e0ayb/RqHx/XBOJpUybQyedRRtVw0I21pezA+OjXoCnL5YkkjCAM2TFvSJt0NwWw==" + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -435,6 +440,11 @@ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" }, + "doc-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/doc-path/-/doc-path-2.0.2.tgz", + "integrity": "sha512-7KAEFeGh+xlqATVLXywN6g2U6RF7CW1la80BdtRF3BaiP1rz3r1UCewUeOnwpJ9A44LX66u32dYaU9zIqC1/TQ==" + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1255,6 +1265,15 @@ "esprima": "^4.0.0" } }, + "json-2-csv": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-3.6.2.tgz", + "integrity": "sha512-aJGagjPIZj9iqAO8UVcNggVuRpxbMvG3WspGntcGm1Kxb3TFCwWC4U36qoybVkkm7n4ivDaVjwBd4PPlQJkP3Q==", + "requires": { + "deeks": "2.2.4", + "doc-path": "2.0.2" + } + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", diff --git a/package.json b/package.json @@ -1,6 +1,6 @@ { "name": "github-csv-tools", - "version": "0.4.0", + "version": "1.0.0", "description": "Tools to import and export, via CSV, from GitHub.", "main": "index.js", "scripts": { @@ -24,7 +24,8 @@ "co": "^4.6.0", "co-prompt": "^1.0.0", "commander": "^5.0.0", - "csv": "^5.3.2" + "csv": "^5.3.2", + "json-2-csv": "^3.6.2" }, "devDependencies": { "eslint": "^6.8.0",