Conversion to promises and general cleanup.
authorStepan Riha <github@nonplus.net>
Fri, 23 Oct 2015 22:06:09 +0000 (17:06 -0500)
committerStepan Riha <github@nonplus.net>
Fri, 23 Oct 2015 22:06:09 +0000 (17:06 -0500)
README.md
m2gl.js
package.json

index 8ce9577..4b867b2 100755 (executable)
--- a/README.md
+++ b/README.md
-# YouTrack2GitLab
-Import YouTrack issues into GitLab.
+# Mantis2GitLab
+
+Import Mantis issues into GitLab.
 
 ## Install
 
 ```
-npm install -g youtrack2gitlab
+npm install -g mantis2gitlab
 ```
 
 ## Usage
 
 ```
-yt2gl -i <input> -u <users> -g <gitlaburl> -p <project> -t <token>
+m2gl -i options
 ```
 
 ## Options
 
 ```
--i, --input
- CSV file exported from YouTrack (Example: issues.csv)
+  -i, --input      CSV file exported from Mantis (Example: issues.csv)               [required]
+  -c, --config     Configuration file (Example: config.json)                         [required]
+  -g, --gitlaburl  GitLab URL hostname (Example: https://gitlab.com)                 [required]
+  -p, --project    GitLab project name including namespace (Example: mycorp/myproj)  [required]
+  -t, --token      An admin user's private token (Example: a2r33oczFyQzq53t23Vj)     [required]
+  -s, --sudo       The username performing the import (Example: bob)                 [required]
+  -f, --from       The first issue # to import (Example: 123)                      
+```
+
+## Config File
+
+In order to correctly map Mantis attributes you should create a JSON file and specify it with the **-c** switch.
+
+### Users
+
+This section maps Mantis `username` (Reporter, Assigned To, etc.) to a corresponding GitLab user name.
+
+```
+{
+  "users": {
+    "mantisUserName1": {
+      "gl_username": "GitLabUserName1"
+    },
+    "mantisUserName2": {
+      "gl_username": "GitLabUserName2"
+    }
+  }
+}
+```
+
+### Mantis URL (optional)
+
+This setting defines the URL to the old mantis installation.  When specified, Mantis cases imported in GitLab
+will contain a back-link to their corresponding Mantis issue.
+
+```
+{
+  "mantisUrl": "https://www.oldserver.com/mantis"
+}
+```
+
+### Category Labels (optional)
+
+This section maps Mantis Categories to corresponding GitLab labels.
+
+```
+{
+  "category_labels": {
+    "Admin UI": "area:Admin",
+    "Voter UI": "area:Voter",
+    "Server": "area:Service"
+    }
+}
+```
+
+### Priority Labels (optional)
+
+This section maps Mantis Priorities to corresponding GitLab labels.
+Note that the numeric priorities are used when exporting from SQL.
+
+```
+{
+  "priority_labels": {
+    "20": "priority:low",
+    "low": "priority:low",
+    "40": "priority:high",
+    "high": "priority:high",
+    "50": "priority:urgent",
+    "urgent": "priority:urgent",
+    "60": "priority:immediate",
+    "immediate": "priority:immediate"
+  }
+}
+```
 
--u, --users
- User mapping file (Example: users.json)
+### Severity Labels (optional)
 
--g, --gitlaburl
- GitLab URL hostname (Example: gitlab.example.com)
+This section maps Mantis Severities to corresponding GitLab labels.
+Note that the numeric severities are used when exporting from SQL.
 
--p, --project
- GitLab project name including namespace (Example: mycorp/myproj)
+```
+{
+  "severity_labels": {
+       "10": "severity:feature",
+       "feature": "severity:feature",
+       "20": "severity:trivial",
+       "trivial": "severity:trivial",
+       "30": "severity:text",
+       "text": "severity:text",
+       "40": "severity:tweak",
+       "tweak": "severity:tweak",
+       "50": "severity:minor",
+       "minor": "severity:minor",
+       "60": "severity:major",
+       "major": "severity:major",
+       "70": "severity:crash",
+       "crash": "severity:crash",
+       "80": "severity:block",
+       "block": "severity:block"
+  }
+}
+```
+
+### Closed Statuses (optional)
 
