PageRenderTime 49ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/tasks/migrate_from_trac.rake

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