Advertisement
Guest User

Untitled

a guest
Jun 2nd, 2018
113
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 30.63 KB | None | 0 0
  1. # redMine - project management software
  2. # Copyright (C) 2006-2007 Jean-Philippe Lang
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; either version 2
  7. # of the License, or (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  17.  
  18. require 'active_record'
  19. require 'iconv'
  20. require 'pp'
  21.  
  22. namespace :redmine do
  23. desc 'Trac migration script'
  24. task :migrate_from_trac => :environment do
  25.  
  26. module TracMigrate
  27. TICKET_MAP = []
  28.  
  29. DEFAULT_STATUS = IssueStatus.default
  30. assigned_status = IssueStatus.find_by_position(2)
  31. resolved_status = IssueStatus.find_by_position(3)
  32. feedback_status = IssueStatus.find_by_position(4)
  33. closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
  34. STATUS_MAPPING = {'new' => DEFAULT_STATUS,
  35. 'reopened' => feedback_status,
  36. 'assigned' => assigned_status,
  37. 'closed' => closed_status
  38. }
  39.  
  40. priorities = IssuePriority.all
  41. DEFAULT_PRIORITY = priorities[0]
  42. PRIORITY_MAPPING = {'lowest' => priorities[0],
  43. 'low' => priorities[0],
  44. 'normal' => priorities[1],
  45. 'high' => priorities[2],
  46. 'highest' => priorities[3],
  47. # ---
  48. 'trivial' => priorities[0],
  49. 'minor' => priorities[1],
  50. 'major' => priorities[2],
  51. 'critical' => priorities[3],
  52. 'blocker' => priorities[4]
  53. }
  54.  
  55. TRACKER_BUG = Tracker.find_by_position(1)
  56. TRACKER_FEATURE = Tracker.find_by_position(2)
  57. DEFAULT_TRACKER = TRACKER_BUG
  58. TRACKER_MAPPING = {'defect' => TRACKER_BUG,
  59. 'enhancement' => TRACKER_FEATURE,
  60. 'task' => TRACKER_FEATURE,
  61. 'patch' =>TRACKER_FEATURE
  62. }
  63.  
  64. roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
  65. manager_role = roles[0]
  66. developer_role = roles[1]
  67. DEFAULT_ROLE = roles.last
  68. ROLE_MAPPING = {'admin' => manager_role,
  69. 'developer' => developer_role
  70. }
  71.  
  72. PROJECT_CUSTOM_FIELD_VALUES = {}
  73.  
  74. class ::Time
  75. class << self
  76. alias :real_now :now
  77. def now
  78. real_now - @fake_diff.to_i
  79. end
  80. def fake(time)
  81. @fake_diff = real_now - time
  82. res = yield
  83. @fake_diff = 0
  84. res
  85. end
  86. end
  87. end
  88.  
  89. class TracComponent < ActiveRecord::Base
  90. set_table_name :component
  91. end
  92.  
  93. class TracMilestone < ActiveRecord::Base
  94. set_table_name :milestone
  95. # If this attribute is set a milestone has a defined target timepoint
  96. def due
  97. if read_attribute(:due) && read_attribute(:due) > 0
  98. Time.at(read_attribute(:due)).to_date
  99. else
  100. nil
  101. end
  102. end
  103. # This is the real timepoint at which the milestone has finished.
  104. def completed
  105. if read_attribute(:completed) && read_attribute(:completed) > 0
  106. Time.at(read_attribute(:completed)).to_date
  107. else
  108. nil
  109. end
  110. end
  111.  
  112. def description
  113. # Attribute is named descr in Trac v0.8.x
  114. has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
  115. end
  116. end
  117.  
  118. class TracTicketCustom < ActiveRecord::Base
  119. set_table_name :ticket_custom
  120. end
  121.  
  122. class TracAttachment < ActiveRecord::Base
  123. set_table_name :attachment
  124. set_inheritance_column :none
  125.  
  126. def time; Time.at(read_attribute(:time)) end
  127.  
  128. def original_filename
  129. filename
  130. end
  131.  
  132. def content_type
  133. ''
  134. end
  135.  
  136. def exist?
  137. File.file? trac_fullpath
  138. end
  139.  
  140. def open
  141. File.open("#{trac_fullpath}", 'rb') {|f|
  142. @file = f
  143. yield self
  144. }
  145. end
  146.  
  147. def read(*args)
  148. @file.read(*args)
  149. end
  150.  
  151. def description
  152. read_attribute(:description).to_s.slice(0,255)
  153. end
  154.  
  155. private
  156. def trac_fullpath
  157. attachment_type = read_attribute(:type)
  158. trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
  159. "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
  160. end
  161. end
  162.  
  163. class TracTicket < ActiveRecord::Base
  164. set_table_name :ticket
  165. set_inheritance_column :none
  166.  
  167. # ticket changes: only migrate status changes and comments
  168. has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
  169. has_many :attachments, :class_name => "TracAttachment",
  170. :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
  171. " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
  172. ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
  173. has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
  174.  
  175. def ticket_type
  176. read_attribute(:type)
  177. end
  178.  
  179. def summary
  180. read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
  181. end
  182.  
  183. def description
  184. read_attribute(:description).blank? ? summary : read_attribute(:description)
  185. end
  186.  
  187. def time; Time.at(read_attribute(:time)) end
  188. def changetime; Time.at(read_attribute(:changetime)) end
  189. end
  190.  
  191. class TracTicketChange < ActiveRecord::Base
  192. set_table_name :ticket_change
  193.  
  194. def time; Time.at(read_attribute(:time)) end
  195. end
  196.  
  197. TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
  198. TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
  199. TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
  200. TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
  201. TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
  202. WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
  203. CamelCase TitleIndex)
  204.  
  205. class TracWikiPage < ActiveRecord::Base
  206. set_table_name :wiki
  207. set_primary_key :name
  208.  
  209. has_many :attachments, :class_name => "TracAttachment",
  210. :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
  211. " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
  212. ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
  213.  
  214. def self.columns
  215. # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
  216. super.select {|column| column.name.to_s != 'readonly'}
  217. end
  218.  
  219. def time; Time.at(read_attribute(:time)) end
  220. end
  221.  
  222. class TracPermission < ActiveRecord::Base
  223. set_table_name :permission
  224. end
  225.  
  226. class TracSessionAttribute < ActiveRecord::Base
  227. set_table_name :session_attribute
  228. end
  229.  
  230. def self.find_or_create_user(username, project_member = false)
  231. return User.anonymous if username.blank?
  232.  
  233. u = User.find_by_login(username)
  234. if !u
  235. # Create a new user if not found
  236. mail = username[0,limit_for(User, 'mail')]
  237. if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
  238. mail = mail_attr.value
  239. end
  240. mail = "#{mail}@foo.bar" unless mail.include?("@")
  241.  
  242. name = username
  243. if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
  244. name = name_attr.value
  245. end
  246. name =~ (/(.*)(\s+\w+)?/)
  247. fn = $1.strip
  248. ln = ($2 || '-').strip
  249.  
  250. u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
  251. :firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
  252. :lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-')
  253.  
  254. u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
  255. u.password = 'trac'
  256. u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
  257. # finally, a default user is used if the new user is not valid
  258. u = User.find(:first) unless u.save
  259. end
  260. # Make sure he is a member of the project
  261. if project_member && !u.member_of?(@target_project)
  262. role = DEFAULT_ROLE
  263. if u.admin
  264. role = ROLE_MAPPING['admin']
  265. elsif TracPermission.find_by_username_and_action(username, 'developer')
  266. role = ROLE_MAPPING['developer']
  267. end
  268. Member.create(:user => u, :project => @target_project, :roles => [role])
  269. u.reload
  270. end
  271. u
  272. end
  273.  
  274. # Basic wiki syntax conversion
  275. def self.convert_wiki_text(text)
  276. # Titles
  277. text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
  278. # External Links
  279. text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
  280. # Ticket links:
  281. # [ticket:234 Text],[ticket:234 This is a test]
  282. text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
  283. # ticket:1234
  284. # #1 is working cause Redmine uses the same syntax.
  285. text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
  286. # Milestone links:
  287. # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
  288. # The text "Milestone 0.1.0 (Mercury)" is not converted,
  289. # cause Redmine's wiki does not support this.
  290. text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
  291. # [milestone:"0.1.0 Mercury"]
  292. text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
  293. text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
  294. # milestone:0.1.0
  295. text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
  296. text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
  297. # Internal Links
  298. text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
  299. text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
  300. text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
  301. text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
  302. text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
  303. text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
  304.  
  305. # Links to pages UsingJustWikiCaps
  306. text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
  307. # Normalize things that were supposed to not be links
  308. # like !NotALink
  309. text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
  310. # Revisions links
  311. text = text.gsub(/\[(\d+)\]/, 'r\1')
  312. # Ticket number re-writing
  313. text = text.gsub(/#(\d+)/) do |s|
  314. if $1.length < 10
  315. # TICKET_MAP[$1.to_i] ||= $1
  316. "\##{TICKET_MAP[$1.to_i] || $1}"
  317. else
  318. s
  319. end
  320. end
  321. # We would like to convert the Code highlighting too
  322. # This will go into the next line.
  323. shebang_line = false
  324. # Reguar expression for start of code
  325. pre_re = /\{\{\{/
  326. # Code hightlighing...
  327. shebang_re = /^\#\!([a-z]+)/
  328. # Regular expression for end of code
  329. pre_end_re = /\}\}\}/
  330.  
  331. # Go through the whole text..extract it line by line
  332. text = text.gsub(/^(.*)$/) do |line|
  333. m_pre = pre_re.match(line)
  334. if m_pre
  335. line = '<pre>'
  336. else
  337. m_sl = shebang_re.match(line)
  338. if m_sl
  339. shebang_line = true
  340. line = '<code class="' + m_sl[1] + '">'
  341. end
  342. m_pre_end = pre_end_re.match(line)
  343. if m_pre_end
  344. line = '</pre>'
  345. if shebang_line
  346. line = '</code>' + line
  347. end
  348. end
  349. end
  350. line
  351. end
  352.  
  353. # Highlighting
  354. text = text.gsub(/'''''([^\s])/, '_*\1')
  355. text = text.gsub(/([^\s])'''''/, '\1*_')
  356. text = text.gsub(/'''/, '*')
  357. text = text.gsub(/''/, '_')
  358. text = text.gsub(/__/, '+')
  359. text = text.gsub(/~~/, '-')
  360. text = text.gsub(/`/, '@')
  361. text = text.gsub(/,,/, '~')
  362. # Lists
  363. text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
  364.  
  365. text
  366. end
  367.  
  368. def self.migrate
  369. establish_connection
  370.  
  371. # Quick database test
  372. TracComponent.count
  373.  
  374. migrated_components = 0
  375. migrated_milestones = 0
  376. migrated_tickets = 0
  377. migrated_custom_values = 0
  378. migrated_ticket_attachments = 0
  379. migrated_wiki_edits = 0
  380. migrated_wiki_attachments = 0
  381.  
  382. #Wiki system initializing...
  383. @target_project.wiki.destroy if @target_project.wiki
  384. @target_project.reload
  385. wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
  386. wiki_edit_count = 0
  387.  
  388. # Components
  389. print "Migrating components"
  390. issues_category_map = {}
  391. TracComponent.find(:all).each do |component|
  392. print '.'
  393. STDOUT.flush
  394. c = IssueCategory.new :project => @target_project,
  395. :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
  396. next unless c.save
  397. issues_category_map[component.name] = c
  398. migrated_components += 1
  399. end
  400. puts
  401.  
  402. # Milestones
  403. print "Migrating milestones"
  404. version_map = {}
  405. TracMilestone.find(:all).each do |milestone|
  406. print '.'
  407. STDOUT.flush
  408. # First we try to find the wiki page...
  409. p = wiki.find_or_new_page(milestone.name.to_s)
  410. p.content = WikiContent.new(:page => p) if p.new_record?
  411. p.content.text = milestone.description.to_s
  412. p.content.author = find_or_create_user('trac')
  413. p.content.comments = 'Milestone'
  414. p.save
  415.  
  416. v = Version.new :project => @target_project,
  417. :name => encode(milestone.name[0, limit_for(Version, 'name')]),
  418. :description => nil,
  419. :wiki_page_title => milestone.name.to_s,
  420. :effective_date => milestone.completed
  421.  
  422. next unless v.save
  423. version_map[milestone.name] = v
  424. migrated_milestones += 1
  425. end
  426. puts
  427.  
  428. # Custom fields
  429. # TODO: read trac.ini instead
  430. print "Migrating custom fields"
  431. custom_field_map = {}
  432. TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
  433. print '.'
  434. STDOUT.flush
  435. # Redmine custom field name
  436. field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
  437. # Find if the custom already exists in Redmine
  438. f = IssueCustomField.find_by_name(field_name)
  439. # Or create a new one
  440. f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
  441. :field_format => 'string')
  442.  
  443. next if f.new_record?
  444. f.trackers = Tracker.find(:all)
  445. f.projects << @target_project
  446. custom_field_map[field.name] = f
  447. end
  448. puts
  449.  
  450. # Trac 'resolution' field as a Redmine custom field
  451. r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
  452. r = IssueCustomField.new(:name => 'Resolution',
  453. :field_format => 'list',
  454. :is_filter => true) if r.nil?
  455. r.trackers = Tracker.find(:all)
  456. r.projects << @target_project
  457. r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
  458. r.save!
  459. custom_field_map['resolution'] = r
  460.  
  461. # Tickets
  462. print "Migrating tickets"
  463. TracTicket.find_each(:batch_size => 200) do |ticket|
  464. print '.'
  465. STDOUT.flush
  466. i = Issue.new :project => @target_project,
  467. :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
  468. :description => convert_wiki_text(encode(ticket.description)),
  469. :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
  470. :created_on => ticket.time
  471. i.author = find_or_create_user(ticket.reporter)
  472. i.category = issues_category_map[ticket.component] unless ticket.component.blank?
  473. i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
  474. i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
  475. i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
  476. i.id = ticket.id unless Issue.exists?(ticket.id)
  477. next unless Time.fake(ticket.changetime) { i.save }
  478. TICKET_MAP[ticket.id] = i.id
  479. migrated_tickets += 1
  480.  
  481. # Owner
  482. unless ticket.owner.blank?
  483. i.assigned_to = find_or_create_user(ticket.owner, true)
  484. Time.fake(ticket.changetime) { i.save }
  485. end
  486.  
  487. # Comments and status/resolution changes
  488. ticket.changes.group_by(&:time).each do |time, changeset|
  489. status_change = changeset.select {|change| change.field == 'status'}.first
  490. resolution_change = changeset.select {|change| change.field == 'resolution'}.first
  491. comment_change = changeset.select {|change| change.field == 'comment'}.first
  492.  
  493. n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
  494. :created_on => time
  495. n.user = find_or_create_user(changeset.first.author)
  496. n.journalized = i
  497. if status_change &&
  498. STATUS_MAPPING[status_change.oldvalue] &&
  499. STATUS_MAPPING[status_change.newvalue] &&
  500. (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
  501. n.details << JournalDetail.new(:property => 'attr',
  502. :prop_key => 'status_id',
  503. :old_value => STATUS_MAPPING[status_change.oldvalue].id,
  504. :value => STATUS_MAPPING[status_change.newvalue].id)
  505. end
  506. if resolution_change
  507. n.details << JournalDetail.new(:property => 'cf',
  508. :prop_key => custom_field_map['resolution'].id,
  509. :old_value => resolution_change.oldvalue,
  510. :value => resolution_change.newvalue)
  511. end
  512. n.save unless n.details.empty? && n.notes.blank?
  513. end
  514.  
  515. # Attachments
  516. ticket.attachments.each do |attachment|
  517. next unless attachment.exist?
  518. attachment.open {
  519. a = Attachment.new :created_on => attachment.time
  520. a.file = attachment
  521. a.author = find_or_create_user(attachment.author)
  522. a.container = i
  523. a.description = attachment.description
  524. migrated_ticket_attachments += 1 if a.save
  525. }
  526. end
  527.  
  528. # Custom fields
  529. custom_values = ticket.customs.inject({}) do |h, custom|
  530. if custom_field = custom_field_map[custom.name]
  531. h[custom_field.id] = custom.value
  532. migrated_custom_values += 1
  533. end
  534. h
  535. end
  536. if custom_field_map['resolution'] && !ticket.resolution.blank?
  537. custom_values[custom_field_map['resolution'].id] = ticket.resolution
  538. end
  539. i.custom_field_values = custom_values
  540. i.save_custom_field_values
  541. end
  542.  
  543. # update issue id sequence if needed (postgresql)
  544. Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
  545. puts
  546.  
  547. # Wiki
  548. print "Migrating wiki"
  549. if wiki.save
  550. TracWikiPage.find(:all, :order => 'name, version').each do |page|
  551. # Do not migrate Trac manual wiki pages
  552. next if TRAC_WIKI_PAGES.include?(page.name)
  553. wiki_edit_count += 1
  554. print '.'
  555. STDOUT.flush
  556. p = wiki.find_or_new_page(page.name)
  557. p.content = WikiContent.new(:page => p) if p.new_record?
  558. p.content.text = page.text
  559. p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
  560. p.content.comments = page.comment
  561. Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
  562.  
  563. next if p.content.new_record?
  564. migrated_wiki_edits += 1
  565.  
  566. # Attachments
  567. page.attachments.each do |attachment|
  568. next unless attachment.exist?
  569. next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
  570. attachment.open {
  571. a = Attachment.new :created_on => attachment.time
  572. a.file = attachment
  573. a.author = find_or_create_user(attachment.author)
  574. a.description = attachment.description
  575. a.container = p
  576. migrated_wiki_attachments += 1 if a.save
  577. }
  578. end
  579. end
  580.  
  581. wiki.reload
  582. wiki.pages.each do |page|
  583. page.content.text = convert_wiki_text(page.content.text)
  584. Time.fake(page.content.updated_on) { page.content.save }
  585. end
  586. end
  587. puts
  588.  
  589. puts
  590. puts "Components: #{migrated_components}/#{TracComponent.count}"
  591. puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
  592. puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
  593. puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
  594. puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
  595. puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
  596. puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
  597. end
  598.  
  599. def self.limit_for(klass, attribute)
  600. klass.columns_hash[attribute.to_s].limit
  601. end
  602.  
  603. def self.encoding(charset)
  604. @ic = Iconv.new('UTF-8', charset)
  605. rescue Iconv::InvalidEncoding
  606. puts "Invalid encoding!"
  607. return false
  608. end
  609.  
  610. def self.set_trac_directory(path)
  611. @@trac_directory = path
  612. raise "This directory doesn't exist!" unless File.directory?(path)
  613. raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
  614. @@trac_directory
  615. rescue Exception => e
  616. puts e
  617. return false
  618. end
  619.  
  620. def self.trac_directory
  621. @@trac_directory
  622. end
  623.  
  624. def self.set_trac_adapter(adapter)
  625. return false if adapter.blank?
  626. raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
  627. # If adapter is sqlite or sqlite3, make sure that trac.db exists
  628. raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
  629. @@trac_adapter = adapter
  630. rescue Exception => e
  631. puts e
  632. return false
  633. end
  634.  
  635. def self.set_trac_db_host(host)
  636. return nil if host.blank?
  637. @@trac_db_host = host
  638. end
  639.  
  640. def self.set_trac_db_port(port)
  641. return nil if port.to_i == 0
  642. @@trac_db_port = port.to_i
  643. end
  644.  
  645. def self.set_trac_db_name(name)
  646. return nil if name.blank?
  647. @@trac_db_name = name
  648. end
  649.  
  650. def self.set_trac_db_username(username)
  651. @@trac_db_username = username
  652. end
  653.  
  654. def self.set_trac_db_password(password)
  655. @@trac_db_password = password
  656. end
  657.  
  658. def self.set_trac_db_schema(schema)
  659. @@trac_db_schema = schema
  660. end
  661.  
  662. def self.set_project_custom_field_value(custom_field, val)
  663. PROJECT_CUSTOM_FIELD_VALUES[custom_field.id] = val
  664.  
  665. # validate input
  666. v = CustomValue.new(:custom_field => custom_field.clone, :value => val, :customized => nil)
  667.  
  668. # Return false if value is not valid
  669. v.errors.each_full { |msg| puts msg } if !v.valid?
  670. v.valid?
  671. end
  672.  
  673. mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
  674.  
  675. def self.trac_db_path; "#{trac_directory}/db/trac.db" end
  676. def self.trac_attachments_directory; "#{trac_directory}/attachments" end
  677.  
  678. def self.target_project_identifier(identifier)
  679. project = Project.find_by_identifier(identifier)
  680. if !project
  681. # create the target project
  682. project = Project.new :name => identifier.humanize,
  683. :description => ''
  684. project.custom_field_values = PROJECT_CUSTOM_FIELD_VALUES
  685. project.identifier = identifier
  686. if project.save
  687. # enable issues and wiki for the created project
  688. project.enabled_module_names = ['issue_tracking', 'wiki']
  689. else
  690. puts "Unable to create a project with identifier '#{identifier}'!"
  691. project.errors.each_full { |msg| puts msg }
  692. end
  693. else
  694. puts
  695. puts "This project already exists in your Redmine database."
  696. print "Are you sure you want to append data to this project ? [Y/n] "
  697. STDOUT.flush
  698. exit if STDIN.gets.match(/^n$/i)
  699. end
  700. project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
  701. project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
  702. @target_project = project.new_record? ? nil : project
  703. @target_project.reload
  704. end
  705.  
  706. def self.connection_params
  707. if %w(sqlite sqlite3).include?(trac_adapter)
  708. {:adapter => trac_adapter,
  709. :database => trac_db_path}
  710. else
  711. {:adapter => trac_adapter,
  712. :database => trac_db_name,
  713. :host => trac_db_host,
  714. :port => trac_db_port,
  715. :username => trac_db_username,
  716. :password => trac_db_password,
  717. :schema_search_path => trac_db_schema
  718. }
  719. end
  720. end
  721.  
  722. def self.establish_connection
  723. constants.each do |const|
  724. klass = const_get(const)
  725. next unless klass.respond_to? 'establish_connection'
  726. klass.establish_connection connection_params
  727. end
  728. end
  729.  
  730. private
  731. def self.encode(text)
  732. @ic.iconv text
  733. rescue
  734. text
  735. end
  736. end
  737.  
  738. puts
  739. if Redmine::DefaultData::Loader.no_data?
  740. puts "Redmine configuration need to be loaded before importing data."
  741. puts "Please, run this first:"
  742. puts
  743. puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
  744. exit
  745. end
  746.  
  747. puts "WARNING: a new project will be added to Redmine during this process."
  748. print "Are you sure you want to continue ? [y/N] "
  749. STDOUT.flush
  750. break unless STDIN.gets.match(/^y$/i)
  751. puts
  752.  
  753. def prompt(text, options = {}, &block)
  754. default = options[:default] || ''
  755. while true
  756. print "#{text} [#{default}]: "
  757. STDOUT.flush
  758. value = STDIN.gets.chomp!
  759. value = default if value.blank?
  760. break if yield value
  761. end
  762. end
  763.  
  764. DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
  765.  
  766. prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
  767. prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
  768. unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
  769. prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
  770. prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
  771. prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
  772. prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
  773. prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
  774. prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
  775. end
  776. prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
  777.  
  778. Project.new.custom_field_values.each do |custom_field_value|
  779. custom_field = custom_field_value.custom_field
  780.  
  781. name = custom_field.name
  782. options = {}
  783.  
  784. name += '*' if custom_field.is_required
  785. options[:default] = custom_field.default_value if custom_field.default_value
  786.  
  787. prompt(name, options) do |val|
  788. TracMigrate.set_project_custom_field_value custom_field, val
  789. end
  790. end
  791.  
  792. prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
  793. puts
  794.  
  795. # Turn off email notifications
  796. Setting.notified_events = []
  797.  
  798. TracMigrate.migrate
  799. end
  800. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement