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:
| M | CHANGELOG.md | | | 8 | +++++++- | 
| M | README.md | | | 22 | +++++++++++++++++++--- | 
| A | export.js | | | 166 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | import.js | | | 102 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| M | index.js | | | 125 | +++++++++++++------------------------------------------------------------------ | 
| M | package-lock.json | | | 19 | +++++++++++++++++++ | 
| M | package.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",