--t, --token
- An admin user's private token (Example: a2r33oczFyQzq53t23Vj)
+This section maps which Mantis Statuses indicate that the issue is closed.
+Note that the numeric severities are used when exporting from SQL.
+
+```
+{
+  "closed_statuses": {
+       "80": true,
+       "resolved": true,
+       "90": true,
+       "closed": true
+  }
+}
 ```
 
-## User Mapping File
-In order to correctly map users you should create a JSON file with the following format and specify it with the **-u** switch:
+## Exporting From Mantis
+
+The input to this script is a CSV file with the following columns:
+
+  * `Id` - Will create a corresponding GitLab *Issue*
+  * `Summary` - Will create a corresponding GitLab *Title* 
+  * `Category` - Will create a corresponding GitLab *Label* from `config.category_labels[Category]` 
+  * `Priority` - Will create a corresponding GitLab *Label* from `config.priority_labels[Priority]` 
+  * `Severity` - Will create a corresponding GitLab *Label* from `config.severity_labels[Severity]` 
+  * `Created` - Will be included in the *Description* header
+  * `Updated` - Will be included in the *Description* header, if different from `Created`
+  * `Reporter` - Will be included in the *Description* header
+  * `Assigned To` - Will be included in the *Description* header
+  * `Description` - Will be included in the *Description*
+  * `Info` - Will be appended the *Description*
+  * `Notes` - Will be split on `"$$$$"` and appended the *Description*
+
+### Exporting from Mantis UI
+
+You can export a summary of the Mantis issues from the _View Issues_ page by clicking on the _Export CSV_ button.
+
+**Note:** This export will only include a subset of the issues and is not the recommended approach.
+
+### Exporting from database
+
+The following SQL query pulls all the supported columns from the Mantis database. Make sure you specify the correct `PROJECT_NAME`:
 
 ```
-[
-  {
-    "yt_username": "USER'S USERNAME IN YOUTRACK",
-    "yt_name": "USER'S NAME IN YOUTRACK",
-    "gl_username": "USER'S USERNAME IN GITLAB",
-    "gl_private_token": "USER'S PRIVATE TOKEN IN GITLAB"
-  },
-  …
-]
+SELECT
+       bug.id as Id,
+       project.name as Project,
+       bug.category as Category,
+       bug.summary as Summary,
+       bug.priority as Priority,
+       bug.severity as Severity,
+       bug.status as Status,
+       bug.date_submitted as Created,
+       bug.last_updated as Updated,
+       reporter.username as Reporter,
+       handler.username as "Assigned To",
+       bug_text.description as Description,
+       bug_text.additional_information as Info,
+       GROUP_CONCAT(
+                               CONCAt('*', bugnote.date_submitted, ' - ', note_reporter.username, '*
+
+', bugnote_text.note)
+                               ORDER BY bugnote.Id
+                               SEPARATOR '$$$$'
+                       ) as Notes
+FROM
+       mantis_bug_table as bug
+       JOIN mantis_project_table project ON bug.project_id = project.id
+       JOIN mantis_bug_text_table bug_text ON bug.bug_text_id = bug_text.id
+       JOIN mantis_user_table as reporter ON bug.reporter_id = reporter.id
+       LEFT OUTER JOIN mantis_user_table as handler ON bug.handler_id = handler.id
+       LEFT OUTER JOIN mantis_bugnote_table as bugnote ON bugnote.bug_id = bug.id
+       LEFT OUTER JOIN mantis_bugnote_text_table as bugnote_text ON bugnote.bugnote_text_id = bugnote_text.id
+       LEFT OUTER JOIN mantis_user_table as note_reporter ON bugnote.reporter_id = note_reporter.id
+WHERE
+       project.name = 'PROJECT_NAME'
+GROUP BY bug.id
+ORDER BY bug.id
 ```
 
 ## Notes
 - Make sure the input CSV file only includes issues for the project you want to import.
-- Make sure that all users have write access to the specified repository or some issues will fail to import. A safer approach is to set repository's **Visibility Level** to **Public** and revert it when the import process is complete.
 - In version 6.4.3, GitLab API does not support setting creation date of issues. So all imported issues will have a creation time of now.
 - In version 6.4.3, GitLab API fails to import issues with very long titles.
 - In version 6.4.3, GitLab does not allow issues to be deleted. So be careful when importing issues into an active project.
@@ -60,14 +212,15 @@ In order to correctly map users you should create a JSON file with the following
        + Initial release
 
 ## Author
-**Soheil Rashidi**
+**Stepan Riha**
 
