Project

General

Profile

Feature #1385 » migrate_jira.rake

alternate script - Luciano Pacheco, 2011-06-07 06:28

 
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
    #attr_reader :jira_fullname, :jira_email
138

    
139
    #def initialize(node)
140
    #  super
141
    #  # parsing specific things of each user
142
    #  @jira_fullname = get_property("fullName")
143
    #  @jira_email = get_property("email")
144
    #end
145
    def retrieve
146
      user = self.class::DEST_MODEL.find_by_login(self.red_login)
147
      if !user
148
        user = self.class::DEST_MODEL.find_by_mail(self.jira_email)
149
      end
150

    
151
      return user
152
    end
153

    
154
    def jira_fullname 
155
      get_property("fullName")
156
    end
157
    def jira_email
158
      get_property("email")
159
    end
160

    
161
    def get_property(property)
162
      # User properties (like fullname and email)
163
      # Wow... we need to find 2 things to reach the desired property
164
      entityId = self.jira_id
165
      query = "/entity-engine-xml/OSPropertyEntry[@entityId='#{entityId}' and @propertyKey='#{property}']/@id"
166
      prop = $doc.elements.each(query).first()
167
      prop = prop.value
168

    
169
      query = "/entity-engine-xml/OSPropertyString[@id='#{prop}']/@value"
170
      prop = $doc.elements.each(query).first()
171
      return prop.value
172
    end
173

    
174
    def get_first_name()
175
       firstname, lastname = self.jira_fullname.split ' ', 2 
176
       return firstname
177
    end
178
    def get_last_name()
179
       firstname, lastname = self.jira_fullname.split ' ', 2 
180
       return lastname
181
    end
182
    def migrate
183
      super
184
      $MIGRATED_USERS_BY_NAME[self.jira_name] = self.new_record
185
    end
186

    
187
    # First Name, Last Name, E-mail, Password
188
    # here is the tranformation of Jira attributes in Redmine attribues
189
    def red_firstname()
190
      self.get_first_name
191
    end
192
    def red_lastname
193
      self.get_last_name.sub("(", "").sub(")", "")
194
    end
195
    def red_mail
196
      self.jira_email
197
    end
198
    def red_password
199
      self.jira_name
200
    end
201
    def red_login
202
      self.jira_name
203
    end
204
    def before_save(new_record)
205
      new_record.login = red_login
206
    end
207
    #def red_username
208
    #  self.jira_name
209
    #end
210
  end
211

    
212
  class JiraComment < BaseJira
213
    DEST_MODEL = Journal
214
    MAP = {}
215

    
216
    def initialize(node)
217
      super
218
      # get a body from a comment
219
      # comment can have the comment body as a attribute or as a child tag
220
      @jira_body = @tag.attributes["body"] || @tag.elements["body"].text
221
    end
222

    
223
    def jira_marker
224
      return "FROM JIRA: #{self.jira_id}\n"
225
    end
226
    def retrieve
227
      Journal.first(:conditions => "notes LIKE '#{self.jira_marker}%'")
228
    end
229

    
230
    # here is the tranformation of Jira attributes in Redmine attribues
231
    def red_notes
232
      self.jira_marker + "\n" + @jira_body
233
    end
234
    def red_created_on
235
      DateTime.parse(self.jira_created)
236
    end
237
    def red_user
238
      # retrieving the Rails object
239
      $MIGRATED_USERS_BY_NAME[self.jira_author]
240
    end
241
    def red_journalized
242
      # retrieving the Rails object
243
      JiraIssue::MAP[self.jira_issue]
244
    end
245
  end
246

    
247
  class JiraIssue < BaseJira
248
    DEST_MODEL = Issue
249
    MAP = {}
250
    #attr_reader :jira_id, :jira_key, :jira_project, :jira_reporter, 
251
    #            :jira_type, :jira_summary, :jira_assignee, :jira_priority
252
    #            :jira_resolution, :jira_status, :jira_created, :jira_resolutiondate
