Revision 1297:0a574315af3e .svn/pristine/94

View differences:

.svn/pristine/94/946610db029f9c718cdfb0d1e063ab6fbd404ac9.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  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
require 'iconv'
20

  
21
class PdfTest < ActiveSupport::TestCase
22
  fixtures :users, :projects, :roles, :members, :member_roles,
23
           :enabled_modules, :issues, :trackers, :attachments
24

  
25
  def test_fix_text_encoding_nil
26
    assert_equal '', Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(nil, "UTF-8")
27
    assert_equal '', Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(nil, "ISO-8859-1")
28
  end
29

  
30
  def test_rdm_pdf_iconv_cannot_convert_ja_cp932
31
    encoding = ( RUBY_PLATFORM == 'java' ? "SJIS" : "CP932" )
32
    utf8_txt_1  = "\xe7\x8b\x80\xe6\x85\x8b"
33
    utf8_txt_2  = "\xe7\x8b\x80\xe6\x85\x8b\xe7\x8b\x80"
34
    utf8_txt_3  = "\xe7\x8b\x80\xe7\x8b\x80\xe6\x85\x8b\xe7\x8b\x80"
35
    if utf8_txt_1.respond_to?(:force_encoding)
36
      txt_1 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_1, encoding)
37
      txt_2 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_2, encoding)
38
      txt_3 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_3, encoding)
39
      assert_equal "?\x91\xd4", txt_1
40
      assert_equal "?\x91\xd4?", txt_2
41
      assert_equal "??\x91\xd4?", txt_3
42
      assert_equal "ASCII-8BIT", txt_1.encoding.to_s
43
      assert_equal "ASCII-8BIT", txt_2.encoding.to_s
44
      assert_equal "ASCII-8BIT", txt_3.encoding.to_s
45
    elsif RUBY_PLATFORM == 'java'
46
      assert_equal "??",
47
                   Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_1, encoding)
48
      assert_equal "???",
49
                   Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_2, encoding)
50
      assert_equal "????",
51
                   Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_3, encoding)
52
    else
53
      assert_equal "???\x91\xd4",
54
                   Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_1, encoding)
55
      assert_equal "???\x91\xd4???",
56
                   Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_2, encoding)
57
      assert_equal "??????\x91\xd4???",
58
                   Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_3, encoding)
59
    end
60
  end
61

  
62
  def test_rdm_pdf_iconv_invalid_utf8_should_be_replaced_en
63
    str1 = "Texte encod\xe9 en ISO-8859-1"
64
    str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
65
    str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
66
    str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
67
    txt_1 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(str1, 'UTF-8')
68
    txt_2 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(str2, 'UTF-8')
69
    if txt_1.respond_to?(:force_encoding)
70
      assert_equal "ASCII-8BIT", txt_1.encoding.to_s
71
      assert_equal "ASCII-8BIT", txt_2.encoding.to_s
72
    end
73
    assert_equal "Texte encod? en ISO-8859-1", txt_1
74
    assert_equal "?a?b?c?d?e test", txt_2
75
  end
76

  
77
  def test_rdm_pdf_iconv_invalid_utf8_should_be_replaced_ja
78
    str1 = "Texte encod\xe9 en ISO-8859-1"
79
    str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
80
    str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
81
    str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
82
    encoding = ( RUBY_PLATFORM == 'java' ? "SJIS" : "CP932" )
83
    txt_1 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(str1, encoding)
84
    txt_2 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(str2, encoding)
85
    if txt_1.respond_to?(:force_encoding)
86
      assert_equal "ASCII-8BIT", txt_1.encoding.to_s
87
      assert_equal "ASCII-8BIT", txt_2.encoding.to_s
88
    end
89
    assert_equal "Texte encod? en ISO-8859-1", txt_1
90
    assert_equal "?a?b?c?d?e test", txt_2
91
  end
92

  
93
  def test_attach
94
    set_fixtures_attachments_directory
95

  
96
    str2 = "\x83e\x83X\x83g"
97
    str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
