bazaar_adapter.rb 11.4 KB
Newer Older
1
# Redmine - project management software
jplang's avatar
jplang committed
2
# Copyright (C) 2006-2017  Jean-Philippe Lang
jplang's avatar
jplang committed
3 4 5 6 7
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
8
#
jplang's avatar
jplang committed
9 10 11 12
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
13
#
jplang's avatar
jplang committed
14 15 16 17
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

18
require 'redmine/scm/adapters/abstract_adapter'
jplang's avatar
jplang committed
19 20 21

module Redmine
  module Scm
22
    module Adapters
jplang's avatar
jplang committed
23
      class BazaarAdapter < AbstractAdapter
24

jplang's avatar
jplang committed
25
        # Bazaar executable name
26
        BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr"
27 28 29 30 31 32 33

        class << self
          def client_command
            @@bin    ||= BZR_BIN
          end

          def sq_bin
34
            @@sq_bin ||= shell_quote_command
35
          end
36 37 38 39 40 41 42 43 44 45

          def client_version
            @@client_version ||= (scm_command_version || [])
          end

          def client_available
            !client_version.empty?
          end

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

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

57
        def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
58 59
          @url = url
          @root_url = url
60
          @path_encoding = 'UTF-8'
61
          # do not call *super* for non ASCII repository path
62 63
        end

64 65 66 67
        def bzr_path_encodig=(encoding)
          @path_encoding = encoding
        end

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

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

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

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

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

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

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

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

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

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

331 332 333 334
        def bzr_target(path)
          target(path, false)
        end
        private :bzr_target
jplang's avatar
jplang committed
335 336 337 338
      end
    end
  end
end