253
    attr_reader  :jira_description
254

    
255

    
256
    def initialize(node_tag)
257
      super
258
      @jira_description = @tag.elements["description"].text if @tag.elements["description"]
259
    end
260
    def jira_marker
261
      return "FROM JIRA: #{self.jira_key}\n"
262
    end
263
    def retrieve
264
      Issue.first(:conditions => "description LIKE '#{self.jira_marker}%'")
265
    end
266

    
267
    def red_project
268
      # needs to return the Rails Project object
269
      proj = self.jira_project
270
      JiraProject::MAP[proj]
271
    end
272
    def red_subject
273
      #:subject => encode(issue.title[0, limit_for(Issue, 'subject')]),
274
      self.jira_summary
275
    end
276
    def red_description
277
      dsc = self.jira_marker + "\n"
278
      if @jira_description
279
        dsc += @jira_description 
280
      else
281
        dsc += self.red_subject
282
      end
283
      return dsc
284
    end
285
    def red_priority
286
      name = $MIGRATED_ISSUE_PRIORITIES_BY_ID[self.jira_priority]
287
      return $MIGRATED_ISSUE_PRIORITIES[name]
288
    end
289
    def red_created_on
290
      Time.parse(self.jira_created)
291
    end
292
    def red_updated_on
293
      Time.parse(self.jira_updated)
294
    end
295
    def red_status
296
      name = $MIGRATED_ISSUE_STATUS_BY_ID[self.jira_status]
297
      return $MIGRATED_ISSUE_STATUS[name]
298
    end
299
    def red_tracker
300
      type_name = $MIGRATED_ISSUE_TYPES_BY_ID[self.jira_type]
301
      return $MIGRATED_ISSUE_TYPES[type_name]
302
    end
303
    def red_author
304
      $MIGRATED_USERS_BY_NAME[self.jira_reporter]
305
    end
306
    def red_assigned_to
307
      $MIGRATED_USERS_BY_NAME[self.jira_assignee]
308
    end
309

    
310
  end
311

    
312
  class JiraAttachment < BaseJira
313
    DEST_MODEL = Attachment
314
    MAP = {}
315

    
316
    def retrieve
317
      self.class::DEST_MODEL.find_by_disk_filename(self.red_filename)
318
    end
319
    def before_save(new_record)
320
      new_record.container = self.red_container
321
      pp(new_record)
322
    end
323

    
324
    # here is the tranformation of Jira attributes in Redmine attribues
325
    #<FileAttachment id="10084" issue="10255" mimetype="image/jpeg" filename="Landing_Template.jpg" 
326
    #                created="2011-05-05 15:54:59.411" filesize="236515" author="emiliano"/>
327
    def red_filename
328
      self.jira_filename.gsub(/[^\w\.\-]/,'_')  # stole from Redmine: app/model/attachment (methods sanitize_filenanme)
329
    end
330
    def red_disk_filename 
331
      Attachment.disk_filename(self.jira_filename)
332
    end
333
    def red_content_type 
334
      self.jira_mimetype.to_s.chomp
335
    end
336
    def red_filesize 
337
      self.jira_filesize
338
    end
339

    
340
    def red_created_on
341
      DateTime.parse(self.jira_created)
342
    end
343
    def red_author
344
      $MIGRATED_USERS_BY_NAME[self.jira_author]
345
    end
346
    def red_container
347
      JiraIssue::MAP[self.jira_issue]
348
    end
349
  end
350

    
351

    
352
  def self.parse_projects()
353
    # PROJECTS:
354
    # for project we need (identifies, name and description)
355
    # in exported data we have name and key, in Redmine name and descr. will be equal
356
    # the key will be the identifier
357
    projs = []
358
    $doc.elements.each('/*/Project') do |node|
359
      proj = JiraProject.new(node)
360
      projs.push(proj)
361
    end
362

    
363
    migrated_projects = {}
