4 var FS = require('q-io/fs');
5 var util = require('util');
6 var colors = require('colors');
7 var csv = require('csv');
8 var rest = require('restler-q');
9 var async = require('async');
10 var _ = require('lodash');
11 var argv = require('optimist')
12 .demand(['i', 'c', 'g', 'p', 't', 's'])
15 .alias('g', 'gitlaburl')
16 .alias('p', 'project')
20 .describe('i', 'CSV file exported from Mantis (Example: issues.csv)')
21 .describe('c', 'Configuration file (Example: config.json)')
22 .describe('g', 'GitLab URL hostname (Example: https://gitlab.com)')
23 .describe('p', 'GitLab project name including namespace (Example: mycorp/myproj)')
24 .describe('t', 'An admin user\'s private token (Example: a2r33oczFyQzq53t23Vj)')
25 .describe('s', 'The username performing the import (Example: bob)')
26 .describe('f', 'The first issue # to import (Example: 123)')
29 var inputFile = __dirname + '/' + argv.input;
30 var configFile = __dirname + '/' + argv.config;
31 var fromIssueId = Number(argv.from||0);
32 var gitlabAPIURLBase = argv.gitlaburl + '/api/v4';
33 var gitlabProjectName = argv.project;
34 var gitlabAdminPrivateToken = argv.token;
35 var gitlabSudo = argv.sudo;
39 var promise = getConfig()
40 .then(readMantisIssues)
41 .then((JSON.stringify(gitLab.mantisIssues)))
42 .then(getGitLabProject)
43 .then(getGitLabProjectMembers)
44 .then(mapGitLabUserIds)
45 .then(validateMantisIssues)
46 .then(getGitLabProjectIssues)
47 .then(importGitLabIssues)
50 promise.then(function() {
51 console.log(("Done!").bold.green);
57 * Read and parse config.json file - assigns config
59 function getConfig() {
60 log_progress("Reading configuration...");
61 return FS.read(configFile, {encoding: 'utf8'})
62 .then(function(data) {
63 var config = JSON.parse(data);
64 config.users = _.extend({
67 gl_username: gitlabSudo
71 }).then(function(cfg) {
74 throw new Error('Cannot read config file: ' + configFile);
79 * Read and parse import.csv file - assigns gitLab.mantisIssues
81 function readMantisIssues() {
82 log_progress("Reading Mantis export file...");
83 return FS.read(inputFile, {encoding: 'utf8'}).then(function(data) {
87 csv().from(data, {delimiter: '\t', quote: '~', escape: '~', columns: true})
88 .on('record', function(row, index) { rows.push(row) })
89 .on('end', function(error, data) {
94 .then(function(rows) {
95 _.forEach(rows, function(row) {
96 row.Id = Number(row.Id);
100 rows = _.filter(rows, function(row) {
101 return row.Id >= fromIssueId;
105 return gitLab.mantisIssues = _.sortBy(rows, "Id");
107 throw new Error('Cannot read input file: ' + inputFile + " - " + error);
113 * Fetch project info from GitLab - assigns gitLab.project
115 function getGitLabProject() {
116 log_progress("Fetching project from GitLab...");
117 var url = gitlabAPIURLBase + '/projects';
118 var data = { per_page: 100, private_token: gitlabAdminPrivateToken, sudo: gitlabSudo };
120 return rest.get(url, {data: data}).then(function(result) {
122 gitLab.project = _.find(result, { path_with_namespace : gitlabProjectName }) || null;
124 if (!gitLab.project) {
125 throw new Error('Cannot find GitLab project: ' + gitlabProjectName);
128 return gitLab.project;
130 throw new Error('Cannot get list of projects from gitlab: ' + url);
135 * Fetch project members from GitLab - assigns gitLab.gitlabUsers
137 function getGitLabProjectMembers() {
138 log_progress("getGitLabProjectMembers");
139 var url = gitlabAPIURLBase + '/projects/' + gitLab.project.id + "/members";
140 var data = { per_page: 100, private_token: gitlabAdminPrivateToken, sudo: gitlabSudo };
142 return rest.get(url, {data: data}).then(function(result) {
143 return gitLab.gitlabUsers = result;
145 throw new Error('Cannot get list of users from gitlab: ' + url);
150 * Sets config.users[].gl_id based gitLab.gitlabUsers
152 function mapGitLabUserIds() {
153 var users = config.users,
154 gitlabUsers = gitLab.gitlabUsers;
155 _.forEach(users, function(user) {
156 user.gl_id = (_.find(gitlabUsers, { id: user.gl_username }) || {}).id;
161 * Ensure that Mantise user names in gitLab.mantisIssues have corresponding GitLab user mapping
163 function validateMantisIssues() {
164 log_progress("Validating Mantis Users...");
166 var mantisIssues = gitLab.mantisIssues;
167 var users = config.users;
169 var missingUsernames = [];
171 for (var i = 0; i < mantisIssues.length; i++) {
172 var assignee = mantisIssues[i]["Assigned To"];
174 if (!getUserByMantisUsername(assignee) && missingUsernames.indexOf(assignee) == -1)
175 missingUsernames.push(assignee);
178 for (var i = 0; i < mantisIssues.length; i++) {
179 var reporter = mantisIssues[i].Reporter;
181 if (!getUserByMantisUsername(reporter) && missingUsernames.indexOf(reporter) == -1)
182 missingUsernames.push(reporter);
185 if (missingUsernames.length > 0) {
186 for (var i = 0; i < missingUsernames.length; i++)
187 console.error('Error: Cannot map Mantis user with username: ' + missingUsernames[i]);
189 throw new Error("User Validation Failed");
194 * Import gitLab.mantisIssues into GitLab
197 function importGitLabIssues() {
198 log_progress("Importing Mantis issues into GitLab from #" + fromIssueId + " ...");
199 return _.reduce(gitLab.mantisIssues, function(p, mantisIssue) {
200 return p.then(function() {
201 return importIssue(mantisIssue);
207 function importIssue(mantisIssue) {
208 var issueId = mantisIssue.Id;
209 var title = mantisIssue.Summary;
210 var description = getDescription(mantisIssue);
211 var assignee = getUserByMantisUsername(mantisIssue["Assigned To"]);
212 var milestoneId = '';
213 var labels = getLabels(mantisIssue);
214 var author = getUserByMantisUsername(mantisIssue.Reporter);
216 log_progress("Importing: #" + issueId + " - " + title + " ...");
220 description: description,
221 assignee_id: assignee && assignee.gl_id,
222 milestone_id: milestoneId,
225 private_token: gitlabAdminPrivateToken
228 return getIssue(gitLab.project.id, issueId)
229 .then(function(gitLabIssue) {
231 return updateIssue(gitLab.project.id, issueId, _.extend({
232 state_event: isClosed(mantisIssue) ? 'close' : 'reopen'
235 console.log(("#" + issueId + ": Updated successfully.").green);
238 return insertSkippedIssues(issueId-1)
240 return insertAndCloseIssue(issueId, data, isClosed(mantisIssue));
246 function insertSkippedIssues(issueId) {
247 if (issueId < 1 || gitLab.gitlabIssues[issueId]) {
251 console.warn(("Skipping Missing Mantis Issue (<= #" + issueId + ") ...").yellow);
254 title: "--Platzhalter--",
255 description: "Fehlende Bugnummer in Mantis. Wird hier ausgelassen, damit die Bugnummern in Mantis mit denen in Gitlab uebereinstimmen.",
257 private_token: gitlabAdminPrivateToken
260 return insertAndCloseIssue(issueId, data, true, getSkippedIssueData)
262 return insertSkippedIssues(issueId);
265 function getSkippedIssueData(gitLabIssue) {
266 var issueId = gitLabIssue.iid;
268 if (config.mantisUrl) {
269 description = "[Mantis Issue " + issueId + "](" + config.mantisUrl + "/view.php?id=" + issueId + ")";
271 description = "Mantis Issue " + issueId;
274 title: "Skipped Mantis Issue " + issueId,
275 description: "_Skipped " + description + "_"
280 function insertAndCloseIssue(issueId, data, close, custom) {
282 return insertIssue(gitLab.project.id, data).then(function(issue) {
283 gitLab.gitlabIssues[issue.iid] = issue;
285 return closeIssue(issue, custom && custom(issue)).then(
287 console.log((issueId + ': Inserted and closed successfully. #' + issue.iid).green);
289 console.warn((issueId + ': Inserted successfully but failed to close. #' + issue.iid).yellow);
293 console.log((issueId + ': Inserted successfully. #' + issue.iid).green);
295 console.error((issueId + ': Failed to insert.').red, error);
300 * Fetch all existing project issues from GitLab - assigns gitLab.gitlabIssues
302 function getGitLabProjectIssues() {
303 return getRemainingGitLabProjectIssues(0, 100)
304 .then(function(result) {
305 log_progress("Fetched " + result.length + " GitLab issues.");
306 var issues = _.indexBy(result, 'iid');
307 return gitLab.gitlabIssues = issues;
312 * Recursively fetch the remaining issues in the project
316 function getRemainingGitLabProjectIssues(page, per_page) {
317 var from = page * per_page;
318 log_progress("Fetching Project Issues from GitLab [" + (from + 1) + "-" + (from + per_page) + "]...");
319 var url = gitlabAPIURLBase + '/projects/' + gitLab.project.id + "/issues";
323 //per_page: per_page,
325 private_token: gitlabAdminPrivateToken, sudo: gitlabSudo };
327 return rest.get(url, {data: data}).then(function(issues) {
328 if(issues.length < per_page) {
331 return getRemainingGitLabProjectIssues(page+1, per_page)
332 .then(function(remainingIssues) {
333 return issues.concat(remainingIssues);
336 throw new Error('Cannot get list of issues from gitlab: ' + url + " page=" + page);
340 function getUserByMantisUsername(username) {
341 return (username && config.users[username]) || config.users[""] || null;
344 function getDescription(row) {
346 var issueId = row.Id;
348 if (config.mantisUrl) {
349 attributes.push("[Mantis Issue " + issueId + "](" + config.mantisUrl + "/view.php?id=" + issueId + ")");
351 attributes.push("Mantis Issue " + issueId);
354 if (value = row.Reporter) {
355 attributes.push("Reported By: " + value);
358 if (value = row["Assigned To"]) {
359 attributes.push("Assigned To: " + value);
362 if (value = row.Created) {
363 attributes.push("Created: " + value);
366 if (value = row.Updated && value != row.Created) {
367 attributes.push("Updated: " + value);
370 var description = "_" + attributes.join(", ") + "_\n\n";
372 description += row.Description;
374 if (value = row.Info) {
375 description += "\n\n" + value;
378 if (value = row.Notes) {
379 description += "\n\n" + value.split("$$$$").join("\n\n")
382 description = description.replace(/\\n/g, "\n");
384 description = description.replace(/\n *----/g, "\n>>>");
386 //description = description.replace(/schnalke/g, "@MSchnalke");
388 Object.keys(config.users).forEach(function(muser) {
390 var gluser = config.users[muser].gl_username;
391 var re = new RegExp(muser, "g");
392 description = description.replace(re, "@" + gluser);
399 function getLabels(row) {
401 var labels = (row.tags || []).slice(0);
403 if(label = config.category_labels[row.Category]) {
407 if(label = config.priority_labels[row.Priority]) {
411 if(label = config.severity_labels[row.Severity]) {
415 return labels.join(",");
418 function isClosed(row) {
419 return config.closed_statuses[row.Status];
422 function getIssue(projectId, issueId) {
423 return Q(gitLab.gitlabIssues[issueId]);
425 //var url = gitlabAPIURLBase + '/projects/' + projectId + '/issues?iid=' + issueId;
426 //var data = { private_token: gitlabAdminPrivateToken, sudo: gitlabSudo };
428 //return rest.get(url, {data: data})
429 // .then(function(issues) {
430 // var issue = issues[0];
432 // throw new Error("Issue not found: " + issueId);
438 function insertIssue(projectId, data) {
439 var url = gitlabAPIURLBase + '/projects/' + projectId + '/issues';
441 return rest.post(url, {data: data})
442 .then(null, function(error) {
443 throw new Error('Failed to insert issue into GitLab: ' + url);
447 function updateIssue(projectId, issueId, data) {
448 var url = gitlabAPIURLBase + '/projects/' + projectId + '/issues/' + issueId;
450 return rest.put(url, {data: data})
451 .then(null, function(error) {
452 throw new Error('Failed to update issue in GitLab: ' + url + " " + JSON.stringify(error));
456 function closeIssue(issue, custom) {
457 var url = gitlabAPIURLBase + '/projects/' + issue.project_id + '/issues/' + issue.id;
458 var data = _.extend({
459 state_event: 'close',
460 private_token: gitlabAdminPrivateToken,
464 return rest.put(url, {data: data})
465 .then(null, function(error) {
466 throw new Error('Failed to close issue in GitLab: ' + url);
471 function log_progress(message) {
472 console.log(message.grey);