Project

General

Profile

Migration JIRA to Redmine ยป migrate_jira.rake

Heribert Gasparoli, 2021-10-29 10:22

 
1
require 'rexml/document'
2
require 'active_record'
3
require 'yaml'
4
require File.expand_path('../config/environment',  __FILE__)
5

    
6

    
7
module JiraMigration
8
  include REXML
9

    
10
  file = File.new('entities.xml')
11
  doc = Document.new file
12
  $doc = doc
13

    
14
  CONF_FILE = "map_jira_to_redmine.yml"
15

    
16
  $MIGRATED_USERS_BY_NAME = {} # Maps the Jira username to the Redmine Rails User object
17
  $MIGRATED_ISSUE_TYPES = {} 
18
  $MIGRATED_ISSUE_STATUS = {}
19
  $MIGRATED_ISSUE_PRIORITIES = {}
20

    
21
  $MIGRATED_ISSUE_TYPES_BY_ID = {} 
22
  $MIGRATED_ISSUE_STATUS_BY_ID = {}
23
  $MIGRATED_ISSUE_PRIORITIES_BY_ID = {}
24

    
25
  def self.get_all_options()
26
    # return all options 
27
    # Issue Type, Issue Status, Issue Priority
28
    ret = {}
29
    ret["types"] = self.get_jira_issue_types()
30
    ret["status"] = self.get_jira_status()
31
    ret["priorities"] = self.get_jira_priorities()
32

    
33
    return ret
34
  end
35
  def self.get_list_from_tag(xpath_query)
36
    # Get a tag node and get all attributes as a hash
37
    ret = []
38
    $doc.elements.each(xpath_query) {|node| ret.push(node.attributes.rehash)}
39

    
40
    return ret
41
  end
42

    
43
  class BaseJira
44
    attr_reader :tag
45
    attr_accessor :new_record
46
    MAP = {}
47

    
48
    def map
49
      self.class::MAP
50
    end
51

    
52
    def initialize(node)
53
      @tag = node
54
    end
55

    
56
    def method_missing(key, *args)
57
      if key.to_s.start_with?("jira_")
58
        attr = key.to_s.sub("jira_", "")
59
        return @tag.attributes[attr]
60
      end
61
      puts "Method missing: #{key}"
62
      raise NoMethodError key
63
    end
64

    
65
    def run_all_redmine_fields
66
      ret = {}
67
      self.methods.each do |method_name|
68
        m = method_name.to_s
69
        if m.start_with?("red_")
70
          mm = m.to_s.sub("red_", "")
71
          ret[mm] = self.send(m)
72
        end
73
      end
74
      return ret
75
    end
76
    def migrate
77
      all_fields = self.run_all_redmine_fields()
78
      pp("Saving:", all_fields)
79
      record = self.retrieve
80
      if record
81
        record.update_attributes(all_fields)
82
      else
83
        record = self.class::DEST_MODEL.new all_fields
84
      end
85
      if self.respond_to?("before_save")
86
        self.before_save(record)
87
      end
88
      record.save!
89
      record.reload
90
      self.map[self.jira_id] = record
91
      self.new_record = record
92
      if self.respond_to?("post_migrate")
93
        self.post_migrate(record)
94
      end
95
      return record 
96
    end
97
    def retrieve
98
      self.class::DEST_MODEL.find_by_name(self.jira_id)
99
    end
100
  end
101

    
102
  class JiraProject < BaseJira
103
    DEST_MODEL = Project
104
    MAP = {}
105

    
106
    def retrieve
107
      self.class::DEST_MODEL.find_by_identifier(self.red_identifier)
108
    end
109
    def post_migrate(new_record)
110
      if !new_record.module_enabled?('issue_tracking')
111
        new_record.enabled_modules << EnabledModule.new(:name => 'issue_tracking')
112
      end
113
      $MIGRATED_ISSUE_TYPES.values.uniq.each do |issue_type|