-+ http://soheilrashidi.com
-+ http://twitter.com/soheilpro
-+ http://github.com/soheilpro
++ http://github.com/nonplus
 
 ## Copyright and License
-Copyright 2014 Soheil Rashidi
+
+Based on https://github.com/soheilpro/youtrack2gitlab
+
+Copyright 2015 Stepan Riha
 
 Licensed under the The MIT License (the "License");
 you may not use this work except in compliance with the License.
diff --git a/m2gl.js b/m2gl.js
index 042fa7e..67c0a3b 100755 (executable)
--- a/m2gl.js
+++ b/m2gl.js
@@ -1,10 +1,11 @@
 #!/usr/bin/env node
 
-var fs = require('fs');
+var Q = require('q');
+var FS = require('q-io/fs');
 var util = require('util');
 var colors = require('colors');
 var csv = require('csv');
-var rest = require('restler');
+var rest = require('restler-q');
 var async = require('async');
 var _ = require('lodash');
 var argv = require('optimist')
@@ -15,215 +16,326 @@ var argv = require('optimist')
     .alias('p', 'project')
     .alias('t', 'token')
     .alias('s', 'sudo')
+    .alias('f', 'from')
     .describe('i', 'CSV file exported from Mantis (Example: issues.csv)')
     .describe('c', 'Configuration file (Example: config.json)')
     .describe('g', 'GitLab URL hostname (Example: https://gitlab.com)')
     .describe('p', 'GitLab project name including namespace (Example: mycorp/myproj)')
     .describe('t', 'An admin user\'s private token (Example: a2r33oczFyQzq53t23Vj)')
     .describe('s', 'The username performing the import (Example: bob)')
+    .describe('f', 'The first issue # to import (Example: 123)')
     .argv;
 
 var inputFile = __dirname + '/' + argv.input;
 var configFile = __dirname + '/' + argv.config;
+var fromIssueId = Number(argv.from||0);
 var gitlabAPIURLBase = argv.gitlaburl + '/api/v3';
 var gitlabProjectName = argv.project;
 var gitlabAdminPrivateToken = argv.token;
 var gitlabSudo = argv.sudo;
 var config = {};
 