364
    projs.each do |p|
365
      #puts "Name and descr.: #{p.red_name} and #{p.red_description}"
366
      #puts "identifier: #{p.red_identifier}"
367
      migrated_projects[p.jira_id] = p
368
    end
369
    #puts migrated_projects
370
    return projs
371
  end
372

    
373
  def self.parse_users()
374
    users = []
375
    #users = self.get_list_from_tag('/*/OSUser')
376
    # For users in Redmine we need:
377
    # First Name, Last Name, E-mail, Password
378
    # In Jira, the fullname and email are property (a little more hard to get)
379

    
380
    $doc.elements.each('/entity-engine-xml/OSUser') do |node|
381
      user = JiraUser.new(node)
382
      users.push(user)
383
    end
384

    
385
    return users
386
  end
387

    
388
  ISSUE_TYPE_MARKER = "(choose a Redmine Tracker)"
389
  DEFAULT_ISSUE_TYPE_MAP = {
390
    # Default map from Jira (key) to Redmine (value)
391
    # the comments on right side are Jira definitions - http://confluence.atlassian.com/display/JIRA/What+is+an+Issue#
392
    "Bug" => "Bug",              # A problem which impairs or prevents the functions of the product.
393
    "Improvement" => "Feature",  # An enhancement to an existing feature.
394
    "New Feature" => "Feature",  # A new feature of the product.
395
    "Task" => "Task",            # A task that needs to be done.
396
    "Custom Issue" => "Support", # A custom issue type, as defined by your organisation if required.
397
  }
398
  def self.get_jira_issue_types()
399
    # Issue Type
400
    issue_types = self.get_list_from_tag('/*/IssueType') 
401
    #migrated_issue_types = {"jira_type" => "redmine tracker"}
402
    migrated_issue_types = {}
403
    issue_types.each do |issue|
404
      migrated_issue_types[issue["name"]] = DEFAULT_ISSUE_TYPE_MAP.fetch(issue["name"], ISSUE_TYPE_MARKER)
405
      $MIGRATED_ISSUE_TYPES_BY_ID[issue["id"]] = issue["name"]
406
    end
407
    return migrated_issue_types
408
  end
409

    
410
  ISSUE_STATUS_MARKER = "(choose a Redmine Issue Status)"
411
  DEFAULT_ISSUE_STATUS_MAP = {
412
    # Default map from Jira (key) to Redmine (value)
413
    # the comments on right side are Jira definitions - http://confluence.atlassian.com/display/JIRA/What+is+an+Issue#
414
    "Open" => "New",                # This issue is in the initial 'Open' state, ready for the assignee to start work on it.
415
    "In Progress" => "In Progress", # This issue is being actively worked on at the moment by the assignee.
416
    "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'.
417
    "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.
418
    "Closed" => "Closed",           # This issue is complete. ## Be careful to choose one which a "issue closed" attribute marked :-)
419
  }
420
  def self.get_jira_status()
421
    # Issue Status
422
    issue_status = self.get_list_from_tag('/*/Status') 
423
    migrated_issue_status = {}
424
    issue_status.each do |issue|
425
      migrated_issue_status[issue["name"]] = DEFAULT_ISSUE_STATUS_MAP.fetch(issue["name"], ISSUE_STATUS_MARKER)
426
      $MIGRATED_ISSUE_STATUS_BY_ID[issue["id"]] = issue["name"]
427
    end
428
    return migrated_issue_status
429
  end
430

    
431
  ISSUE_PRIORITY_MARKER = "(choose a Redmine Enumeration Issue Priority)"
432
  DEFAULT_ISSUE_PRIORITY_MAP = {
433
    # Default map from Jira (key) to Redmine (value)
434
    # the comments on right side are Jira definitions - http://confluence.atlassian.com/display/JIRA/What+is+an+Issue#
435
     "Blocker" => "Immediate", # Highest priority. Indicates that this issue takes precedence over all others.
436
     "Critical" => "Urgent",   # Indicates that this issue is causing a problem and requires urgent attention.
437
     "Major" => "High",        # Indicates that this issue has a significant impact.
438
     "Minor" => "Normal",      # Indicates that this issue has a relatively minor impact.
439
     "Trivial" => "Low",       # Lowest priority.
440
  }