114
        if !new_record.trackers.include?(issue_type)
115
          new_record.trackers << issue_type
116
        end
117
      end
118
    end
119

    
120
    # here is the tranformation of Jira attributes in Redmine attribues
121
    def red_name
122
      self.jira_name
123
    end
124
    def red_description
125
      self.jira_name
126
    end
127
    def red_identifier
128
      ret = self.jira_key.downcase
129
      return ret
130
    end
131
  end
132

    
133
  class JiraUser < BaseJira
134
    DEST_MODEL = User
135
    MAP = {}
136
    def retrieve
137
      user = self.class::DEST_MODEL.find_by_login(self.red_login)
138
      if !user
139
        user = self.class::DEST_MODEL.find_by_mail(self.jira_emailAddress)
140
      end
141

    
142
      return user
143
    end
144
    def migrate
145
      super
146
      $MIGRATED_USERS_BY_NAME[self.jira_userName] = self.new_record
147
    end
148

    
149
    # First Name, Last Name, E-mail, Password
150
    # here is the tranformation of Jira attributes in Redmine attribues
151
    def red_firstname()
152
       self.jira_userName
153
     # self.jira_firstName
154
    end
155
    def red_lastname
156
       self.jira_userName
157
      #self.jira_lastName
158
    end
159
    def red_mail
160
      self.jira_emailAddress
161
    end
162
    def red_password
163
      self.jira_userName
164
    end
165
    def red_login
166
      self.jira_userName
167
    end
168
    def before_save(new_record)
169
      new_record.login = red_login
170
    end
171
    #def red_username
172
    #  self.jira_name
173
    #end
174
  end
175

    
176
  class JiraComment < BaseJira
177
    DEST_MODEL = Journal
178
    MAP = {}
179

    
180
    def initialize(node)
181
      super
182
      # get a body from a comment
183
      # comment can have the comment body as a attribute or as a child tag
184
      @jira_body = @tag.attributes["body"] || @tag.elements["body"].text
185
    end
186

    
187
    def jira_marker
188
      return "FROM JIRA: #{self.jira_id}\n"
189
    end
190
    def retrieve
191
      Journal.first(:conditions => "notes LIKE '#{self.jira_marker}%'")
192
    end
193

    
194
    # here is the tranformation of Jira attributes in Redmine attribues
195
    def red_notes
196
      self.jira_marker + "\n" + @jira_body
197
    end
198
    def red_created_on
199
      DateTime.parse(self.jira_created)
200
    end
201
    def red_user
202
      # retrieving the Rails object
203
      $MIGRATED_USERS_BY_NAME[self.jira_author]
204
    end
205
    def red_journalized
206
      # retrieving the Rails object
207
      JiraIssue::MAP[self.jira_issue]
208
    end
209
  end
210

    
211
  class JiraIssue < BaseJira
212
    DEST_MODEL = Issue
213
    MAP = {}
214
    #attr_reader :jira_id, :jira_key, :jira_project, :jira_reporter, 
215
    #            :jira_type, :jira_summary, :jira_assignee, :jira_priority
216
    #            :jira_resolution, :jira_status, :jira_created, :jira_resolutiondate
217
    attr_reader  :jira_description
218

    
219
    def initialize(node_tag)
220
      super
221
      @jira_description = @tag.elements["description"].text if @tag.elements["description"]
222
    end
223
    def jira_marker
224
      return "FROM JIRA: #{self.jira_key}\n"
225
    end
226
    def retrieve
227
      Issue.first.each(:conditions => "description LIKE '#{self.jira_marker}%'")
228
    end
229

    
230
    def red_project
231
      # needs to return the Rails Project object
232
      proj = self.jira_project
233
      JiraProject::MAP[proj]
234
    end
235
    def red_subject
236
      #:subject => encode(issue.title[0, limit_for(Issue, 'subject')]),
