Project

General

Profile

Feature #1385 » migrate_jira.rake

Rudy Attias, 2012-06-13 15:12

 
1
require 'rexml/document'
2
require 'active_record'
3
require 'yaml'
4
require 'config/environment'
5

    
6

    
7

    
8
module JiraMigration
9
  include REXML
10

    
11
  file = File.new('backup_jira.xml')
12
  doc = Document.new file
13
  $doc = doc
14

    
15
  CONF_FILE = "map_jira_to_redmine.yml"
16

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

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

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

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

    
41
    return ret
42
  end
43

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
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(: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
    end
286

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

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

    
314

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

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

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

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

    
348
    return users
349
  end
350

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

    
373
  ISSUE_STATUS_MARKER = "(choose a Redmine Issue Status)"
374
  DEFAULT_ISSUE_STATUS_MAP = {
375
    # Default map from Jira (key) to Redmine (value)
376
    # the comments on right side are Jira definitions - http://confluence.atlassian.com/display/JIRA/What+is+an+Issue#
377
    "Open" => "New",                # This issue is in the initial 'Open' state, ready for the assignee to start work on it.
378
    "In Progress" => "In Progress", # This issue is being actively worked on at the moment by the assignee.
379
    "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'.
380
    "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.
381
    "Closed" => "Closed",           # This issue is complete. ## Be careful to choose one which a "issue closed" attribute marked :-)
382
  }
383
  def self.get_jira_status()
384
    # Issue Status
385
    issue_status = self.get_list_from_tag('/*/Status') 
386
    migrated_issue_status = {}
387
    issue_status.each do |issue|
388
      migrated_issue_status[issue["name"]] = DEFAULT_ISSUE_STATUS_MAP.fetch(issue["name"], ISSUE_STATUS_MARKER)
389
      $MIGRATED_ISSUE_STATUS_BY_ID[issue["id"]] = issue["name"]
390
    end
391
    return migrated_issue_status
392
  end
393

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

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

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

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

    
440
    return attachs
441
  end
442
end
443

    
444

    
445

    
446

    
447
namespace :jira_migration do
448

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

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

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

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

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

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

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

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

    
507

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

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

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

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

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

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

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

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

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

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

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

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

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

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