Revision 1298:4f746d8966dd .svn/pristine/33

View differences:

.svn/pristine/33/3310ce8725bdb75d262ab89bf521469eb429b588.svn-base
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

  
18
require 'active_record'
19
require 'iconv' if RUBY_VERSION < '1.9'
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.where(:is_closed => true).first
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.where(:builtin => 0).order('position ASC').all
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
      class ::Time
73
        class << self
74
          alias :real_now :now
75
          def now
76
            real_now - @fake_diff.to_i
77
          end
78
          def fake(time)
79
            @fake_diff = real_now - time
80
            res = yield
81
            @fake_diff = 0
82
           res
83
          end
84
        end
85
      end
86

  
87
      class TracComponent < ActiveRecord::Base
88
        self.table_name = :component
89
      end
90

  
91
      class TracMilestone < ActiveRecord::Base
92
        self.table_name = :milestone
93
        # If this attribute is set a milestone has a defined target timepoint
94
        def due
95
          if read_attribute(:due) && read_attribute(:due) > 0
96
            Time.at(read_attribute(:due)).to_date
97
          else
98
            nil
99
          end
100
        end
101
        # This is the real timepoint at which the milestone has finished.
102
        def completed
103
          if read_attribute(:completed) && read_attribute(:completed) > 0
104
            Time.at(read_attribute(:completed)).to_date
105
          else
106
            nil
107
          end
108
        end
109

  
110
        def description
111
          # Attribute is named descr in Trac v0.8.x
112
          has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
113
        end
114
      end
115

  
116
      class TracTicketCustom < ActiveRecord::Base
117
        self.table_name = :ticket_custom
118
      end
119

  
120
      class TracAttachment < ActiveRecord::Base
121
        self.table_name = :attachment
122
        set_inheritance_column :none
123

  
124
        def time; Time.at(read_attribute(:time)) end
125

  
126
        def original_filename
127
          filename
128
        end
129

  
130
        def content_type
131
          ''
132
        end
133

  
134
        def exist?
135
          File.file? trac_fullpath
136
        end
137

  
138
        def open
139
          File.open("#{trac_fullpath}", 'rb') {|f|
140
            @file = f
141
            yield self
142
          }
143
        end
144

  
145
        def read(*args)
146
          @file.read(*args)
147
        end
148

  
149
        def description
150
          read_attribute(:description).to_s.slice(0,255)
151
        end
152

  
153
      private
154
        def trac_fullpath
155
          attachment_type = read_attribute(:type)
156
          trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
157
          "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
158
        end
159
      end
160

  
161
      class TracTicket < ActiveRecord::Base
162
        self.table_name = :ticket
163
        set_inheritance_column :none
164

  
165
        # ticket changes: only migrate status changes and comments
166
        has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
167
        has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
168

  
169
        def attachments
170
          TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
171
        end
172

  
173
        def ticket_type
174
          read_attribute(:type)
175
        end
176

  
177
        def summary
178
          read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
179
        end
180

  
181
        def description
182
          read_attribute(:description).blank? ? summary : read_attribute(:description)
183
        end
184

  
185
        def time; Time.at(read_attribute(:time)) end
186
        def changetime; Time.at(read_attribute(:changetime)) end
187
      end
188

  
189
      class TracTicketChange < ActiveRecord::Base
190
        self.table_name = :ticket_change
191

  
192
        def self.columns
193
          # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
194
          super.select {|column| column.name.to_s != 'field'}
195
        end
196

  
197
        def time; Time.at(read_attribute(:time)) end
198
      end
199

  
200
      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
201
                           TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
202
                           TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
203
                           TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
204
                           TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
205
                           WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
206
                           CamelCase TitleIndex)
207

  
208
      class TracWikiPage < ActiveRecord::Base
209
        self.table_name = :wiki
210
        set_primary_key :name
211

  
212
        def self.columns
213
          # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
214
          super.select {|column| column.name.to_s != 'readonly'}
215
        end
216

  
217
        def attachments
218
          TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
219
        end
220

  
221
        def time; Time.at(read_attribute(:time)) end
222
      end
223

  
224
      class TracPermission < ActiveRecord::Base
225
        self.table_name = :permission
226
      end
227

  
228
      class TracSessionAttribute < ActiveRecord::Base
229
        self.table_name = :session_attribute
230
      end
231

  
232
      def self.find_or_create_user(username, project_member = false)
233
        return User.anonymous if username.blank?
234

  
235
        u = User.find_by_login(username)
236
        if !u
237
          # Create a new user if not found
238
          mail = username[0, User::MAIL_LENGTH_LIMIT]
239
          if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
240
            mail = mail_attr.value
241
          end
242
          mail = "#{mail}@foo.bar" unless mail.include?("@")
243

  
244
          name = username
245
          if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
246
            name = name_attr.value
247
          end
248
          name =~ (/(.*)(\s+\w+)?/)
249
          fn = $1.strip
250
          ln = ($2 || '-').strip
251

  
252
          u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
253
                       :firstname => fn[0, limit_for(User, 'firstname')],
254
                       :lastname => ln[0, limit_for(User, 'lastname')]
255

  
256
          u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
