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 29KB

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