237
      self.jira_summary
238
    end
239
    def red_description
240
      dsc = self.jira_marker + "\n"
241
      if @jira_description
242
        dsc += @jira_description 
243
      else
244
        dsc += self.red_subject
245
      end
246
      return dsc
247
    end
248
    def red_priority
249
      name = $MIGRATED_ISSUE_PRIORITIES_BY_ID[self.jira_priority]
250
      return $MIGRATED_ISSUE_PRIORITIES[name]
251
    end
252
    def red_created_on
253
      Time.parse(self.jira_created)
254
    end
255
    def red_updated_on
256
      Time.parse(self.jira_updated)
257
    end
258
    def red_status
259
      name = $MIGRATED_ISSUE_STATUS_BY_ID[self.jira_status]
260
      return $MIGRATED_ISSUE_STATUS[name]
261
    end
262
    def red_tracker
263
      type_name = $MIGRATED_ISSUE_TYPES_BY_ID[self.jira_type]
264
      return $MIGRATED_ISSUE_TYPES[type_name]
265
    end
266
    def red_author
267
      $MIGRATED_USERS_BY_NAME[self.jira_reporter]
268
    end
269
    def red_assigned_to
270
      $MIGRATED_USERS_BY_NAME[self.jira_assignee]
271
    end
272

    
273
  end
274

    
275
  class JiraAttachment < BaseJira
276
    DEST_MODEL = Attachment
277
    MAP = {}
278

    
279
    def retrieve
280
      self.class::DEST_MODEL.find_by_disk_filename(self.red_filename)
281
    end
282
    def before_save(new_record)
283
      new_record.container = self.red_container
284
      pp(new_record)
285
      
286
    end
287

    
288
    # here is the tranformation of Jira attributes in Redmine attribues
289
    #<FileAttachment id="10084" issue="10255" mimetype="image/jpeg" filename="Landing_Template.jpg" 
290
    #                created="2011-05-05 15:54:59.411" filesize="236515" author="emiliano"/>
291
    def red_filename
292
      self.jira_filename.gsub(/[^\w\.\-]/,'_')  # stole from Redmine: app/model/attachment (methods sanitize_filenanme)
293
    end
294
    def red_disk_filename 
295
      Attachment.disk_filename(self.jira_issue+self.jira_filename)
296
    end
297
    def red_content_type 
298
      self.jira_mimetype.to_s.chomp
299
    end
300
    def red_filesize 
301
      self.jira_filesize
302
    end
303

    
304
    def red_created_on
305
      DateTime.parse(self.jira_created)
306
    end
307
    def red_author
308
      $MIGRATED_USERS_BY_NAME[self.jira_author]
309
    end
310
    def red_container
311
      JiraIssue::MAP[self.jira_issue]
312
    end
313
  end
314

    
315

    
316
  def self.parse_projects()
317
    # PROJECTS:
318
    # for project we need (identifies, name and description)
319
    # in exported data we have name and key, in Redmine name and descr. will be equal
320
    # the key will be the identifier
321
    projs = []
322
    $doc.elements.each('/*/Project') do |node|
323
      proj = JiraProject.new(node)
324
      projs.push(proj)
325
    end
326

    
327
    migrated_projects = {}
328
    projs.each do |p|
329
      #puts "Name and descr.: #{p.red_name} and #{p.red_description}"
330
      #puts "identifier: #{p.red_identifier}"
331
      migrated_projects[p.jira_id] = p
332
    end
333
    #puts migrated_projects
334
    return projs
335
  end
336

    
337
  def self.parse_users()
338
    users = []
339
    #users = self.get_list_from_tag('/*/OSUser')
340
    # For users in Redmine we need:
341
    # First Name, Last Name, E-mail, Password
342
    # In Jira, the fullname and email are property (a little more hard to get)
343

    
344
    $doc.elements.each('/entity-engine-xml/User') do |node|