98
    encoding = ( RUBY_PLATFORM == 'java' ? "SJIS" : "CP932" )
99

  
100
    a1 = Attachment.find(17)
101
    a2 = Attachment.find(19)
102

  
103
    User.current = User.find(1)
104
    assert a1.readable?
105
    assert a1.visible?
106
    assert a2.readable?
107
    assert a2.visible?
108

  
109
    aa1 = Redmine::Export::PDF::RDMPdfEncoding::attach(Attachment.all, "Testfile.PNG", "UTF-8")
110
    assert_not_nil aa1
111
    assert_equal 17, aa1.id
112
    aa2 = Redmine::Export::PDF::RDMPdfEncoding::attach(Attachment.all, "test#{str2}.png", encoding)
113
    assert_not_nil aa2
114
    assert_equal 19, aa2.id
115

  
116
    User.current = nil
117
    assert a1.readable?
118
    assert (! a1.visible?)
119
    assert a2.readable?
120
    assert (! a2.visible?)
121

  
122
    aa1 = Redmine::Export::PDF::RDMPdfEncoding::attach(Attachment.all, "Testfile.PNG", "UTF-8")
123
    assert_equal nil, aa1
124
    aa2 = Redmine::Export::PDF::RDMPdfEncoding::attach(Attachment.all, "test#{str2}.png", encoding)
125
    assert_equal nil, aa2
126

  
127
    set_tmp_attachments_directory
128
  end
129
end
.svn/pristine/94/94687f576f5fe0537e2f3dcf7fc47fbcb965fd20.svn-base
1
<div id="block_<%= block_name.dasherize %>" class="mypage-box">
2

  
3
    <div style="float:right;margin-right:16px;z-index:500;">
4
    <%= link_to "", {:action => "remove_block", :block => block_name}, :method => 'post', :class => "close-icon" %>
5
    </div>
6

  
7
    <div class="handle">
8
    <%= render :partial => "my/blocks/#{block_name}", :locals => { :user => user } %>
9
    </div>
10
</div>
.svn/pristine/94/94e11c870fc879bb4baf66f3ed996a69a2f5f60d.svn-base
1
module ObjectHelpers
2
  def User.generate!(attributes={})
3
    @generated_user_login ||= 'user0'
4
    @generated_user_login.succ!
5
    user = User.new(attributes)
6
    user.login = @generated_user_login if user.login.blank?
7
    user.mail = "#{@generated_user_login}@example.com" if user.mail.blank?
8
    user.firstname = "Bob" if user.firstname.blank?
9
    user.lastname = "Doe" if user.lastname.blank?
10
    yield user if block_given?
11
    user.save!
12
    user
13
  end
14

  
15
  def User.add_to_project(user, project, roles=nil)
16
    roles = Role.find(1) if roles.nil?
17
    roles = [roles] unless roles.is_a?(Array)
18
    Member.create!(:principal => user, :project => project, :roles => roles)
19
  end
20

  
21
  def Group.generate!(attributes={})
22
    @generated_group_name ||= 'Group 0'
23
    @generated_group_name.succ!
24
    group = Group.new(attributes)
25
    group.name = @generated_group_name if group.name.blank?
26
    yield group if block_given?
27
    group.save!
28
    group
29
  end
30

  
31
  def Project.generate!(attributes={})
32
    @generated_project_identifier ||= 'project-0000'
33
    @generated_project_identifier.succ!
34
    project = Project.new(attributes)
35
    project.name = @generated_project_identifier if project.name.blank?
36
    project.identifier = @generated_project_identifier if project.identifier.blank?
37
    yield project if block_given?
38
    project.save!
39
    project
40
  end
41

  
42
  def Tracker.generate!(attributes={})
43
    @generated_tracker_name ||= 'Tracker 0'
44
    @generated_tracker_name.succ!
45
    tracker = Tracker.new(attributes)
46
    tracker.name = @generated_tracker_name if tracker.name.blank?
47
    yield tracker if block_given?
48
    tracker.save!
49
    tracker
50
  end