-getGitLabProject(gitlabProjectName, gitlabAdminPrivateToken, function(error, project) {
-  if (error) {
-    console.error('Error: Cannot get list of projects from gitlab: ' + gitlabAPIURLBase);
-    return;
-  }
-
-  if (!project) {
-    console.error('Error: Cannot find GitLab project: ' + gitlabProjectName);
-    return;
-  }
-
-  getGitLabUsers(gitlabAdminPrivateToken, function(error, gitlabUsers) {
-    if (error) {
-      console.error('Error: Cannot get list of users from gitlab: ' + gitlabAPIURLBase);
-      return;
-    }
-
-    getConfig(configFile, function(error, cfg) {
-      if (error) {
-        console.error('Error: Cannot read config file: ' + configFile);
-        return;
-      }
-
-      config = cfg;
-
-      var users = config.users;
-
-      setGitLabUserIds(users, gitlabUsers);
+var gitLab = {};
+var promise = getConfig()
+        .then(readMantisIssues)
+        .then(getGitLabProject)
+        .then(getGitLabProjectMembers)
+        .then(mapGitLabUserIds)
+        .then(validateMantisIssues)
+        .then(getGitLabProjectIssues)
+        .then(importGitLabIssues)
+    ;
+
+promise.then(function() {
+  console.log(("Done!").bold.green);
+}, function(err) {
+  console.error(err);
+});
+
+/**
+ * Read and parse config.json file - assigns config
+ */
+function getConfig() {
+  log_progress("Reading configuration...");
+  return FS.read(configFile, {encoding: 'utf8'})
+      .then(function(data) {
+        var config = JSON.parse(data);
+        config.users = _.extend({
+          "": {
+            name: "Unknown",
+            gl_username: gitlabSudo
+          }
+        }, config.users);
+        return config;
+      }).then(function(cfg) {
+        config = cfg;
+      }, function() {
+        throw new Error('Cannot read config file: ' + configFile);
+      });
+}
 
-      readRows(inputFile, function(error, rows) {
-        if (error) {
-          console.error('Error: Cannot read input file: ' + inputFile);
-          return;
-        }
+/**
+ * Read and parse import.csv file - assigns gitLab.mantisIssues
+ */
+function readMantisIssues() {
+  log_progress("Reading Mantis export file...");
+  return FS.read(inputFile, {encoding: 'utf8'}).then(function(data) {
+    var rows = [];
+    var dfd = Q.defer();
 
-        validate(rows, users, function(missingUsernames, missingNames) {
-          if (missingUsernames.length > 0 || missingNames.length > 0) {
-            for (var i = 0; i < missingUsernames.length; i++)
-              console.error('Error: Cannot map Mantis user with username: ' + missingUsernames[i]);
+    csv().from(data, {delimiter: ',', escape: '"', columns: true})
+        .on('record', function(row, index) { rows.push(row) })
+        .on('end', function(error, data) {
+          dfd.resolve(rows);
+        });
 
-            for (var i = 0; i < missingNames.length; i++)
-              console.error('Error: Cannot map Mantis user with name: ' + missingNames[i]);
+    return dfd.promise
+        .then(function(rows) {
+          _.forEach(rows, function(row) {
+            row.Id = Number(row.Id);
+          });
 
-            return;
+          if(fromIssueId) {
+            rows = _.filter(rows, function(row) {
+              return row.Id >= fromIssueId;
+            })
           }
 
-          rows = _.sortBy(rows, function(row) { return Date.parse(row.Created); });
-
-          async.eachSeries(rows, function(row, callback) {
-            var issueId = row.Id;
-            var title = row.Summary;
-            var description = getDescription(row);
-            var assignee = getUserByMantisUsername(users, row["Assigned To"]);
-            var milestoneId = '';
-            var labels = getLabels(row);
-            var author = getUserByMantisUsername(users, row.Reporter);
-
-            insertIssue(project.id, title, description, assignee && assignee.gl_id, milestoneId, labels, author.gl_username, gitlabAdminPrivateToken, function(error, issue) {
-              setTimeout(callback, 1000);
-
-              if (error) {
-                console.error((issueId + ': Failed to insert.').red, error);
-                return;
-              }
-
-              if (isClosed(row)) {
-                closeIssue(issue, assignee.gl_private_token || gitlabAdminPrivateToken, function(error) {
-                  if (error)
-                    console.warn((issueId + ': Inserted successfully but failed to close. #' + issue.iid).yellow);
-                  else
-                    console.error((issueId + ': Inserted and closed successfully. #' + issue.iid).green);
-                });
-
-                return;
-              }
-
-              console.log((issueId + ': Inserted successfully. #' + issue.iid).green);
-            });
-          });
+          return gitLab.mantisIssues = _.sortBy(rows, "Id");
+        }, function(error) {
+          throw new Error('Cannot read input file: ' + inputFile + " - " + error);
         });
-      });
-    });
   });
-})
+}
 
-function getGitLabProject(name, privateToken, callback) {
+/**
+ * Fetch project info from GitLab - assigns gitLab.project
+ */
+function getGitLabProject() {
+  log_progress("Fetching project from GitLab...");
   var url = gitlabAPIURLBase + '/projects';
-  var data = { per_page: 100, private_token: privateToken, sudo: gitlabSudo };
+  var data = { per_page: 100, private_token: gitlabAdminPrivateToken, sudo: gitlabSudo };
 
-  rest.get(url, {data: data}).on('complete', function(result, response) {
-    if (util.isError(result)) {
-      callback(result);
-      return;
-    }
+  return rest.get(url, {data: data}).then(function(result) {
+
+    gitLab.project = _.find(result, { path_with_namespace : gitlabProjectName }) || null;
 
-    if (response.statusCode != 200) {
-      callback(result);
-      return;
+    if (!gitLab.project) {
+      throw new Error('Cannot find GitLab project: ' + gitlabProjectName);
     }
 
-    for (var i = 0; i < result.length; i++) {
-      if (result[i].path_with_namespace === name) {
-        callback(null, result[i]);
-        return;
-      }
-    };
+    return gitLab.project;
+  }, function(error) {
+    throw new Error('Cannot get list of projects from gitlab: ' + url);
+  });
+}
 
-    callback(null, null);
+/**
+ * Fetch project members from GitLab - assigns gitLab.gitlabUsers
+ */
+function getGitLabProjectMembers() {
+  log_progress("getGitLabProjectMembers");
+  var url = gitlabAPIURLBase + '/projects/' + gitLab.project.id + "/members";
+  var data = { per_page: 100, private_token: gitlabAdminPrivateToken, sudo: gitlabSudo };
+
+  return rest.get(url, {data: data}).then(function(result) {
+    return gitLab.gitlabUsers = result;
+  }, function(error) {
+    throw new Error('Cannot get list of users from gitlab: ' + url);
   });
 }
 
-function getGitLabUsers(privateToken, callback) {
-  var url = gitlabAPIURLBase + '/users';
-  var data = { per_page: 100, private_token: privateToken, sudo: gitlabSudo };
+/**
+ * Sets config.users[].gl_id based gitLab.gitlabUsers
+ */
+function mapGitLabUserIds() {
+  var users = config.users,
+      gitlabUsers = gitLab.gitlabUsers;
+  _.forEach(users, function(user) {
+    user.gl_id = (_.find(gitlabUsers, { id: user.gl_username }) || {}).id;
+  });
+}
 
-  rest.get(url, {data: data}).on('complete', function(result, response) {
-    if (util.isError(result)) {
-      callback(result);
-      return;
-    }
+/**
+ * Ensure that Mantise user names in gitLab.mantisIssues have corresponding GitLab user mapping
+ */
+function validateMantisIssues() {
+  log_progress("Validating Mantis Users...");
 
-    if (response.statusCode != 200) {
-      callback(result);
-      return;
-    }
+  var mantisIssues = gitLab.mantisIssues;
+  var users = config.users;
 
-    callback(null, result);
-  });
-}
+  var missingUsernames = [];
 
-function getConfig(configFile, callback) {
-  fs.readFile(configFile, {encoding: 'utf8'}, function(error, data) {
-    if (error) {
-      callback(error);
-      return;
-    }
+  for (var i = 0; i < mantisIssues.length; i++) {
+    var assignee = mantisIssues[i]["Assigned To"];
 
-    var config = JSON.parse(data);
-    config.users = config.users || [];
+    if (!getUserByMantisUsername(assignee) && missingUsernames.indexOf(assignee) == -1)
+      missingUsernames.push(assignee);
+  }
 
-    callback(null, config);
-  });
-}
+  for (var i = 0; i < mantisIssues.length; i++) {
+    var reporter = mantisIssues[i].Reporter;
 
-function setGitLabUserIds(users, gitlabUsers) {
-  for (var i = 0; i < users.length; i++) {
-    for (var j = 0; j < gitlabUsers.length; j++) {
-      if (users[i].gl_username === gitlabUsers[j].username) {
-        users[i].gl_id = gitlabUsers[j].id;
-        break;
-      }
-    }
+    if (!getUserByMantisUsername(reporter) && missingUsernames.indexOf(reporter) == -1)
+      missingUsernames.push(reporter);
   }
-}
 
-function readRows(inputFile, callback) {
-  fs.readFile(inputFile, {encoding: 'utf8'}, function(error, data) {
-    if (error) {
-      callback(error);
-      return;
-    }
+  if (missingUsernames.length > 0) {
+    for (var i = 0; i < missingUsernames.length; i++)
+      console.error('Error: Cannot map Mantis user with username: ' + missingUsernames[i]);
 
-    var rows = [];
+    throw new Error("User Validation Failed");
+  }
+}
+
+/**
+ * Import gitLab.mantisIssues into GitLab
+ * @returns {*}
+ */
+function importGitLabIssues() {
+  log_progress("Importing Mantis issues into GitLab from #" + fromIssueId + " ...");
+  return _.reduce(gitLab.mantisIssues, function(p, mantisIssue) {
+    return p.then(function() {
+      return importIssue(mantisIssue);
+    });
+  }, Q());
 
-    csv().from(data, {delimiter: ',', escape: '"', columns: true})
-    .on('record', function(row, index) { rows.push(row) })
-    .on('end', function() { callback(null, rows) });
-  });
 }
 
-function validate(rows, users, callback) {
-  var missingUsername = [];
-  var missingNames = [];
+function importIssue(mantisIssue) {
+  var issueId = mantisIssue.Id;
+  var title = mantisIssue.Summary;
+  var description = getDescription(mantisIssue);
+  var assignee = getUserByMantisUsername(mantisIssue["Assigned To"]);
+  var milestoneId = '';
+  var labels = getLabels(mantisIssue);
+  var author = getUserByMantisUsername(mantisIssue.Reporter);
 
-  for (var i = 0; i < rows.length; i++) {
-    var assignee = rows[i]["Assigned To"];
+  log_progress("Importing: #" + issueId + " - " + title + " ...");
 
-    if (!getUserByMantisUsername(users, assignee) && missingUsername.indexOf(assignee) == -1)
-      missingUsername.push(assignee);
+  var data = {
+    title: title,
+    description: description,
+    assignee_id: assignee && assignee.gl_id,
+    milestone_id: milestoneId,
+    labels: labels,
+    sudo: gitlabSudo,
+    private_token: gitlabAdminPrivateToken
+  };
+
+  return getIssue(gitLab.project.id, issueId)
+      .then(function(gitLabIssue) {
+        if (gitLabIssue) {
+          return updateIssue(gitLab.project.id, gitLabIssue.id, _.extend({
+            state_event: isClosed(mantisIssue) ? 'close' : 'reopen'
+          }, data))
+              .then(function() {
+                console.log(("#" + issueId + ": Updated successfully.").green);
+              });
+        } else {
+          return insertSkippedIssues(issueId-1)
+              .then(function() {
+                return insertAndCloseIssue(issueId, data, isClosed(mantisIssue));
+              });
+        }
+      });
+}
+
+function insertSkippedIssues(issueId) {
+  if (gitLab.gitlabIssues[issueId]) {
+    return Q();
   }
 
-  for (var i = 0; i < rows.length; i++) {
-    var reporter = rows[i].Reporter;
+  console.warn(("Skipping Missing Mantis Issue (<= #" + issueId + ") ...").yellow);
 
-    if (!getUserByMantisUsername(users, reporter) && missingNames.indexOf(reporter) == -1)
-      missingNames.push(reporter);
+  var data = {
+    title: "Skipped Mantis Issue",
+    sudo: gitlabSudo,
+    private_token: gitlabAdminPrivateToken
+  };
+
+  return insertAndCloseIssue(issueId, data, true, getSkippedIssueData)
+      .then(function() {
+        return insertSkippedIssues(issueId);
+      });
+
+  function getSkippedIssueData(gitLabIssue) {
+    var issueId = gitLabIssue.iid;
+    var description;
+    if (config.mantisUrl) {
+      description = "[Mantis Issue " + issueId + "](" + config.mantisUrl + "/view.php?id=" + issueId + ")";
+    } else {
+      description = "Mantis Issue " + issueId;
+    }
+    return {
+      title: "Skipped Mantis Issue " + issueId,
+      description: "_Skipped " + description + "_"
+    };
   }
+}
 
-  callback(missingUsername, missingNames);
+function insertAndCloseIssue(issueId, data, close, custom) {
+
+  return insertIssue(gitLab.project.id, data).then(function(issue) {
+    gitLab.gitlabIssues[issue.iid] = issue;
+    if (close) {
+      return closeIssue(issue, custom && custom(issue)).then(
+          function() {
+            console.log((issueId + ': Inserted and closed successfully. #' + issue.iid).green);
+          }, function(error) {
+            console.warn((issueId + ': Inserted successfully but failed to close. #' + issue.iid).yellow);
+          });
+    }
+
+    console.log((issueId + ': Inserted successfully. #' + issue.iid).green);
+  }, function(error) {
+    console.error((issueId + ': Failed to insert.').red, error);
+  });
 }
 
-function getUserByMantisUsername(users, username) {
-  return (username && _.find(users, {username: username || null })) || null;
+/**
+ * Fetch all existing project issues from GitLab - assigns gitLab.gitlabIssues
+ */
+function getGitLabProjectIssues() {
+  return getRemainingGitLabProjectIssues(0, 100)
+      .then(function(result) {
+        log_progress("Fetched " + result.length + " GitLab issues.");
+        var issues = _.indexBy(result, 'iid');
+        return gitLab.gitlabIssues = issues;
+      });
+}
+
+/**
+ * Recursively fetch the remaining issues in the project
+ * @param page
+ * @param per_page
+ */
+function getRemainingGitLabProjectIssues(page, per_page) {
+  var from = page * per_page;
+  log_progress("Fetching Project Issues from GitLab [" + (from + 1) + "-" + (from + per_page) + "]...");
+  var url = gitlabAPIURLBase + '/projects/' + gitLab.project.id + "/issues";
+  var data = {
+    page: page,
+    per_page: per_page,
+    order_by: 'id',
+    private_token: gitlabAdminPrivateToken, sudo: gitlabSudo };
+
+  return rest.get(url, {data: data}).then(function(issues) {
+    if(issues.length < per_page) {
+      return issues;
+    }
+    return getRemainingGitLabProjectIssues(page+1, per_page)
+        .then(function(remainingIssues) {
+          return issues.concat(remainingIssues);
+        });
+  }, function(error) {
+    throw new Error('Cannot get list of issues from gitlab: ' + url + " page=" + page);
+  });
+}
+
+function getUserByMantisUsername(username) {
+  return (username && config.users[username]) || config.users[""] || null;
 }
 
 function getDescription(row) {
@@ -260,6 +372,10 @@ function getDescription(row) {
     description += "\n\n" + value;
   }
 
+  if (value = row.Notes) {
+    description += "\n\n" + value.split("$$$$").join("\n\n")
+  }
+
   return description;
 }
 
@@ -286,52 +402,55 @@ function isClosed(row) {
   return config.closed_statuses[row.Status];
 }
 
-function insertIssue(projectId, title, description, assigneeId, milestoneId, labels, creatorId, privateToken, callback) {
+function getIssue(projectId, issueId) {
+  return Q(gitLab.gitlabIssues[issueId]);
+  //
+  //var url = gitlabAPIURLBase + '/projects/' + projectId + '/issues?iid=' + issueId;
+  //var data = { private_token: gitlabAdminPrivateToken, sudo: gitlabSudo };
+  //
+  //return rest.get(url, {data: data})
+  //    .then(function(issues) {
+  //      var issue = issues[0];
+  //      if(!issue) {
+  //        throw new Error("Issue not found: " + issueId);
+  //      }
+  //      return issue;
+  //    });
+}
+
+function insertIssue(projectId, data) {
   var url = gitlabAPIURLBase + '/projects/' + projectId + '/issues';
-  var data = {
-    title: title,
-    description: description,
-    assignee_id: assigneeId,
-    milestone_id: milestoneId,
-    labels: labels,
-    sudo: creatorId,
-    private_token: privateToken
-  };
 
-  rest.post(url, {data: data}).on('complete', function(result, response) {
-    if (util.isError(result)) {
-      callback(result);
-      return;
-    }
+  return rest.post(url, {data: data})
+      .then(null, function(error) {
+        throw new Error('Failed to insert issue into GitLab: ' + url);
+      });
+}
 
-    if (response.statusCode != 201) {
-      callback(result);
-      return;
-    }
+function updateIssue(projectId, issueId, data) {
+  var url = gitlabAPIURLBase + '/projects/' + projectId + '/issues/' + issueId;
 
-    callback(null, result);
-  });
+  return rest.put(url, {data: data})
+      .then(null, function(error) {
+        throw new Error('Failed to update issue in GitLab: ' + url + " " + JSON.stringify(error));
+      });
 }
 
-function closeIssue(issue, privateToken, callback) {
+function closeIssue(issue, custom) {
   var url = gitlabAPIURLBase + '/projects/' + issue.project_id + '/issues/' + issue.id;
-  var data = {
+  var data = _.extend({
     state_event: 'close',
-    private_token: privateToken,
+    private_token: gitlabAdminPrivateToken,
     sudo: gitlabSudo
-  };
+  }, custom);
 
-  rest.put(url, {data: data}).on('complete', function(result, response) {
-    if (util.isError(result)) {
-      callback(result);
-      return;
-    }
+  return rest.put(url, {data: data})
+      .then(null, function(error) {
+        throw new Error('Failed to close issue in GitLab: ' + url);
+      });
+}
 
-    if (response.statusCode != 200) {
-      callback(result);
-      return;
-    }
 
-    callback(null);
-  });
-}
+function log_progress(message) {
+  console.log(message.grey);
+}
\ No newline at end of file
index 44158ef..22145a6 100644 (file)
@@ -8,7 +8,10 @@
     "csv": "~0.3.6",
     "lodash": "^3.10.1",
     "optimist": "~0.6.0",
-    "restler": "~3.1.0"
+    "q": "^1.4.1",
+    "q-io": "^1.13.1",
+    "restler": "~3.1.0",
+    "restler-q": "^0.1.1"
   },
   "devDependencies": {},
   "scripts": {