441
  def self.get_jira_priorities()
442
    # Issue Priority
443
    issue_priority = self.get_list_from_tag('/*/Priority') 
444
    migrated_issue_priority = {}
445
    issue_priority.each do |issue|
446
      migrated_issue_priority[issue["name"]] = DEFAULT_ISSUE_PRIORITY_MAP.fetch(issue["name"], ISSUE_PRIORITY_MARKER)
447
      $MIGRATED_ISSUE_PRIORITIES_BY_ID[issue["id"]] = issue["name"]
448
    end
449
    return migrated_issue_priority
450
  end
451

    
452
  def self.parse_comments()
453
    ret = []
454
    $doc.elements.each('/*/Action[@type="comment"]') do |node|
455
      comment = JiraComment.new(node)
456
      ret.push(comment)
457
    end
458
    return ret
459
  end 
460

    
461
  def self.parse_issues()
462
    ret = []
463
    $doc.elements.each('/*/Issue') do |node|
464
      issue = JiraIssue.new(node)
465
      ret.push(issue)
466
    end
467
    return ret
468
  end 
469

    
470
  def self.parse_attachments()
471
    attachs = []
472
    $doc.elements.each('/*/FileAttachment') do |node|
473
      attach = JiraAttachment.new(node)
474
      attachs.push(attach)
475
    end
476

    
477
    return attachs
478
  end
479
end
480

    
481

    
482

    
483

    
484
namespace :jira_migration do
485

    
486
  desc "Generates the configuration for the map things from Jira to Redmine"
487
  task :generate_conf => :environment do
488
    conf_file = JiraMigration::CONF_FILE
489
    conf_exists = File.exists?(conf_file)
490
    if conf_exists
491
      puts "You already have a conf file"
492
      print "You want overwrite it ? [y/N] "
493
      overwrite = STDIN.gets.match(/^y$/i)
494
    end
495

    
496
    if !conf_exists or overwrite
497
      # Let's give the user all options to fill out
498
      options = JiraMigration.get_all_options()
499

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

    
502
      puts "This migration script needs the migration table to continue "
503
      puts "Please... fill the map table on the file: '#{conf_file}' and run again the script"
504
      puts "To start the options again, just remove the file '#{conf_file} and run again the script"
505
      exit(0)
506
    end
507
  end
508

    
509
  desc "Gets the configuration from YAML"
510
  task :pre_conf => :environment do
511
    conf_file = JiraMigration::CONF_FILE
512
    conf_exists = File.exists?(conf_file)
513

    
514
    if !conf_exists 
515
      Rake::Task['jira_migration:generate_conf'].invoke
516
    end
517
    $confs = YAML.load_file(conf_file)
518
  end
519

    
520
  desc "Tests all parsers!"
521
  task :test_all_migrations => [:environment, :pre_conf,
522
                              :test_parse_projects, 
523
                              :test_parse_users, 
524
                              :test_parse_comments, 
525
                              :test_parse_issues, 
526
                             ] do
527
    puts "All parsers was run! :-)"
528
  end
529

    
530
  desc "Tests all parsers!"
531
  task :do_all_migrations => [:environment, :pre_conf,
532
                              :migrate_issue_types, 
533
                              :migrate_issue_status, 
534
                              :migrate_issue_priorities, 
535
                              :migrate_projects, 
536
                              :migrate_users, 
537
                              :migrate_issues, 
538
                              :migrate_comments, 
539
                              :migrate_attachments,
540
                             ] do
541
    puts "All migrations done! :-)"
542
  end
543

    
544

    
545
  desc "Migrates Jira Issue Types to Redmine Trackes"