51

  
52
  def Role.generate!(attributes={})
53
    @generated_role_name ||= 'Role 0'
54
    @generated_role_name.succ!
55
    role = Role.new(attributes)
56
    role.name = @generated_role_name if role.name.blank?
57
    yield role if block_given?
58
    role.save!
59
    role
60
  end
61

  
62
  def Issue.generate!(attributes={})
63
    issue = Issue.new(attributes)
64
    issue.project ||= Project.find(1)
65
    issue.tracker ||= issue.project.trackers.first
66
    issue.subject = 'Generated' if issue.subject.blank?
67
    issue.author ||= User.find(2)
68
    yield issue if block_given?
69
    issue.save!
70
    issue
71
  end
72

  
73
  # Generates an issue with 2 children and a grandchild
74
  def Issue.generate_with_descendants!(attributes={})
75
    issue = Issue.generate!(attributes)
76
    child = Issue.generate!(:project => issue.project, :subject => 'Child1', :parent_issue_id => issue.id)
77
    Issue.generate!(:project => issue.project, :subject => 'Child2', :parent_issue_id => issue.id)
78
    Issue.generate!(:project => issue.project, :subject => 'Child11', :parent_issue_id => child.id)
79
    issue.reload
80
  end
81

  
82
  def Journal.generate!(attributes={})
83
    journal = Journal.new(attributes)
84
    journal.user ||= User.first
85
    journal.journalized ||= Issue.first
86
    yield journal if block_given?
87
    journal.save!
88
    journal
89
  end
90

  
91
  def Version.generate!(attributes={})
92
    @generated_version_name ||= 'Version 0'
93
    @generated_version_name.succ!
94
    version = Version.new(attributes)
95
    version.name = @generated_version_name if version.name.blank?
96
    yield version if block_given?
97
    version.save!
98
    version
99
  end
100

  
101
  def AuthSource.generate!(attributes={})
102
    @generated_auth_source_name ||= 'Auth 0'
103
    @generated_auth_source_name.succ!
104
    source = AuthSource.new(attributes)
105
    source.name = @generated_auth_source_name if source.name.blank?
106
    yield source if block_given?
107
    source.save!
108
    source
109
  end
110

  
111
  def Board.generate!(attributes={})
112
    @generated_board_name ||= 'Forum 0'
113
    @generated_board_name.succ!
114
    board = Board.new(attributes)
115
    board.name = @generated_board_name if board.name.blank?
116
    board.description = @generated_board_name if board.description.blank?
117
    yield board if block_given?
118
    board.save!
119
    board
120
  end
121
end
.svn/pristine/94/94e5ac8fbd269485670f62fe0e6e82b861c05911.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  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 BazaarAdapter < AbstractAdapter
24

  
25
        # Bazaar executable name
26
        BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr"
27

  
28
        class << self
29
          def client_command
30
            @@bin    ||= BZR_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") { |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
          @url = url
62
          @root_url = url
63
          @path_encoding = 'UTF-8'
64
          # do not call *super* for non ASCII repository path
65
        end
66

  
67
        def bzr_path_encodig=(encoding)
68
          @path_encoding = encoding
69
        end
70

  
71
        # Get info about the repository
72
        def info
73
          cmd_args = %w|revno|
74
          cmd_args << bzr_target('')
75
          info = nil
76
          scm_cmd(*cmd_args) do |io|
77
            if io.read =~ %r{^(\d+)\r?$}
78
              info = Info.new({:root_url => url,
79
                               :lastrev => Revision.new({
80
                                 :identifier => $1
81
                               })
82
                             })
83
            end
84
          end
85
          info
86
        rescue ScmCommandAborted
87
          return nil
88
        end
89

  
90
        # Returns an Entries collection
91
        # or nil if the given path doesn't exist in the repository
92
        def entries(path=nil, identifier=nil, options={})
93
          path ||= ''
94
          entries = Entries.new
95
          identifier = -1 unless identifier && identifier.to_i > 0
96
          cmd_args = %w|ls -v --show-ids|