257
          u.password = 'trac'
258
          u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
259
          # finally, a default user is used if the new user is not valid
260
          u = User.first unless u.save
261
        end
262
        # Make sure he is a member of the project
263
        if project_member && !u.member_of?(@target_project)
264
          role = DEFAULT_ROLE
265
          if u.admin
266
            role = ROLE_MAPPING['admin']
267
          elsif TracPermission.find_by_username_and_action(username, 'developer')
268
            role = ROLE_MAPPING['developer']
269
          end
270
          Member.create(:user => u, :project => @target_project, :roles => [role])
271
          u.reload
272
        end
273
        u
274
      end
275

  
276
      # Basic wiki syntax conversion
277
      def self.convert_wiki_text(text)
278
        # Titles
279
        text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
280
        # External Links
281
        text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
282
        # Ticket links:
283
        #      [ticket:234 Text],[ticket:234 This is a test]
284
        text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
285
        #      ticket:1234
286
        #      #1 is working cause Redmine uses the same syntax.
287
        text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
288
        # Milestone links:
289
        #      [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
290
        #      The text "Milestone 0.1.0 (Mercury)" is not converted,
291
        #      cause Redmine's wiki does not support this.
292
        text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
293
        #      [milestone:"0.1.0 Mercury"]
294
        text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
295
        text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
296
        #      milestone:0.1.0
297
        text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
298
        text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
299
        # Internal Links
300
        text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
301
        text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
302
        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
303
        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
304
        text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
305
        text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
306

  
307
  # Links to pages UsingJustWikiCaps
308
  text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
309
  # Normalize things that were supposed to not be links
310
  # like !NotALink
311
  text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
312
        # Revisions links
313
        text = text.gsub(/\[(\d+)\]/, 'r\1')
314
        # Ticket number re-writing