345
      user = JiraUser.new(node)
346
      users.push(user)
347
    end
348

    
349
    return users
350
  end
351

    
352
  ISSUE_TYPE_MARKER = "(choose a Redmine Tracker)"
353
  DEFAULT_ISSUE_TYPE_MAP = {
354
    # Default map from Jira (key) to Redmine (value)
355
    # the comments on right side are Jira definitions - http://confluence.atlassian.com/display/JIRA/What+is+an+Issue#
356
    "Bug" => "Bug",              # A problem which impairs or prevents the functions of the product.
357
    "Improvement" => "Feature",  # An enhancement to an existing feature.
358
    "New Feature" => "Feature",  # A new feature of the product.
359
    "Task" => "Task",            # A task that needs to be done.
360
    "Custom Issue" => "Support", # A custom issue type, as defined by your organisation if required.
361
  }
362
  def self.get_jira_issue_types()
363
    # Issue Type
364
    issue_types = self.get_list_from_tag('/*/IssueType') 
365
    #migrated_issue_types = {"jira_type" => "redmine tracker"}
366
    migrated_issue_types = {}
367
    issue_types.each do |issue|
368
      migrated_issue_types[issue["name"]] = DEFAULT_ISSUE_TYPE_MAP.fetch(issue["name"], ISSUE_TYPE_MARKER)
369
      $MIGRATED_ISSUE_TYPES_BY_ID[issue["id"]] = issue["name"]
370
    end
371
    return migrated_issue_types
372
  end
373

    
374
  ISSUE_STATUS_MARKER = "(choose a Redmine Issue Status)"
375
  DEFAULT_ISSUE_STATUS_MAP = {
376
    # Default map from Jira (key) to Redmine (value)
377
    # the comments on right side are Jira definitions - http://confluence.atlassian.com/display/JIRA/What+is+an+Issue#
378
    "Open" => "New",                # This issue is in the initial 'Open' state, ready for the assignee to start work on it.
379
    "In Progress" => "In Progress", # This issue is being actively worked on at the moment by the assignee.
380
    "Resolved" => "Resolved",       # A Resolution has been identified or implemented, and this issue is awaiting verification by the reporter. From here, issues are either 'Reopened' or are 'Closed'.
381
    "Reopened" => "Assigned",       # This issue was once 'Resolved' or 'Closed', but is now being re-examined. (For example, an issue with a Resolution of 'Cannot Reproduce' is Reopened when more information becomes available and the issue becomes reproducible). From here, issues are either marked In Progress, Resolved or Closed.
382
    "Closed" => "Closed",           # This issue is complete. ## Be careful to choose one which a "issue closed" attribute marked :-)
383
  }
384
  def self.get_jira_status()
385
    # Issue Status
386
    issue_status = self.get_list_from_tag('/*/Status') 
387
    migrated_issue_status = {}
388
    issue_status.each do |issue|
389
      migrated_issue_status[issue["name"]] = DEFAULT_ISSUE_STATUS_MAP.fetch(issue["name"], ISSUE_STATUS_MARKER)
390
      $MIGRATED_ISSUE_STATUS_BY_ID[issue["id"]] = issue["name"]
391
    end
392
    return migrated_issue_status
393
  end
394

    
395
  ISSUE_PRIORITY_MARKER = "(choose a Redmine Enumeration Issue Priority)"
396
  DEFAULT_ISSUE_PRIORITY_MAP = {
397
    # Default map from Jira (key) to Redmine (value)
398
    # the comments on right side are Jira definitions - http://confluence.atlassian.com/display/JIRA/What+is+an+Issue#
399
     "Blocker" => "Immediate", # Highest priority. Indicates that this issue takes precedence over all others.
400
     "Critical" => "Urgent",   # Indicates that this issue is causing a problem and requires urgent attention.
401
     "Major" => "High",        # Indicates that this issue has a significant impact.
402
     "Minor" => "Normal",      # Indicates that this issue has a relatively minor impact.
403
     "Trivial" => "Low",       # Lowest priority.
404
  }