97
          cmd_args << "-r#{identifier.to_i}"
98
          cmd_args << bzr_target(path)
99
          scm_cmd(*cmd_args) do |io|
100
            prefix_utf8 = "#{url}/#{path}".gsub('\\', '/')
101
            logger.debug "PREFIX: #{prefix_utf8}"
102
            prefix = scm_iconv(@path_encoding, 'UTF-8', prefix_utf8)
103
            prefix.force_encoding('ASCII-8BIT') if prefix.respond_to?(:force_encoding)
104
            re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
105
            io.each_line do |line|
106
              next unless line =~ re
107
              name_locale = $3.strip
108
              name = scm_iconv('UTF-8', @path_encoding, name_locale)
109
              entries << Entry.new({:name => name,
110
                                    :path => ((path.empty? ? "" : "#{path}/") + name),
111
                                    :kind => ($4.blank? ? 'file' : 'dir'),
112
                                    :size => nil,
113
                                    :lastrev => Revision.new(:revision => $5.strip)
114
                                  })
115
            end
116
          end
117
          if logger && logger.debug?
118
            logger.debug("Found #{entries.size} entries in the repository for #{target(path)}")
119
          end
120
          entries.sort_by_name
121
        rescue ScmCommandAborted
122
          return nil
123
        end
124

  
125
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
126
          path ||= ''
127
          identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
128
          identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
129
          revisions = Revisions.new
130
          cmd_args = %w|log -v --show-ids|
131
          cmd_args << "-r#{identifier_to}..#{identifier_from}"
132
          cmd_args << bzr_target(path)
133
          scm_cmd(*cmd_args) do |io|
134
            revision = nil
135
            parsing  = nil
136
            io.each_line do |line|
137
              if line =~ /^----/
138
                revisions << revision if revision
139
                revision = Revision.new(:paths => [], :message => '')
140
                parsing = nil
141
              else
142
                next unless revision
143
                if line =~ /^revno: (\d+)($|\s\[merge\]$)/
144
                  revision.identifier = $1.to_i
145
                elsif line =~ /^committer: (.+)$/
146
                  revision.author = $1.strip
147
                elsif line =~ /^revision-id:(.+)$/
148
                  revision.scmid = $1.strip
149
                elsif line =~ /^timestamp: (.+)$/
150
                  revision.time = Time.parse($1).localtime
151
                elsif line =~ /^    -----/
152
                  # partial revisions
153
                  parsing = nil unless parsing == 'message'
154
                elsif line =~ /^(message|added|modified|removed|renamed):/
155
                  parsing = $1
156
                elsif line =~ /^  (.*)$/
157
                  if parsing == 'message'
158
                    revision.message << "#{$1}\n"
159
                  else
160
                    if $1 =~ /^(.*)\s+(\S+)$/
161
                      path_locale = $1.strip
162
                      path = scm_iconv('UTF-8', @path_encoding, path_locale)
163
                      revid = $2
164
                      case parsing
165
                      when 'added'
166
                        revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
167
                      when 'modified'
168
                        revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
169
                      when 'removed'
170
                        revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
171
                      when 'renamed'
172
                        new_path = path.split('=>').last
173
                        if new_path
174
                          revision.paths << {:action => 'M', :path => "/#{new_path.strip}",
175
                                             :revision => revid}
176
                        end
177
                      end
178
                    end
179
                  end
180
                else
181
                  parsing = nil
182
                end
183
              end
184
            end
185
            revisions << revision if revision
186
          end
187
          revisions
188
        rescue ScmCommandAborted
189
          return nil
190
        end
191

  
192
        def diff(path, identifier_from, identifier_to=nil)
193
          path ||= ''
194
          if identifier_to
195
            identifier_to = identifier_to.to_i
196
          else
197
            identifier_to = identifier_from.to_i - 1
198
          end
199
          if identifier_from
200
            identifier_from = identifier_from.to_i
201
          end
202
          diff = []
203
          cmd_args = %w|diff|