315
        text = text.gsub(/#(\d+)/) do |s|
316
          if $1.length < 10
317
#            TICKET_MAP[$1.to_i] ||= $1
318
            "\##{TICKET_MAP[$1.to_i] || $1}"
319
          else
320
            s
321
          end
322
        end
323
        # We would like to convert the Code highlighting too
324
        # This will go into the next line.
325
        shebang_line = false
326
        # Reguar expression for start of code
327
        pre_re = /\{\{\{/
328
        # Code hightlighing...
329
        shebang_re = /^\#\!([a-z]+)/
330
        # Regular expression for end of code
331
        pre_end_re = /\}\}\}/
332

  
333
        # Go through the whole text..extract it line by line
334
        text = text.gsub(/^(.*)$/) do |line|
335
          m_pre = pre_re.match(line)
336
          if m_pre
337
            line = '<pre>'
338
          else
339
            m_sl = shebang_re.match(line)
340
            if m_sl
341
              shebang_line = true
342
              line = '<code class="' + m_sl[1] + '">'
343
            end
344
            m_pre_end = pre_end_re.match(line)
345
            if m_pre_end
346
              line = '</pre>'
347
              if shebang_line
348
                line = '</code>' + line
349
              end
350
            end
351
          end
352
          line
353
        end
354

  
355
        # Highlighting
356
        text = text.gsub(/'''''([^\s])/, '_*\1')
357
        text = text.gsub(/([^\s])'''''/, '\1*_')
358
        text = text.gsub(/'''/, '*')
359
        text = text.gsub(/''/, '_')
360
        text = text.gsub(/__/, '+')
361
        text = text.gsub(/~~/, '-')
362
        text = text.gsub(/`/, '@')
363
        text = text.gsub(/,,/, '~')
364
        # Lists
365
        text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
366

  
367
        text
368
      end
369

  
370
      def self.migrate
371
        establish_connection
372

  
373
        # Quick database test
374
        TracComponent.count
375

  
376
        migrated_components = 0
377
        migrated_milestones = 0
378
        migrated_tickets = 0
379
        migrated_custom_values = 0
380
        migrated_ticket_attachments = 0
381
        migrated_wiki_edits = 0
382
        migrated_wiki_attachments = 0
383

  
384
        #Wiki system initializing...
385
        @target_project.wiki.destroy if @target_project.wiki
386
        @target_project.reload
387
        wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
388
        wiki_edit_count = 0
389

  
390
        # Components
391
        print "Migrating components"
392
        issues_category_map = {}
393
        TracComponent.all.each do |component|
394
        print '.'
395
        STDOUT.flush
396
          c = IssueCategory.new :project => @target_project,
397
                                :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
398
        next unless c.save
399
        issues_category_map[component.name] = c
400
        migrated_components += 1
401
        end
402
        puts
403

  
404
        # Milestones
405
        print "Migrating milestones"
406
        version_map = {}
407
        TracMilestone.all.each do |milestone|
408
          print '.'
409
          STDOUT.flush
410
          # First we try to find the wiki page...
411
          p = wiki.find_or_new_page(milestone.name.to_s)
412
          p.content = WikiContent.new(:page => p) if p.new_record?
413
          p.content.text = milestone.description.to_s
414
          p.content.author = find_or_create_user('trac')
415
          p.content.comments = 'Milestone'
416
          p.save
417

  
418
          v = Version.new :project => @target_project,
419
                          :name => encode(milestone.name[0, limit_for(Version, 'name')]),
420
                          :description => nil,
421
                          :wiki_page_title => milestone.name.to_s,
422
                          :effective_date => milestone.completed
423

  
424
          next unless v.save
425
          version_map[milestone.name] = v
426
          migrated_milestones += 1
427
        end
428
        puts
429

  
430
        # Custom fields
431
        # TODO: read trac.ini instead
432
        print "Migrating custom fields"
433
        custom_field_map = {}
434
        TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
435
          print '.'
436
          STDOUT.flush
437
          # Redmine custom field name
438
          field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
439
          # Find if the custom already exists in Redmine
440
          f = IssueCustomField.find_by_name(field_name)
441
          # Or create a new one
442
          f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
443
                                        :field_format => 'string')
444

  
445
          next if f.new_record?
446
          f.trackers = Tracker.all
447
          f.projects << @target_project
448
          custom_field_map[field.name] = f
449
        end
450
        puts
451

  
452
        # Trac 'resolution' field as a Redmine custom field
453
        r = IssueCustomField.where(:name => "Resolution").first
454
        r = IssueCustomField.new(:name => 'Resolution',
455
                                 :field_format => 'list',
456
                                 :is_filter => true) if r.nil?
457
        r.trackers = Tracker.all
458
        r.projects << @target_project
459
        r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
460
        r.save!
461
        custom_field_map['resolution'] = r
462

  
463
        # Tickets
464
        print "Migrating tickets"
465
          TracTicket.find_each(:batch_size => 200) do |ticket|
466
          print '.'
467
          STDOUT.flush
468
          i = Issue.new :project => @target_project,
469
                          :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
470
                          :description => convert_wiki_text(encode(ticket.description)),
471
                          :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
472
                          :created_on => ticket.time
473
          i.author = find_or_create_user(ticket.reporter)
474
          i.category = issues_category_map[ticket.component] unless ticket.component.blank?
475
          i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
476
          i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
477
          i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
478
          i.id = ticket.id unless Issue.exists?(ticket.id)
479
          next unless Time.fake(ticket.changetime) { i.save }
480
          TICKET_MAP[ticket.id] = i.id
481
          migrated_tickets += 1
482

  
483
          # Owner
484
            unless ticket.owner.blank?
485
              i.assigned_to = find_or_create_user(ticket.owner, true)
486
              Time.fake(ticket.changetime) { i.save }
487
            end
488

  
489
          # Comments and status/resolution changes
490
          ticket.ticket_changes.group_by(&:time).each do |time, changeset|
491
              status_change = changeset.select {|change| change.field == 'status'}.first
492
              resolution_change = changeset.select {|change| change.field == 'resolution'}.first
493
              comment_change = changeset.select {|change| change.field == 'comment'}.first
494

  
495
              n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
496
                              :created_on => time
497
              n.user = find_or_create_user(changeset.first.author)
498
              n.journalized = i
499
              if status_change &&
500
                   STATUS_MAPPING[status_change.oldvalue] &&
501
                   STATUS_MAPPING[status_change.newvalue] &&
502
                   (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
503
                n.details << JournalDetail.new(:property => 'attr',
504
                                               :prop_key => 'status_id',
505
                                               :old_value => STATUS_MAPPING[status_change.oldvalue].id,
506
                                               :value => STATUS_MAPPING[status_change.newvalue].id)
507
              end
508
              if resolution_change
509
                n.details << JournalDetail.new(:property => 'cf',
510
                                               :prop_key => custom_field_map['resolution'].id,
511
                                               :old_value => resolution_change.oldvalue,
512
                                               :value => resolution_change.newvalue)
513
              end
514
              n.save unless n.details.empty? && n.notes.blank?
515
          end
516

  
517
          # Attachments
518
          ticket.attachments.each do |attachment|
519
            next unless attachment.exist?
520
              attachment.open {
521
                a = Attachment.new :created_on => attachment.time
522
                a.file = attachment
523
                a.author = find_or_create_user(attachment.author)
524
                a.container = i
525
                a.description = attachment.description
526
                migrated_ticket_attachments += 1 if a.save
527
              }
528
          end
529

  
530
          # Custom fields
531
          custom_values = ticket.customs.inject({}) do |h, custom|
532
            if custom_field = custom_field_map[custom.name]
533
              h[custom_field.id] = custom.value
534
              migrated_custom_values += 1
535
            end
536
            h
537
          end
538
          if custom_field_map['resolution'] && !ticket.resolution.blank?
539
            custom_values[custom_field_map['resolution'].id] = ticket.resolution
540
          end
541
          i.custom_field_values = custom_values
542
          i.save_custom_field_values
543
        end
544

  
545
        # update issue id sequence if needed (postgresql)
546
        Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
547
        puts
548

  
549
        # Wiki
550
        print "Migrating wiki"
551
        if wiki.save
552
          TracWikiPage.order('name, version').all.each do |page|
553
            # Do not migrate Trac manual wiki pages
554
            next if TRAC_WIKI_PAGES.include?(page.name)
555
            wiki_edit_count += 1
556
            print '.'
557
            STDOUT.flush
558
            p = wiki.find_or_new_page(page.name)
559
            p.content = WikiContent.new(:page => p) if p.new_record?
560
            p.content.text = page.text
561
            p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
562
            p.content.comments = page.comment
563
            Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
564

  
565
            next if p.content.new_record?
566
            migrated_wiki_edits += 1
567

  
568
            # Attachments
569
            page.attachments.each do |attachment|
570
              next unless attachment.exist?
571
              next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
572
              attachment.open {
573
                a = Attachment.new :created_on => attachment.time
574
                a.file = attachment
575
                a.author = find_or_create_user(attachment.author)
576
                a.description = attachment.description
577
                a.container = p
578
                migrated_wiki_attachments += 1 if a.save
579
              }
580
            end
581
          end
582

  
583
          wiki.reload
584
          wiki.pages.each do |page|
585
            page.content.text = convert_wiki_text(page.content.text)
586
            Time.fake(page.content.updated_on) { page.content.save }
587
          end
588
        end
589
        puts
590

  
591
        puts
592
        puts "Components:      #{migrated_components}/#{TracComponent.count}"
593
        puts "Milestones:      #{migrated_milestones}/#{TracMilestone.count}"
594
        puts "Tickets:         #{migrated_tickets}/#{TracTicket.count}"
595
        puts "Ticket files:    #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
596
        puts "Custom values:   #{migrated_custom_values}/#{TracTicketCustom.count}"
597
        puts "Wiki edits:      #{migrated_wiki_edits}/#{wiki_edit_count}"
598
        puts "Wiki files:      #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
599
      end
600

  
601
      def self.limit_for(klass, attribute)
602
        klass.columns_hash[attribute.to_s].limit
603
      end
604

  
605
      def self.encoding(charset)
606
        @charset = charset
607
      end
608

  
609
      def self.set_trac_directory(path)
610
        @@trac_directory = path
611
        raise "This directory doesn't exist!" unless File.directory?(path)
612
        raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
613
        @@trac_directory
614
      rescue Exception => e
615
        puts e
616
        return false
617
      end
618

  
619
      def self.trac_directory
620
        @@trac_directory
621
      end
622

  
623
      def self.set_trac_adapter(adapter)
624
        return false if adapter.blank?
625
        raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
626
        # If adapter is sqlite or sqlite3, make sure that trac.db exists
627
        raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
628
        @@trac_adapter = adapter
629
      rescue Exception => e
630
        puts e
631
        return false
632
      end
633

  
634
      def self.set_trac_db_host(host)
635
        return nil if host.blank?
636
        @@trac_db_host = host
637
      end
638

  
639
      def self.set_trac_db_port(port)
640
        return nil if port.to_i == 0
641
        @@trac_db_port = port.to_i
642
      end
643

  
644
      def self.set_trac_db_name(name)
645
        return nil if name.blank?
646
        @@trac_db_name = name
647
      end
648

  
649
      def self.set_trac_db_username(username)
650
        @@trac_db_username = username
651
      end
652

  
653
      def self.set_trac_db_password(password)
654
        @@trac_db_password = password
655
      end
656

  
657
      def self.set_trac_db_schema(schema)
658
        @@trac_db_schema = schema
659
      end
660

  
661
      mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
662

  
663
      def self.trac_db_path; "#{trac_directory}/db/trac.db" end
664
      def self.trac_attachments_directory; "#{trac_directory}/attachments" end
665

  
666
      def self.target_project_identifier(identifier)
667
        project = Project.find_by_identifier(identifier)
668
        if !project
669
          # create the target project
670
          project = Project.new :name => identifier.humanize,
671
                                :description => ''
672
          project.identifier = identifier
673
          puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
674
          # enable issues and wiki for the created project
675
          project.enabled_module_names = ['issue_tracking', 'wiki']
676
        else
677
          puts
678
          puts "This project already exists in your Redmine database."
679
          print "Are you sure you want to append data to this project ? [Y/n] "
680
          STDOUT.flush
681
          exit if STDIN.gets.match(/^n$/i)
682
        end
683
        project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
684
        project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
685
        @target_project = project.new_record? ? nil : project
686
        @target_project.reload
687
      end
688

  
689
      def self.connection_params
690
        if trac_adapter == 'sqlite3'
691
          {:adapter => 'sqlite3',
692
           :database => trac_db_path}
693
        else
694
          {:adapter => trac_adapter,
695
           :database => trac_db_name,
696
           :host => trac_db_host,
697
           :port => trac_db_port,
698
           :username => trac_db_username,
699
           :password => trac_db_password,
700
           :schema_search_path => trac_db_schema
701
          }
702
        end
703
      end
704

  
705
      def self.establish_connection
706
        constants.each do |const|
707
          klass = const_get(const)
708
          next unless klass.respond_to? 'establish_connection'
709
          klass.establish_connection connection_params
710
        end
711
      end
712

  
713
      def self.encode(text)
714
        if RUBY_VERSION < '1.9'
715
          @ic ||= Iconv.new('UTF-8', @charset)
716
          @ic.iconv text
717
        else
718
          text.to_s.force_encoding(@charset).encode('UTF-8')
719
        end
720
      end
721
    end
722

  
723
    puts
724
    if Redmine::DefaultData::Loader.no_data?
725
      puts "Redmine configuration need to be loaded before importing data."
726
      puts "Please, run this first:"
727
      puts
728
      puts "  rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
729
      exit
730
    end
731

  
732
    puts "WARNING: a new project will be added to Redmine during this process."
733
    print "Are you sure you want to continue ? [y/N] "
734
    STDOUT.flush
735
    break unless STDIN.gets.match(/^y$/i)
736
    puts
737

  
738
    def prompt(text, options = {}, &block)
739
      default = options[:default] || ''
740
      while true
741
        print "#{text} [#{default}]: "
742
        STDOUT.flush
743
        value = STDIN.gets.chomp!
744
        value = default if value.blank?
745
        break if yield value
746
      end
747
    end
748

  
749
    DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
750

  
751
    prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
752
    prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
753
    unless %w(sqlite3).include?(TracMigrate.trac_adapter)
754
      prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
755
      prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
756
      prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
757
      prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
758
      prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
759
      prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
760
    end
761
    prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
762
    prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
763
    puts
764

  
765
    # Turn off email notifications
766
    Setting.notified_events = []
767

  
768
    TracMigrate.migrate
769
  end
770
end
771

  
.svn/pristine/33/332beac4e5a854120b53b926beb38dfe9376b34a.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  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
module Redmine
19
  module Search
20

  
21
    mattr_accessor :available_search_types
22

  
23
    @@available_search_types = []
24

  
25
    class << self
26
      def map(&block)
27
        yield self
28
      end
29

  
30
      # Registers a search provider
31
      def register(search_type, options={})
32
        search_type = search_type.to_s
33
        @@available_search_types << search_type unless @@available_search_types.include?(search_type)
34
      end
35
    end
36

  
37
    module Controller
38
      def self.included(base)
39
        base.extend(ClassMethods)
40
      end
41

  
42
      module ClassMethods
43
        @@default_search_scopes = Hash.new {|hash, key| hash[key] = {:default => nil, :actions => {}}}
44
        mattr_accessor :default_search_scopes
45

  
46
        # Set the default search scope for a controller or specific actions
47
        # Examples:
48
        #   * search_scope :issues # => sets the search scope to :issues for the whole controller
49
        #   * search_scope :issues, :only => :index
50
        #   * search_scope :issues, :only => [:index, :show]
51
        def default_search_scope(id, options = {})
52
          if actions = options[:only]
53
            actions = [] << actions unless actions.is_a?(Array)
54
            actions.each {|a| default_search_scopes[controller_name.to_sym][:actions][a.to_sym] = id.to_s}
55
          else
56
            default_search_scopes[controller_name.to_sym][:default] = id.to_s
57
          end
58
        end
59
      end
60

  
61
      def default_search_scopes
62
        self.class.default_search_scopes
63
      end
64

  
65
      # Returns the default search scope according to the current action
66
      def default_search_scope
67
        @default_search_scope ||= default_search_scopes[controller_name.to_sym][:actions][action_name.to_sym] ||
68
                                  default_search_scopes[controller_name.to_sym][:default]
69
      end
70
    end
71
  end
72
end
.svn/pristine/33/3349206fec5e88c2e11146378e031801f405150d.svn-base
1
<%= link_to 'root', :action => 'show', :id => @project, :path => '', :rev => @rev %>
2
<%
3
dirs = path.split('/')
4
if 'file' == kind
5
    filename = dirs.pop
6
end
7
link_path = ''
8
dirs.each do |dir|
9
    next if dir.blank?
10
    link_path << '/' unless link_path.empty?
11
    link_path << "#{dir}"
12
    %>
13
    / <%= link_to h(dir), :action => 'show', :id => @project,
14
                :path => to_path_param(link_path), :rev => @rev %>
15
<% end %>
16
<% if filename %>
17
    / <%= link_to h(filename),
18
                   :action => 'changes', :id => @project,
19
                   :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %>
20
<% end %>
21
<%
22
  # @rev is revsion or Git and Mercurial branch or tag.
23
  # For Mercurial *tip*, @rev and @changeset are nil.
24
  rev_text = @changeset.nil? ? @rev : format_revision(@changeset)
25
%>
26
<%= "@ #{h rev_text}" unless rev_text.blank? %>
27

  
28
<% html_title(with_leading_slash(path)) -%>
.svn/pristine/33/3357225f79b9b7908f02d51c4dcc557f0e92041e.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  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 'redmine/scm/adapters/abstract_adapter'
19

  
20
module Redmine
21
  module Scm
22
    module Adapters
23
      class GitAdapter < AbstractAdapter
24

  
25
        # Git executable name
26
        GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
27

  
28
        class << self
29
          def client_command
30
            @@bin    ||= GIT_BIN
31
          end
32

  
33
          def sq_bin
34
            @@sq_bin ||= shell_quote_command
35
          end
36

  
37
          def client_version
38
            @@client_version ||= (scm_command_version || [])
39
          end
40

  
41
          def client_available
42
            !client_version.empty?
43
          end
44

  
45
          def scm_command_version
46
            scm_version = scm_version_from_command_line.dup
47
            if scm_version.respond_to?(:force_encoding)
48
              scm_version.force_encoding('ASCII-8BIT')
49
            end
50
            if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
51
              m[2].scan(%r{\d+}).collect(&:to_i)
52
            end
53
          end
54

  
55
          def scm_version_from_command_line
56
            shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
57
          end
58
        end
59

  
60
        def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
61
          super
62
          @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
63
        end
64

  
65
        def path_encoding
66
          @path_encoding
67
        end
68

  
69
        def info
70
          begin
71
            Info.new(:root_url => url, :lastrev => lastrev('',nil))
72
          rescue
73
            nil
74
          end
75
        end
76

  
77
        def branches
78
          return @branches if @branches
79
          @branches = []
80
          cmd_args = %w|branch --no-color --verbose --no-abbrev|
81
          scm_cmd(*cmd_args) do |io|
82
            io.each_line do |line|
83
              branch_rev = line.match('\s*\*?\s*(.*?)\s*([0-9a-f]{40}).*$')
84
              bran = Branch.new(branch_rev[1])
85
              bran.revision =  branch_rev[2]
86
              bran.scmid    =  branch_rev[2]
87
              @branches << bran
88
            end
89
          end
90
          @branches.sort!
91
        rescue ScmCommandAborted
92
          nil
93
        end
94

  
95
        def tags
96
          return @tags if @tags
97
          cmd_args = %w|tag|
98
          scm_cmd(*cmd_args) do |io|
99
            @tags = io.readlines.sort!.map{|t| t.strip}
100
          end
101
        rescue ScmCommandAborted
102
          nil
103
        end
104

  
105
        def default_branch
106
          bras = self.branches
107
          return nil if bras.nil?
108
          bras.include?('master') ? 'master' : bras.first
109
        end
110

  
111
        def entry(path=nil, identifier=nil)
112
          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
113
          search_path = parts[0..-2].join('/')
114
          search_name = parts[-1]
115
          if search_path.blank? && search_name.blank?
116
            # Root entry
117
            Entry.new(:path => '', :kind => 'dir')
118
          else
119
            # Search for the entry in the parent directory
120
            es = entries(search_path, identifier,
121
                         options = {:report_last_commit => false})
122
            es ? es.detect {|e| e.name == search_name} : nil
123
          end
124
        end
125

  
126
        def entries(path=nil, identifier=nil, options={})
127
          path ||= ''
128
          p = scm_iconv(@path_encoding, 'UTF-8', path)
129
          entries = Entries.new
130
          cmd_args = %w|ls-tree -l|
131
          cmd_args << "HEAD:#{p}"          if identifier.nil?
132
          cmd_args << "#{identifier}:#{p}" if identifier
133
          scm_cmd(*cmd_args) do |io|
134
            io.each_line do |line|
135
              e = line.chomp.to_s
136
              if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
137
                type = $1
138
                sha  = $2
139
                size = $3
140
                name = $4
141
                if name.respond_to?(:force_encoding)
142
                  name.force_encoding(@path_encoding)
143
                end
144
                full_path = p.empty? ? name : "#{p}/#{name}"
145
                n      = scm_iconv('UTF-8', @path_encoding, name)
146
                full_p = scm_iconv('UTF-8', @path_encoding, full_path)
147
                entries << Entry.new({:name => n,
148
                 :path => full_p,
149
                 :kind => (type == "tree") ? 'dir' : 'file',
150
                 :size => (type == "tree") ? nil : size,
151
                 :lastrev => options[:report_last_commit] ?
152
                                 lastrev(full_path, identifier) : Revision.new
153
                }) unless entries.detect{|entry| entry.name == name}
154
              end
155
            end
156
          end
157
          entries.sort_by_name
158
        rescue ScmCommandAborted
159
          nil
160
        end
161

  
162
        def lastrev(path, rev)
163
          return nil if path.nil?
164
          cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
165
          cmd_args << rev if rev
166
          cmd_args << "--" << path unless path.empty?
167
          lines = []
168
          scm_cmd(*cmd_args) { |io| lines = io.readlines }
169
          begin
170
              id = lines[0].split[1]
171
              author = lines[1].match('Author:\s+(.*)$')[1]
172
              time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
173

  
174
              Revision.new({
175
                :identifier => id,
176
                :scmid      => id,
177
                :author     => author,
178
                :time       => time,
179
                :message    => nil,
180
                :paths      => nil
181
                })
182
          rescue NoMethodError => e
183
              logger.error("The revision '#{path}' has a wrong format")
184
              return nil
185
          end
186
        rescue ScmCommandAborted
187
          nil
188
        end
189

  
190
        def revisions(path, identifier_from, identifier_to, options={})
191
          revs = Revisions.new
192
          cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents|
193
          cmd_args << "--reverse" if options[:reverse]
194
          cmd_args << "--all" if options[:all]
195
          cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
196
          from_to = ""
197
          from_to << "#{identifier_from}.." if identifier_from
198
          from_to << "#{identifier_to}" if identifier_to
199
          cmd_args << from_to if !from_to.empty?
200
          cmd_args << "--since=#{options[:since].strftime("%Y-%m-%d %H:%M:%S")}" if options[:since]
201
          cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
202

  
203
          scm_cmd *cmd_args do |io|
204
            files=[]
205
            changeset = {}
206
            parsing_descr = 0  #0: not parsing desc or files, 1: parsing desc, 2: parsing files
207

  
208
            io.each_line do |line|
209
              if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/
210
                key = "commit"
211
                value = $1
212
                parents_str = $2
213
                if (parsing_descr == 1 || parsing_descr == 2)
214
                  parsing_descr = 0
215
                  revision = Revision.new({
216
                    :identifier => changeset[:commit],
217
                    :scmid      => changeset[:commit],
218
                    :author     => changeset[:author],
219
                    :time       => Time.parse(changeset[:date]),
220
                    :message    => changeset[:description],
221
                    :paths      => files,
222
                    :parents    => changeset[:parents]
223
                  })
224
                  if block_given?
225
                    yield revision
226
                  else
227
                    revs << revision
228
                  end
229
                  changeset = {}
230
                  files = []
231
                end
232
                changeset[:commit] = $1
233
                unless parents_str.nil? or parents_str == ""
234
                  changeset[:parents] = parents_str.strip.split(' ')
235
                end
236
              elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
237
                key = $1
238
                value = $2
239
                if key == "Author"
240
                  changeset[:author] = value
241
                elsif key == "CommitDate"
242
                  changeset[:date] = value
243
                end
244
              elsif (parsing_descr == 0) && line.chomp.to_s == ""
245
                parsing_descr = 1
246
                changeset[:description] = ""
247
              elsif (parsing_descr == 1 || parsing_descr == 2) \
248
                  && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
249
                parsing_descr = 2
250
                fileaction    = $1
251
                filepath      = $2
252
                p = scm_iconv('UTF-8', @path_encoding, filepath)
253
                files << {:action => fileaction, :path => p}
254
              elsif (parsing_descr == 1 || parsing_descr == 2) \
255
                  && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
256
                parsing_descr = 2
257
                fileaction    = $1
258
                filepath      = $3
259
                p = scm_iconv('UTF-8', @path_encoding, filepath)
260
                files << {:action => fileaction, :path => p}
261
              elsif (parsing_descr == 1) && line.chomp.to_s == ""
262
                parsing_descr = 2
263
              elsif (parsing_descr == 1)
264
                changeset[:description] << line[4..-1]
265
              end
266
            end
267

  
268
            if changeset[:commit]
269
              revision = Revision.new({
270
                :identifier => changeset[:commit],
271
                :scmid      => changeset[:commit],
272
                :author     => changeset[:author],
273
                :time       => Time.parse(changeset[:date]),
274
                :message    => changeset[:description],
275
                :paths      => files,
276
                :parents    => changeset[:parents]
277
                 })
278
              if block_given?
279
                yield revision
280
              else
281
                revs << revision
282
              end
283
            end
284
          end
285
          revs
286
        rescue ScmCommandAborted => e
287
          logger.error("git log #{from_to.to_s} error: #{e.message}")
288
          revs
289
        end
290

  
291
        def diff(path, identifier_from, identifier_to=nil)
292
          path ||= ''
293
          cmd_args = []
294
          if identifier_to
295
            cmd_args << "diff" << "--no-color" <<  identifier_to << identifier_from
296
          else
297
            cmd_args << "show" << "--no-color" << identifier_from
298
          end
299
          cmd_args << "--" <<  scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
300
          diff = []
301
          scm_cmd *cmd_args do |io|
302
            io.each_line do |line|
303
              diff << line
304
            end
305
          end
306
          diff
307
        rescue ScmCommandAborted
308
          nil
309
        end
310

  
311
        def annotate(path, identifier=nil)
312
          identifier = 'HEAD' if identifier.blank?
313
          cmd_args = %w|blame|
314
          cmd_args << "-p" << identifier << "--" <<  scm_iconv(@path_encoding, 'UTF-8', path)
315
          blame = Annotate.new
316
          content = nil
317
          scm_cmd(*cmd_args) { |io| io.binmode; content = io.read }
318
          # git annotates binary files
319
          return nil if content.is_binary_data?
320
          identifier = ''
321
          # git shows commit author on the first occurrence only
322
          authors_by_commit = {}
323
          content.split("\n").each do |line|
324
            if line =~ /^([0-9a-f]{39,40})\s.*/
325
              identifier = $1
326
            elsif line =~ /^author (.+)/
327
              authors_by_commit[identifier] = $1.strip
328
            elsif line =~ /^\t(.*)/
329
              blame.add_line($1, Revision.new(
330
                                    :identifier => identifier,
331
                                    :revision   => identifier,
332
                                    :scmid      => identifier,
333
                                    :author     => authors_by_commit[identifier]
334
                                    ))
335
              identifier = ''
336
              author = ''
337
            end
338
          end
339
          blame
340
        rescue ScmCommandAborted
341
          nil
342
        end
343

  
344
        def cat(path, identifier=nil)
345
          if identifier.nil?
346
            identifier = 'HEAD'
347
          end
348
          cmd_args = %w|show --no-color|
349
          cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
350
          cat = nil
351
          scm_cmd(*cmd_args) do |io|
352
            io.binmode
353
            cat = io.read
354
          end
355
          cat
356
        rescue ScmCommandAborted
357
          nil
358
        end
359

  
360
        class Revision < Redmine::Scm::Adapters::Revision
361
          # Returns the readable identifier
362
          def format_identifier
363
            identifier[0,8]
364
          end
365
        end
366

  
367
        def scm_cmd(*args, &block)
368
          repo_path = root_url || url
369
          full_args = ['--git-dir', repo_path]
370
          if self.class.client_version_above?([1, 7, 2])
371
            full_args << '-c' << 'core.quotepath=false'
372
            full_args << '-c' << 'log.decorate=no'
373
          end
374
          full_args += args
375
          ret = shellout(
376
                   self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
377
                   &block
378
                   )
379
          if $? && $?.exitstatus != 0
380
            raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
381
          end
382
          ret
383
        end
384
        private :scm_cmd
385
      end
386
    end
387
  end
388
end
.svn/pristine/33/3396ee85e7860d58bce2840b1f00fae7a535409f.svn-base
1
module TestHelper
2
  def self.report_location(path)
3
    [RAILS_ROOT + '/', 'vendor/plugins/'].each { |part| path.sub! part, ''}
4
    path = path.split('/')
5
    location, subject = path.first, path.last
6
    if subject.sub! '.rb', ''
7
      subject = subject.classify
8
    else 
9
      subject.sub! '.html.erb', ''
10
    end
11
    "#{subject} (from #{location})"
12
  end
13
  
14
  def self.view_path_for path
15
    [RAILS_ROOT + '/', 'vendor/plugins/', '.html.erb'].each { |part| path.sub! part, ''}
16
    parts = path.split('/')
17
    parts[(parts.index('views')+1)..-1].join('/')
18
  end
19
end
20

  
21
class Test::Unit::TestCase
22
  # Add more helper methods to be used by all tests here...  
23
  def get_action_on_controller(*args)
24
    action = args.shift
25
    with_controller *args
26
    get action
27
  end
28
  
29
  def with_controller(controller, namespace = nil)
30
    classname = controller.to_s.classify + 'Controller'
31
    classname = namespace.to_s.classify + '::' + classname unless namespace.nil?
32
    @controller = classname.constantize.new
33
  end
34
  
35
  def assert_response_body(expected)
36
    assert_equal expected, @response.body
37
  end
38
end
39

  
40
# Because we're testing this behaviour, we actually want these features on!
41
Engines.disable_application_view_loading = false
42
Engines.disable_application_code_loading = false
.svn/pristine/33/339df18b16b4ab05fedfba76e31080e6dda3b82a.svn-base
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

  
18
require File.expand_path('../../../test_helper', __FILE__)
19

  
20
class RoutingMailHandlerTest < ActionController::IntegrationTest
21
  def test_mail_handler
22
    assert_routing(
23
        { :method => "post", :path => "/mail_handler" },
24
        { :controller => 'mail_handler', :action => 'index' }
25
      )
26
  end
27
end
.svn/pristine/33/33a69333d665b6d06e88fdb324d960ab74dff338.svn-base
1
module CodeRay
2
  
3
  # The result of a scan operation is a TokensProxy, but should act like Tokens.
4
  # 
5
  # This proxy makes it possible to use the classic CodeRay.scan.encode API
6
  # while still providing the benefits of direct streaming.
7
  class TokensProxy
8
    
9
    attr_accessor :input, :lang, :options, :block
10
    
11
    # Create a new TokensProxy with the arguments of CodeRay.scan.
12
    def initialize input, lang, options = {}, block = nil
13
      @input   = input
14
      @lang    = lang
15
      @options = options
16
      @block   = block
17
    end
18
    
19
    # Call CodeRay.encode if +encoder+ is a Symbol;
20
    # otherwise, convert the receiver to tokens and call encoder.encode_tokens.
21
    def encode encoder, options = {}
22
      if encoder.respond_to? :to_sym
23
        CodeRay.encode(input, lang, encoder, options)
24
      else
25
        encoder.encode_tokens tokens, options
26
      end
27
    end
28
    
29
    # Tries to call encode;
30
    # delegates to tokens otherwise.
31
    def method_missing method, *args, &blk
32
      encode method.to_sym, *args
33
    rescue PluginHost::PluginNotFound
34
      tokens.send(method, *args, &blk)
35
    end
36
    
37
    # The (cached) result of the tokenized input; a Tokens instance.
38
    def tokens
39
      @tokens ||= scanner.tokenize(input)
40
    end
41
    
42
    # A (cached) scanner instance to use for the scan task.
43
    def scanner
44
      @scanner ||= CodeRay.scanner(lang, options, &block)
45
    end
46
    
47
    # Overwrite Struct#each.
48
    def each *args, &blk
49
      tokens.each(*args, &blk)
50
      self
51
    end
52
    
53
  end
54
  
55
end

Also available in: Unified diff