405
  def self.get_jira_priorities()
406
    # Issue Priority
407
    issue_priority = self.get_list_from_tag('/*/Priority') 
408
    migrated_issue_priority = {}
409
    issue_priority.each do |issue|
410
      migrated_issue_priority[issue["name"]] = DEFAULT_ISSUE_PRIORITY_MAP.fetch(issue["name"], ISSUE_PRIORITY_MARKER)
411
      $MIGRATED_ISSUE_PRIORITIES_BY_ID[issue["id"]] = issue["name"]
412
    end
413
    return migrated_issue_priority
414
  end
415

    
416
  def self.parse_comments()
417
    ret = []
418
    $doc.elements.each('/*/Action[@type="comment"]') do |node|
419
      comment = JiraComment.new(node)
420
      ret.push(comment)
421
    end
422
    return ret
423
  end 
424

    
425
  def self.parse_issues()
426
    ret = []
427
    $doc.elements.each('/*/Issue') do |node|
428
      issue = JiraIssue.new(node)
429
      ret.push(issue)
430
    end
431
    return ret
432
  end 
433

    
434
  def self.parse_attachments()
435
    attachs = []
436
    $doc.elements.each('/*/FileAttachment') do |node|
437
      attach = JiraAttachment.new(node)
438
      attachs.push(attach)
439
    end
440

    
441
    return attachs
442
  end
443
end
444

    
445

    
446

    
447

    
448
namespace :jira_migration do
449

    
450
  desc "Generates the configuration for the map things from Jira to Redmine"
451
  task :generate_conf => :environment do
452
    conf_file = JiraMigration::CONF_FILE
453
    conf_exists = File.exists?(conf_file)
454
    if conf_exists
455
      puts "You already have a conf file"
456
      print "You want overwrite it ? [y/N] "
457
      overwrite = STDIN.gets.match(/^y$/i)
458
    end
459

    
460
    if !conf_exists or overwrite
461
      # Let's give the user all options to fill out
462
      options = JiraMigration.get_all_options()
463

    
464
      File.open(conf_file, "w"){ |f| f.write(options.to_yaml) }
465

    
466
      puts "This migration script needs the migration table to continue "
467
      puts "Please... fill the map table on the file: '#{conf_file}' and run again the script"
468
      puts "To start the options again, just remove the file '#{conf_file} and run again the script"
469
      exit(0)
470
    end
471
  end
472

    
473
  desc "Gets the configuration from YAML"
474
  task :pre_conf => :environment do
475
    conf_file = JiraMigration::CONF_FILE
476
    conf_exists = File.exists?(conf_file)
477

    
478
    if !conf_exists 
479
      Rake::Task['jira_migration:generate_conf'].invoke
480
    end
481
    $confs = YAML.load_file(conf_file)
482
  end
483

    
484
  desc "Tests all parsers!"
485
  task :test_all_migrations => [:environment, :pre_conf,
486
                              :test_parse_projects, 
487
                              :test_parse_users, 
488
                              :test_parse_comments, 
489
                              :test_parse_issues, 
490
                             ] do
491
    puts "All parsers was run! :-)"
492
  end
493

    
494
  desc "Tests all parsers!"
495
  task :do_all_migrations => [:environment, :pre_conf,
496
                              :migrate_issue_types, 
497
                              :migrate_issue_status, 
498
                              :migrate_issue_priorities, 
499
                              :migrate_projects, 
500
                              :migrate_users, 
501
                              :migrate_issues, 
502
                              :migrate_comments, 
503
                              :migrate_attachments,
504
                             ] do
505
    puts "All migrations done! :-)"
506
  end
507

    
508

    
509
  desc "Migrates Jira Issue Types to Redmine Trackes"