546
  task :migrate_issue_types => [:environment, :pre_conf] do
547

    
548
    JiraMigration.get_jira_issue_types()
549
    types = $confs["types"]
550
    types.each do |key, value|
551
      t = Tracker.find_or_create_by_name(value)
552
      t.save!
553
      t.reload
554
      $MIGRATED_ISSUE_TYPES[key] = t
555
    end
556
    puts "Migrated issue types"
557
  end
558

    
559
  desc "Migrates Jira Issue Status to Redmine Status"
560
  task :migrate_issue_status => [:environment, :pre_conf] do
561
    JiraMigration.get_jira_status()
562
    status = $confs["status"]
563
    status.each do |key, value|
564
      s = IssueStatus.find_or_create_by_name(value)
565
      s.save!
566
      s.reload
567
      $MIGRATED_ISSUE_STATUS[key] = s
568
    end
569
    puts "Migrated issue status"
570
  end
571

    
572
  desc "Migrates Jira Issue Priorities to Redmine Priorities"
573
  task :migrate_issue_priorities => [:environment, :pre_conf] do
574
    JiraMigration.get_jira_priorities()
575
    priorities = $confs["priorities"]
576

    
577
    priorities.each do |key, value|
578
      p = IssuePriority.find_or_create_by_name(value)
579
      p.save!
580
      p.reload
581
      $MIGRATED_ISSUE_PRIORITIES[key] = p
582
    end
583
    puts "Migrated issue priorities"
584
  end
585

    
586
  desc "Migrates Jira Projects to Redmine Projects"
587
  task :migrate_projects => :environment do
588
    projects = JiraMigration.parse_projects()
589
    projects.each do |p|
590
      #pp(p)
591
      p.migrate
592
    end
593
  end
594

    
595
  desc "Migrates Jira Users to Redmine Users"
596
  task :migrate_users => :environment do
597
    users = JiraMigration.parse_users()
598
    users.each do |u|
599
      #pp(u)
600
      u.migrate
601
    end
602
  end
603

    
604
  desc "Migrates Jira Issues to Redmine Issues"
605
  task :migrate_issues => :environment do
606
    issues = JiraMigration.parse_issues()
607
    issues.each do |i|
608
      #pp(i)
609
      i.migrate
610
    end
611
  end
612

    
613
  desc "Migrates Jira Issues Comments to Redmine Issues Journals (Notes)"
614
  task :migrate_comments => :environment do
615
    comments = JiraMigration.parse_comments()
616
    comments.each do |c|
617
      #pp(c)
618
      c.migrate
619
    end
620
  end
621

    
622
  desc "Migrates Jira Issues Attachments to Redmine Attachments"
623
  task :migrate_attachments => :environment do
624
    attachs = JiraMigration.parse_attachments()
625
    attachs.each do |a|
626
      #pp(c)
627
      a.migrate
628
    end
629
  end
630

    
631
  # Tests.....
632
  desc "Just pretty print Jira Projects on screen"
633
  task :test_parse_projects => :environment do
634
    projects = JiraMigration.parse_projects()
635
    projects.each {|p| pp(p.run_all_redmine_fields) }
636
  end
637

    
638
  desc "Just pretty print Jira Users on screen"
639
  task :test_parse_users => :environment do
640
    users = JiraMigration.parse_users()
641
    users.each {|u| pp( u.run_all_redmine_fields) }
642
  end
643

    
644
  desc "Just pretty print Jira Comments on screen"
645
  task :test_parse_comments => :environment do
646
    comments = JiraMigration.parse_comments()
647
    comments.each {|c| pp( c.run_all_redmine_fields) }
648
  end
649

    
650
  desc "Just pretty print Jira Issues on screen"
651
  task :test_parse_issues => :environment do
652
    issues = JiraMigration.parse_issues()
653
    issues.each {|i| pp( i.run_all_redmine_fields) }
654
  end
655
end
(7-7/9)