042fa7edd2555c35fcabf0c9fab18015a24b6143
[mantis2gitlab] / m2gl.js
1 #!/usr/bin/env node
2
3 var fs = require('fs');
4 var util = require('util');
5 var colors = require('colors');
6 var csv = require('csv');
7 var rest = require('restler');
8 var async = require('async');
9 var _ = require('lodash');
10 var argv = require('optimist')
11     .demand(['i', 'c', 'g', 'p', 't', 's'])
12     .alias('i', 'input')
13     .alias('c', 'config')
14     .alias('g', 'gitlaburl')
15     .alias('p', 'project')
16     .alias('t', 'token')
17     .alias('s', 'sudo')
18     .describe('i', 'CSV file exported from Mantis (Example: issues.csv)')
19     .describe('c', 'Configuration file (Example: config.json)')
20     .describe('g', 'GitLab URL hostname (Example: https://gitlab.com)')
21     .describe('p', 'GitLab project name including namespace (Example: mycorp/myproj)')
22     .describe('t', 'An admin user\'s private token (Example: a2r33oczFyQzq53t23Vj)')
23     .describe('s', 'The username performing the import (Example: bob)')
24     .argv;
25
26 var inputFile = __dirname + '/' + argv.input;
27 var configFile = __dirname + '/' + argv.config;
28 var gitlabAPIURLBase = argv.gitlaburl + '/api/v3';
29 var gitlabProjectName = argv.project;
30 var gitlabAdminPrivateToken = argv.token;
31 var gitlabSudo = argv.sudo;
32 var config = {};
33
34 getGitLabProject(gitlabProjectName, gitlabAdminPrivateToken, function(error, project) {
35   if (error) {
36     console.error('Error: Cannot get list of projects from gitlab: ' + gitlabAPIURLBase);
37     return;
38   }
39
40   if (!project) {
41     console.error('Error: Cannot find GitLab project: ' + gitlabProjectName);
42     return;
43   }
44
45   getGitLabUsers(gitlabAdminPrivateToken, function(error, gitlabUsers) {
46     if (error) {
47       console.error('Error: Cannot get list of users from gitlab: ' + gitlabAPIURLBase);
48       return;
49     }
50
51     getConfig(configFile, function(error, cfg) {
52       if (error) {
53         console.error('Error: Cannot read config file: ' + configFile);
54         return;
55       }
56
57       config = cfg;
58
59       var users = config.users;
60
61       setGitLabUserIds(users, gitlabUsers);
62
63       readRows(inputFile, function(error, rows) {
64         if (error) {
65           console.error('Error: Cannot read input file: ' + inputFile);
66           return;
67         }
68
69         validate(rows, users, function(missingUsernames, missingNames) {
70           if (missingUsernames.length > 0 || missingNames.length > 0) {
71             for (var i = 0; i < missingUsernames.length; i++)
72               console.error('Error: Cannot map Mantis user with username: ' + missingUsernames[i]);
73
74             for (var i = 0; i < missingNames.length; i++)
75               console.error('Error: Cannot map Mantis user with name: ' + missingNames[i]);
76
77             return;
78           }
79
80           rows = _.sortBy(rows, function(row) { return Date.parse(row.Created); });
81
82           async.eachSeries(rows, function(row, callback) {
83             var issueId = row.Id;
84             var title = row.Summary;
85             var description = getDescription(row);
86             var assignee = getUserByMantisUsername(users, row["Assigned To"]);
87             var milestoneId = '';
88             var labels = getLabels(row);
89             var author = getUserByMantisUsername(users, row.Reporter);
90
91             insertIssue(project.id, title, description, assignee && assignee.gl_id, milestoneId, labels, author.gl_username, gitlabAdminPrivateToken, function(error, issue) {
92               setTimeout(callback, 1000);
93
94               if (error) {
95                 console.error((issueId + ': Failed to insert.').red, error);
96                 return;
97               }
98
99               if (isClosed(row)) {
100                 closeIssue(issue, assignee.gl_private_token || gitlabAdminPrivateToken, function(error) {
101                   if (error)
102                     console.warn((issueId + ': Inserted successfully but failed to close. #' + issue.iid).yellow);
103                   else
104                     console.error((issueId + ': Inserted and closed successfully. #' + issue.iid).green);
105                 });
106
107                 return;
108               }
109
110               console.log((issueId + ': Inserted successfully. #' + issue.iid).green);
111             });
112           });
113         });
114       });
115     });
116   });
117 })
118
119 function getGitLabProject(name, privateToken, callback) {
120   var url = gitlabAPIURLBase + '/projects';
121   var data = { per_page: 100, private_token: privateToken, sudo: gitlabSudo };
122
123   rest.get(url, {data: data}).on('complete', function(result, response) {
124     if (util.isError(result)) {
125       callback(result);
126       return;
127     }
128
129     if (response.statusCode != 200) {
130       callback(result);
131       return;
132     }
133
134     for (var i = 0; i < result.length; i++) {
135       if (result[i].path_with_namespace === name) {
136         callback(null, result[i]);
137         return;
138       }
139     };
140
141     callback(null, null);
142   });
143 }
144
145 function getGitLabUsers(privateToken, callback) {
146   var url = gitlabAPIURLBase + '/users';
147   var data = { per_page: 100, private_token: privateToken, sudo: gitlabSudo };
148
149   rest.get(url, {data: data}).on('complete', function(result, response) {
150     if (util.isError(result)) {
151       callback(result);
152       return;
153     }
154
155     if (response.statusCode != 200) {
156       callback(result);
157       return;
158     }
159
160     callback(null, result);
161   });
162 }
163
164 function getConfig(configFile, callback) {
165   fs.readFile(configFile, {encoding: 'utf8'}, function(error, data) {
166     if (error) {
167       callback(error);
168       return;
169     }
170
171     var config = JSON.parse(data);
172     config.users = config.users || [];
173
174     callback(null, config);
175   });
176 }
177
178 function setGitLabUserIds(users, gitlabUsers) {
179   for (var i = 0; i < users.length; i++) {
180     for (var j = 0; j < gitlabUsers.length; j++) {
181       if (users[i].gl_username === gitlabUsers[j].username) {
182         users[i].gl_id = gitlabUsers[j].id;
183         break;
184       }
185     }
186   }
187 }
188
189 function readRows(inputFile, callback) {
190   fs.readFile(inputFile, {encoding: 'utf8'}, function(error, data) {
191     if (error) {
192       callback(error);
193       return;
194     }
195
196     var rows = [];
197
198     csv().from(data, {delimiter: ',', escape: '"', columns: true})
199     .on('record', function(row, index) { rows.push(row) })
200     .on('end', function() { callback(null, rows) });
201   });
202 }
203
204 function validate(rows, users, callback) {
205   var missingUsername = [];
206   var missingNames = [];
207
208   for (var i = 0; i < rows.length; i++) {
209     var assignee = rows[i]["Assigned To"];
210
211     if (!getUserByMantisUsername(users, assignee) && missingUsername.indexOf(assignee) == -1)
212       missingUsername.push(assignee);
213   }
214
215   for (var i = 0; i < rows.length; i++) {
216     var reporter = rows[i].Reporter;
217
218     if (!getUserByMantisUsername(users, reporter) && missingNames.indexOf(reporter) == -1)
219       missingNames.push(reporter);
220   }
221
222   callback(missingUsername, missingNames);
223 }
224
225 function getUserByMantisUsername(users, username) {
226   return (username && _.find(users, {username: username || null })) || null;
227 }
228
229 function getDescription(row) {
230   var attributes = [];
231   var issueId = row.Id;
232   var value;
233   if (config.mantisUrl) {
234     attributes.push("[Mantis Issue " + issueId + "](" + config.mantisUrl + "/view.php?id=" + issueId + ")");
235   } else {
236     attributes.push("Mantis Issue " + issueId);
237   }
238
239   if (value = row.Reporter) {
240     attributes.push("Reported By: " + value);
241   }
242
243   if (value = row["Assigned To"]) {
244     attributes.push("Assigned To: " + value);
245   }
246
247   if (value = row.Created) {
248     attributes.push("Created: " + value);
249   }
250
251   if (value = row.Updated && value != row.Created) {
252     attributes.push("Updated: " + value);
253   }
254
255   var description = "_" + attributes.join(", ") + "_\n\n";
256
257   description += row.Description;
258
259   if (value = row.Info) {
260     description += "\n\n" + value;
261   }
262
263   return description;
264 }
265
266 function getLabels(row) {
267   var label;
268   var labels = (row.tags || []).slice(0);
269
270   if(label = config.category_labels[row.Category]) {
271     labels.push(label);
272   }
273
274   if(label = config.priority_labels[row.Priority]) {
275     labels.push(label);
276   }
277
278   if(label = config.severity_labels[row.Severity]) {
279     labels.push(label);
280   }
281
282   return labels.join(",");
283 }
284
285 function isClosed(row) {
286   return config.closed_statuses[row.Status];
287 }
288
289 function insertIssue(projectId, title, description, assigneeId, milestoneId, labels, creatorId, privateToken, callback) {
290   var url = gitlabAPIURLBase + '/projects/' + projectId + '/issues';
291   var data = {
292     title: title,
293     description: description,
294     assignee_id: assigneeId,
295     milestone_id: milestoneId,
296     labels: labels,
297     sudo: creatorId,
298     private_token: privateToken
299   };
300
301   rest.post(url, {data: data}).on('complete', function(result, response) {
302     if (util.isError(result)) {
303       callback(result);
304       return;
305     }
306
307     if (response.statusCode != 201) {
308       callback(result);
309       return;
310     }
311
312     callback(null, result);
313   });
314 }
315
316 function closeIssue(issue, privateToken, callback) {
317   var url = gitlabAPIURLBase + '/projects/' + issue.project_id + '/issues/' + issue.id;
318   var data = {
319     state_event: 'close',
320     private_token: privateToken,
321     sudo: gitlabSudo
322   };
323
324   rest.put(url, {data: data}).on('complete', function(result, response) {
325     if (util.isError(result)) {
326       callback(result);
327       return;
328     }
329
330     if (response.statusCode != 200) {
331       callback(result);
332       return;
333     }
334
335     callback(null);
336   });
337 }