You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

migrate_from_trac.rake 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  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. 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.find :first, :conditions => { :is_closed => true }
  30. STATUS_MAPPING = {'new' => DEFAULT_STATUS,
  31. 'reopened' => feedback_status,
  32. 'assigned' => assigned_status,
  33. 'closed' => closed_status
  34. }
  35. priorities = Enumeration.get_values('IPRI')
  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.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
  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. set_table_name :component
  80. end
  81. class TracMilestone < ActiveRecord::Base
  82. set_table_name :milestone
  83. def due
  84. if read_attribute(:due) && read_attribute(:due) > 0
  85. Time.at(read_attribute(:due)).to_date
  86. else
  87. nil
  88. end
  89. end
  90. end
  91. class TracTicketCustom < ActiveRecord::Base
  92. set_table_name :ticket_custom
  93. end
  94. class TracAttachment < ActiveRecord::Base
  95. set_table_name :attachment
  96. set_inheritance_column :none
  97. def time; Time.at(read_attribute(:time)) end
  98. def original_filename
  99. filename
  100. end
  101. def content_type
  102. Redmine::MimeType.of(filename) || ''
  103. end
  104. def exist?
  105. File.file? trac_fullpath
  106. end
  107. def read
  108. File.open("#{trac_fullpath}", 'rb').read
  109. end
  110. def description
  111. read_attribute(:description).to_s.slice(0,255)
  112. end
  113. private
  114. def trac_fullpath
  115. attachment_type = read_attribute(:type)
  116. trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
  117. "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
  118. end
  119. end
  120. class TracTicket < ActiveRecord::Base
  121. set_table_name :ticket
  122. set_inheritance_column :none
  123. # ticket changes: only migrate status changes and comments
  124. has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
  125. has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'ticket'"
  126. has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
  127. def ticket_type
  128. read_attribute(:type)
  129. end
  130. def summary
  131. read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
  132. end
  133. def description
  134. read_attribute(:description).blank? ? summary : read_attribute(:description)
  135. end
  136. def time; Time.at(read_attribute(:time)) end
  137. def changetime; Time.at(read_attribute(:changetime)) end
  138. end
  139. class TracTicketChange < ActiveRecord::Base
  140. set_table_name :ticket_change
  141. def time; Time.at(read_attribute(:time)) end
  142. end
  143. TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
  144. TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
  145. TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
  146. TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
  147. TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
  148. WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
  149. CamelCase TitleIndex)
  150. class TracWikiPage < ActiveRecord::Base
  151. set_table_name :wiki
  152. set_primary_key :name
  153. has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'wiki'"
  154. def self.columns
  155. # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
  156. super.select {|column| column.name.to_s != 'readonly'}
  157. end
  158. def time; Time.at(read_attribute(:time)) end
  159. end
  160. class TracPermission < ActiveRecord::Base
  161. set_table_name :permission
  162. end
  163. def self.find_or_create_user(username, project_member = false)
  164. return User.anonymous if username.blank?
  165. u = User.find_by_login(username)
  166. if !u
  167. # Create a new user if not found
  168. mail = username[0,limit_for(User, 'mail')]
  169. mail = "#{mail}@foo.bar" unless mail.include?("@")
  170. u = User.new :firstname => username[0,limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
  171. :lastname => '-',
  172. :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-')
  173. u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
  174. u.password = 'trac'
  175. u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
  176. # finally, a default user is used if the new user is not valid
  177. u = User.find(:first) unless u.save
  178. end
  179. # Make sure he is a member of the project
  180. if project_member && !u.member_of?(@target_project)
  181. role = DEFAULT_ROLE
  182. if u.admin
  183. role = ROLE_MAPPING['admin']
  184. elsif TracPermission.find_by_username_and_action(username, 'developer')
  185. role = ROLE_MAPPING['developer']
  186. end
  187. Member.create(:user => u, :project => @target_project, :role => role)
  188. u.reload
  189. end
  190. u
  191. end
  192. # Basic wiki syntax conversion
  193. def self.convert_wiki_text(text)
  194. # Titles
  195. text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
  196. # External Links
  197. text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
  198. # Internal Links
  199. text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
  200. text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
  201. text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
  202. text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
  203. text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
  204. text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
  205. # Links to pages UsingJustWikiCaps
  206. text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
  207. # Normalize things that were supposed to not be links
  208. # like !NotALink
  209. text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
  210. # Revisions links
  211. text = text.gsub(/\[(\d+)\]/, 'r\1')
  212. # Ticket number re-writing
  213. text = text.gsub(/#(\d+)/) do |s|
  214. if $1.length < 10
  215. TICKET_MAP[$1.to_i] ||= $1
  216. "\##{TICKET_MAP[$1.to_i] || $1}"
  217. else
  218. s
  219. end
  220. end
  221. # Preformatted blocks
  222. text = text.gsub(/\{\{\{/, '<pre>')
  223. text = text.gsub(/\}\}\}/, '</pre>')
  224. # Highlighting
  225. text = text.gsub(/'''''([^\s])/, '_*\1')
  226. text = text.gsub(/([^\s])'''''/, '\1*_')
  227. text = text.gsub(/'''/, '*')
  228. text = text.gsub(/''/, '_')
  229. text = text.gsub(/__/, '+')
  230. text = text.gsub(/~~/, '-')
  231. text = text.gsub(/`/, '@')
  232. text = text.gsub(/,,/, '~')
  233. # Lists
  234. text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
  235. text
  236. end
  237. def self.migrate
  238. establish_connection
  239. # Quick database test
  240. TracComponent.count
  241. migrated_components = 0
  242. migrated_milestones = 0
  243. migrated_tickets = 0
  244. migrated_custom_values = 0
  245. migrated_ticket_attachments = 0
  246. migrated_wiki_edits = 0
  247. migrated_wiki_attachments = 0
  248. # Components
  249. print "Migrating components"
  250. issues_category_map = {}
  251. TracComponent.find(:all).each do |component|
  252. print '.'
  253. STDOUT.flush
  254. c = IssueCategory.new :project => @target_project,
  255. :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
  256. next unless c.save
  257. issues_category_map[component.name] = c
  258. migrated_components += 1
  259. end
  260. puts
  261. # Milestones
  262. print "Migrating milestones"
  263. version_map = {}
  264. TracMilestone.find(:all).each do |milestone|
  265. print '.'
  266. STDOUT.flush
  267. v = Version.new :project => @target_project,
  268. :name => encode(milestone.name[0, limit_for(Version, 'name')]),
  269. :description => encode(milestone.description.to_s[0, limit_for(Version, 'description')]),
  270. :effective_date => milestone.due
  271. next unless v.save
  272. version_map[milestone.name] = v
  273. migrated_milestones += 1
  274. end
  275. puts
  276. # Custom fields
  277. # TODO: read trac.ini instead
  278. print "Migrating custom fields"
  279. custom_field_map = {}
  280. TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
  281. print '.'
  282. STDOUT.flush
  283. # Redmine custom field name
  284. field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
  285. # Find if the custom already exists in Redmine
  286. f = IssueCustomField.find_by_name(field_name)
  287. # Or create a new one
  288. f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
  289. :field_format => 'string')
  290. next if f.new_record?
  291. f.trackers = Tracker.find(:all)
  292. f.projects << @target_project
  293. custom_field_map[field.name] = f
  294. end
  295. puts
  296. # Trac 'resolution' field as a Redmine custom field
  297. r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
  298. r = IssueCustomField.new(:name => 'Resolution',
  299. :field_format => 'list',
  300. :is_filter => true) if r.nil?
  301. r.trackers = Tracker.find(:all)
  302. r.projects << @target_project
  303. r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
  304. r.save!
  305. custom_field_map['resolution'] = r
  306. # Tickets
  307. print "Migrating tickets"
  308. TracTicket.find(:all, :order => 'id ASC').each do |ticket|
  309. print '.'
  310. STDOUT.flush
  311. i = Issue.new :project => @target_project,
  312. :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
  313. :description => convert_wiki_text(encode(ticket.description)),
  314. :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
  315. :created_on => ticket.time
  316. i.author = find_or_create_user(ticket.reporter)
  317. i.category = issues_category_map[ticket.component] unless ticket.component.blank?
  318. i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
  319. i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
  320. i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
  321. i.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank?
  322. i.id = ticket.id unless Issue.exists?(ticket.id)
  323. next unless Time.fake(ticket.changetime) { i.save }
  324. TICKET_MAP[ticket.id] = i.id
  325. migrated_tickets += 1
  326. # Owner
  327. unless ticket.owner.blank?
  328. i.assigned_to = find_or_create_user(ticket.owner, true)
  329. Time.fake(ticket.changetime) { i.save }
  330. end
  331. # Comments and status/resolution changes
  332. ticket.changes.group_by(&:time).each do |time, changeset|
  333. status_change = changeset.select {|change| change.field == 'status'}.first
  334. resolution_change = changeset.select {|change| change.field == 'resolution'}.first
  335. comment_change = changeset.select {|change| change.field == 'comment'}.first
  336. n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
  337. :created_on => time
  338. n.user = find_or_create_user(changeset.first.author)
  339. n.journalized = i
  340. if status_change &&
  341. STATUS_MAPPING[status_change.oldvalue] &&
  342. STATUS_MAPPING[status_change.newvalue] &&
  343. (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
  344. n.details << JournalDetail.new(:property => 'attr',
  345. :prop_key => 'status_id',
  346. :old_value => STATUS_MAPPING[status_change.oldvalue].id,
  347. :value => STATUS_MAPPING[status_change.newvalue].id)
  348. end
  349. if resolution_change
  350. n.details << JournalDetail.new(:property => 'cf',
  351. :prop_key => custom_field_map['resolution'].id,
  352. :old_value => resolution_change.oldvalue,
  353. :value => resolution_change.newvalue)
  354. end
  355. n.save unless n.details.empty? && n.notes.blank?
  356. end
  357. # Attachments
  358. ticket.attachments.each do |attachment|
  359. next unless attachment.exist?
  360. a = Attachment.new :created_on => attachment.time
  361. a.file = attachment
  362. a.author = find_or_create_user(attachment.author)
  363. a.container = i
  364. a.description = attachment.description
  365. migrated_ticket_attachments += 1 if a.save
  366. end
  367. # Custom fields
  368. ticket.customs.each do |custom|
  369. next if custom_field_map[custom.name].nil?
  370. v = CustomValue.new :custom_field => custom_field_map[custom.name],
  371. :value => custom.value
  372. v.customized = i
  373. next unless v.save
  374. migrated_custom_values += 1
  375. end
  376. end
  377. # update issue id sequence if needed (postgresql)
  378. Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
  379. puts
  380. # Wiki
  381. print "Migrating wiki"
  382. @target_project.wiki.destroy if @target_project.wiki
  383. @target_project.reload
  384. wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
  385. wiki_edit_count = 0
  386. if wiki.save
  387. TracWikiPage.find(:all, :order => 'name, version').each do |page|
  388. # Do not migrate Trac manual wiki pages
  389. next if TRAC_WIKI_PAGES.include?(page.name)
  390. wiki_edit_count += 1
  391. print '.'
  392. STDOUT.flush
  393. p = wiki.find_or_new_page(page.name)
  394. p.content = WikiContent.new(:page => p) if p.new_record?
  395. p.content.text = page.text
  396. p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
  397. p.content.comments = page.comment
  398. Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
  399. next if p.content.new_record?
  400. migrated_wiki_edits += 1
  401. # Attachments
  402. page.attachments.each do |attachment|
  403. next unless attachment.exist?
  404. next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
  405. a = Attachment.new :created_on => attachment.time
  406. a.file = attachment
  407. a.author = find_or_create_user(attachment.author)
  408. a.description = attachment.description
  409. a.container = p
  410. migrated_wiki_attachments += 1 if a.save
  411. end
  412. end
  413. wiki.reload
  414. wiki.pages.each do |page|
  415. page.content.text = convert_wiki_text(page.content.text)
  416. Time.fake(page.content.updated_on) { page.content.save }
  417. end
  418. end
  419. puts
  420. puts
  421. puts "Components: #{migrated_components}/#{TracComponent.count}"
  422. puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
  423. puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
  424. puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
  425. puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
  426. puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
  427. puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
  428. end
  429. def self.limit_for(klass, attribute)
  430. klass.columns_hash[attribute.to_s].limit
  431. end
  432. def self.encoding(charset)
  433. @ic = Iconv.new('UTF-8', charset)
  434. rescue Iconv::InvalidEncoding
  435. puts "Invalid encoding!"
  436. return false
  437. end
  438. def self.set_trac_directory(path)
  439. @@trac_directory = path
  440. raise "This directory doesn't exist!" unless File.directory?(path)
  441. raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
  442. @@trac_directory
  443. rescue Exception => e
  444. puts e
  445. return false
  446. end
  447. def self.trac_directory
  448. @@trac_directory
  449. end
  450. def self.set_trac_adapter(adapter)
  451. return false if adapter.blank?
  452. raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
  453. # If adapter is sqlite or sqlite3, make sure that trac.db exists
  454. raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
  455. @@trac_adapter = adapter
  456. rescue Exception => e
  457. puts e
  458. return false
  459. end
  460. def self.set_trac_db_host(host)
  461. return nil if host.blank?
  462. @@trac_db_host = host
  463. end
  464. def self.set_trac_db_port(port)
  465. return nil if port.to_i == 0
  466. @@trac_db_port = port.to_i
  467. end
  468. def self.set_trac_db_name(name)
  469. return nil if name.blank?
  470. @@trac_db_name = name
  471. end
  472. def self.set_trac_db_username(username)
  473. @@trac_db_username = username
  474. end
  475. def self.set_trac_db_password(password)
  476. @@trac_db_password = password
  477. end
  478. def self.set_trac_db_schema(schema)
  479. @@trac_db_schema = schema
  480. end
  481. mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
  482. def self.trac_db_path; "#{trac_directory}/db/trac.db" end
  483. def self.trac_attachments_directory; "#{trac_directory}/attachments" end
  484. def self.target_project_identifier(identifier)
  485. project = Project.find_by_identifier(identifier)
  486. if !project
  487. # create the target project
  488. project = Project.new :name => identifier.humanize,
  489. :description => ''
  490. project.identifier = identifier
  491. puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
  492. # enable issues and wiki for the created project
  493. project.enabled_module_names = ['issue_tracking', 'wiki']
  494. else
  495. puts
  496. puts "This project already exists in your Redmine database."
  497. print "Are you sure you want to append data to this project ? [Y/n] "
  498. exit if STDIN.gets.match(/^n$/i)
  499. end
  500. project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
  501. project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
  502. @target_project = project.new_record? ? nil : project
  503. end
  504. def self.connection_params
  505. if %w(sqlite sqlite3).include?(trac_adapter)
  506. {:adapter => trac_adapter,
  507. :database => trac_db_path}
  508. else
  509. {:adapter => trac_adapter,
  510. :database => trac_db_name,
  511. :host => trac_db_host,
  512. :port => trac_db_port,
  513. :username => trac_db_username,
  514. :password => trac_db_password,
  515. :schema_search_path => trac_db_schema
  516. }
  517. end
  518. end
  519. def self.establish_connection
  520. constants.each do |const|
  521. klass = const_get(const)
  522. next unless klass.respond_to? 'establish_connection'
  523. klass.establish_connection connection_params
  524. end
  525. end
  526. private
  527. def self.encode(text)
  528. @ic.iconv text
  529. rescue
  530. text
  531. end
  532. end
  533. puts
  534. if Redmine::DefaultData::Loader.no_data?
  535. puts "Redmine configuration need to be loaded before importing data."
  536. puts "Please, run this first:"
  537. puts
  538. puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
  539. exit
  540. end
  541. puts "WARNING: a new project will be added to Redmine during this process."
  542. print "Are you sure you want to continue ? [y/N] "
  543. break unless STDIN.gets.match(/^y$/i)
  544. puts
  545. def prompt(text, options = {}, &block)
  546. default = options[:default] || ''
  547. while true
  548. print "#{text} [#{default}]: "
  549. value = STDIN.gets.chomp!
  550. value = default if value.blank?
  551. break if yield value
  552. end
  553. end
  554. DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
  555. prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
  556. prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
  557. unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
  558. prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
  559. prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
  560. prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
  561. prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
  562. prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
  563. prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
  564. end
  565. prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
  566. prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
  567. puts
  568. TracMigrate.migrate
  569. end
  570. end