Revision 1298:4f746d8966dd .svn/pristine/33
| .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