204
          cmd_args << "-r#{identifier_to}..#{identifier_from}"
205
          cmd_args << bzr_target(path)
206
          scm_cmd_no_raise(*cmd_args) do |io|
207
            io.each_line do |line|
208
              diff << line
209
            end
210
          end
211
          diff
212
        end
213

  
214
        def cat(path, identifier=nil)
215
          cat = nil
216
          cmd_args = %w|cat|
217
          cmd_args << "-r#{identifier.to_i}" if identifier && identifier.to_i > 0
218
          cmd_args << bzr_target(path)
219
          scm_cmd(*cmd_args) do |io|
220
            io.binmode
221
            cat = io.read
222
          end
223
          cat
224
        rescue ScmCommandAborted
225
          return nil
226
        end
227

  
228
        def annotate(path, identifier=nil)
229
          blame = Annotate.new
230
          cmd_args = %w|annotate -q --all|
231
          cmd_args << "-r#{identifier.to_i}" if identifier && identifier.to_i > 0
232
          cmd_args << bzr_target(path)
233
          scm_cmd(*cmd_args) do |io|
234
            author     = nil
235
            identifier = nil
236
            io.each_line do |line|
237
              next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
238
              rev = $1
239
              blame.add_line($3.rstrip,
240
                 Revision.new(
241
                  :identifier => rev,
242
                  :revision   => rev,
243
                  :author     => $2.strip
244
                  ))
245
            end
246
          end
247
          blame
248
        rescue ScmCommandAborted
249
          return nil
250
        end
251

  
252
        def self.branch_conf_path(path)
253
          bcp = nil
254
          m = path.match(%r{^(.*[/\\])\.bzr.*$})
255
          if m
256
            bcp = m[1]
257
          else
258
            bcp = path
259
          end
260
          bcp.gsub!(%r{[\/\\]$}, "")
261
          if bcp
262
            bcp = File.join(bcp, ".bzr", "branch", "branch.conf")
263
          end
264
          bcp
265
        end
266

  
267
        def append_revisions_only
268
          return @aro if ! @aro.nil?
269
          @aro = false
270
          bcp = self.class.branch_conf_path(url)
271
          if bcp && File.exist?(bcp)
272
            begin
273
              f = File::open(bcp, "r")
274
              cnt = 0
275
              f.each_line do |line|
276
                l = line.chomp.to_s
277
                if l =~ /^\s*append_revisions_only\s*=\s*(\w+)\s*$/
278
                  str_aro = $1
279
                  if str_aro.upcase == "TRUE"
280
                    @aro = true
281
                    cnt += 1
282
                  elsif str_aro.upcase == "FALSE"
283
                    @aro = false
284
                    cnt += 1
285
                  end
286
                  if cnt > 1
287
                    @aro = false
288
                    break
289
                  end
290
                end
291
              end
292
            ensure
293
              f.close
294
            end
295
          end
296
          @aro
297
        end
298

  
299
        def scm_cmd(*args, &block)
300
          full_args = []
301
          full_args += args
302
          full_args_locale = []
303
          full_args.map do |e|
304
            full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
305
          end
306
          ret = shellout(
307
                   self.class.sq_bin + ' ' + 
308
                     full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
309
                   &block
310
                   )
311
          if $? && $?.exitstatus != 0
312
            raise ScmCommandAborted, "bzr exited with non-zero status: #{$?.exitstatus}"
313
          end
314
          ret
315
        end
316
        private :scm_cmd
317

  
318
        def scm_cmd_no_raise(*args, &block)
319
          full_args = []
320
          full_args += args
321
          full_args_locale = []
322
          full_args.map do |e|
323
            full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
324
          end
325
          ret = shellout(
326
                   self.class.sq_bin + ' ' + 
327
                     full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
328
                   &block
329
                   )
330
          ret
331
        end
332
        private :scm_cmd_no_raise
333

  
334
        def bzr_target(path)
335
          target(path, false)
336
        end
337
        private :bzr_target
338
      end
339
    end
340
  end
341
end

Also available in: Unified diff