510
  task :migrate_issue_types => [:environment, :pre_conf] do
511

    
512
    JiraMigration.get_jira_issue_types()
513
    types = $confs["types"]
514
    types.each do |key, value|
515
      t = Tracker.first_or_create(value)
516
      t.save!
517
      t.reload
518
      $MIGRATED_ISSUE_TYPES[key] = t
519
    end
520
    puts "Migrated issue types"
521
  end
522

    
523
  desc "Migrates Jira Issue Status to Redmine Status"
524
  task :migrate_issue_status => [:environment, :pre_conf] do
525
    JiraMigration.get_jira_status()
526
    status = $confs["status"]
527
    status.each do |key, value|
528
      s = IssueStatus.first_or_create(value)
529
      s.save!
530
      s.reload
531
      $MIGRATED_ISSUE_STATUS[key] = s
532
    end
533
    puts "Migrated issue status"
534
  end
535

    
536
  desc "Migrates Jira Issue Priorities to Redmine Priorities"
537
  task :migrate_issue_priorities => [:environment, :pre_conf] do
538
    JiraMigration.get_jira_priorities()
539
    priorities = $confs["priorities"]
540

    
541
    priorities.each do |key, value|
542
      p = IssuePriority.first_or_create(value)
543
      p.save!
544
      p.reload
545
      $MIGRATED_ISSUE_PRIORITIES[key] = p
546
    end
547
    puts "Migrated issue priorities"
548
  end
549

    
550
  desc "Migrates Jira Projects to Redmine Projects"
551
  task :migrate_projects => :environment do
552
    projects = JiraMigration.parse_projects()
553
    projects.each do |p|
554
      #pp(p)
555
      p.migrate
556
    end
557
  end
558

    
559
  desc "Migrates Jira Users to Redmine Users"
560
  task :migrate_users => :environment do
561
    users = JiraMigration.parse_users()
562
    users.each do |u|
563
      #pp(u)
564
      u.migrate
565
    end
566
  end
567

    
568
  desc "Migrates Jira Issues to Redmine Issues"
569
  task :migrate_issues => :environment do
570
    issues = JiraMigration.parse_issues()
571
    issues.each do |i|	
572
      #pp(i)
573
      i.migrate
574
    end
575
  end
576

    
577
  desc "Migrates Jira Issues Comments to Redmine Issues Journals (Notes)"
578
  task :migrate_comments => :environment do
579
    comments = JiraMigration.parse_comments()
580
    comments.each do |c|
581
      #pp(c)
582
      c.migrate
583
    end
584
  end
585

    
586
  desc "Migrates Jira Issues Attachments to Redmine Attachments"
587
  task :migrate_attachments => :environment do
588
    attachs = JiraMigration.parse_attachments()
589
    attachs.each do |a|
590
      #pp(c)
591
      a.migrate
592
    end
593
  end
594

    
595
  # Tests.....
596
  desc "Just pretty print Jira Projects on screen"
597
  task :test_parse_projects => :environment do
598
    projects = JiraMigration.parse_projects()
599
    projects.each {|p| pp(p.run_all_redmine_fields) }
600
  end
601

    
602
  desc "Just pretty print Jira Users on screen"
603
  task :test_parse_users => :environment do
604
    users = JiraMigration.parse_users()
605
    users.each {|u| pp( u.run_all_redmine_fields) }
606
  end
607

    
608
  desc "Just pretty print Jira Comments on screen"
609
  task :test_parse_comments => :environment do
610
    comments = JiraMigration.parse_comments()
611
    comments.each {|c| pp( c.run_all_redmine_fields) }
612
  end
613

    
614
  desc "Just pretty print Jira Issues on screen"
615
  task :test_parse_issues => :environment do
616
    issues = JiraMigration.parse_issues()
617
    issues.each {|i| pp( i.run_all_redmine_fields) }
618
  end
619
end
    (1-1/1)