To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / .svn / pristine / a1 / a10004c586b008b5d6a646ee54831265d7e69cc1.svn-base @ 1297:0a574315af3e
History | View | Annotate | Download (14.4 KB)
| 1 | 1296:038ba2d95de8 | Chris | # 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 GitAdapter < AbstractAdapter |
||
| 24 | |||
| 25 | # Git executable name |
||
| 26 | GIT_BIN = Redmine::Configuration['scm_git_command'] || "git" |
||
| 27 | |||
| 28 | class GitBranch < Branch |
||
| 29 | attr_accessor :is_default |
||
| 30 | end |
||
| 31 | |||
| 32 | class << self |
||
| 33 | def client_command |
||
| 34 | @@bin ||= GIT_BIN |
||
| 35 | end |
||
| 36 | |||
| 37 | def sq_bin |
||
| 38 | @@sq_bin ||= shell_quote_command |
||
| 39 | end |
||
| 40 | |||
| 41 | def client_version |
||
| 42 | @@client_version ||= (scm_command_version || []) |
||
| 43 | end |
||
| 44 | |||
| 45 | def client_available |
||
| 46 | !client_version.empty? |
||
| 47 | end |
||
| 48 | |||
| 49 | def scm_command_version |
||
| 50 | scm_version = scm_version_from_command_line.dup |
||
| 51 | if scm_version.respond_to?(:force_encoding) |
||
| 52 | scm_version.force_encoding('ASCII-8BIT')
|
||
| 53 | end |
||
| 54 | if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
|
||
| 55 | m[2].scan(%r{\d+}).collect(&:to_i)
|
||
| 56 | end |
||
| 57 | end |
||
| 58 | |||
| 59 | def scm_version_from_command_line |
||
| 60 | shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
|
||
| 61 | end |
||
| 62 | end |
||
| 63 | |||
| 64 | def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil) |
||
| 65 | super |
||
| 66 | @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding |
||
| 67 | end |
||
| 68 | |||
| 69 | def path_encoding |
||
| 70 | @path_encoding |
||
| 71 | end |
||
| 72 | |||
| 73 | def info |
||
| 74 | begin |
||
| 75 | Info.new(:root_url => url, :lastrev => lastrev('',nil))
|
||
| 76 | rescue |
||
| 77 | nil |
||
| 78 | end |
||
| 79 | end |
||
| 80 | |||
| 81 | def branches |
||
| 82 | return @branches if @branches |
||
| 83 | @branches = [] |
||
| 84 | cmd_args = %w|branch --no-color --verbose --no-abbrev| |
||
| 85 | git_cmd(cmd_args) do |io| |
||
| 86 | io.each_line do |line| |
||
| 87 | branch_rev = line.match('\s*(\*?)\s*(.*?)\s*([0-9a-f]{40}).*$')
|
||
| 88 | bran = GitBranch.new(branch_rev[2]) |
||
| 89 | bran.revision = branch_rev[3] |
||
| 90 | bran.scmid = branch_rev[3] |
||
| 91 | bran.is_default = ( branch_rev[1] == '*' ) |
||
| 92 | @branches << bran |
||
| 93 | end |
||
| 94 | end |
||
| 95 | @branches.sort! |
||
| 96 | rescue ScmCommandAborted |
||
| 97 | nil |
||
| 98 | end |
||
| 99 | |||
| 100 | def tags |
||
| 101 | return @tags if @tags |
||
| 102 | cmd_args = %w|tag| |
||
| 103 | git_cmd(cmd_args) do |io| |
||
| 104 | @tags = io.readlines.sort!.map{|t| t.strip}
|
||
| 105 | end |
||
| 106 | rescue ScmCommandAborted |
||
| 107 | nil |
||
| 108 | end |
||
| 109 | |||
| 110 | def default_branch |
||
| 111 | bras = self.branches |
||
| 112 | return nil if bras.nil? |
||
| 113 | default_bras = bras.select{|x| x.is_default == true}
|
||
| 114 | return default_bras.first.to_s if ! default_bras.empty? |
||
| 115 | master_bras = bras.select{|x| x.to_s == 'master'}
|
||
| 116 | master_bras.empty? ? bras.first.to_s : 'master' |
||
| 117 | end |
||
| 118 | |||
| 119 | def entry(path=nil, identifier=nil) |
||
| 120 | parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
|
||
| 121 | search_path = parts[0..-2].join('/')
|
||
| 122 | search_name = parts[-1] |
||
| 123 | if search_path.blank? && search_name.blank? |
||
| 124 | # Root entry |
||
| 125 | Entry.new(:path => '', :kind => 'dir') |
||
| 126 | else |
||
| 127 | # Search for the entry in the parent directory |
||
| 128 | es = entries(search_path, identifier, |
||
| 129 | options = {:report_last_commit => false})
|
||
| 130 | es ? es.detect {|e| e.name == search_name} : nil
|
||
| 131 | end |
||
| 132 | end |
||
| 133 | |||
| 134 | def entries(path=nil, identifier=nil, options={})
|
||
| 135 | path ||= '' |
||
| 136 | p = scm_iconv(@path_encoding, 'UTF-8', path) |
||
| 137 | entries = Entries.new |
||
| 138 | cmd_args = %w|ls-tree -l| |
||
| 139 | cmd_args << "HEAD:#{p}" if identifier.nil?
|
||
| 140 | cmd_args << "#{identifier}:#{p}" if identifier
|
||
| 141 | git_cmd(cmd_args) do |io| |
||
| 142 | io.each_line do |line| |
||
| 143 | e = line.chomp.to_s |
||
| 144 | if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
|
||
| 145 | type = $1 |
||
| 146 | sha = $2 |
||
| 147 | size = $3 |
||
| 148 | name = $4 |
||
| 149 | if name.respond_to?(:force_encoding) |
||
| 150 | name.force_encoding(@path_encoding) |
||
| 151 | end |
||
| 152 | full_path = p.empty? ? name : "#{p}/#{name}"
|
||
| 153 | n = scm_iconv('UTF-8', @path_encoding, name)
|
||
| 154 | full_p = scm_iconv('UTF-8', @path_encoding, full_path)
|
||
| 155 | entries << Entry.new({:name => n,
|
||
| 156 | :path => full_p, |
||
| 157 | :kind => (type == "tree") ? 'dir' : 'file', |
||
| 158 | :size => (type == "tree") ? nil : size, |
||
| 159 | :lastrev => options[:report_last_commit] ? |
||
| 160 | lastrev(full_path, identifier) : Revision.new |
||
| 161 | }) unless entries.detect{|entry| entry.name == name}
|
||
| 162 | end |
||
| 163 | end |
||
| 164 | end |
||
| 165 | entries.sort_by_name |
||
| 166 | rescue ScmCommandAborted |
||
| 167 | nil |
||
| 168 | end |
||
| 169 | |||
| 170 | def lastrev(path, rev) |
||
| 171 | return nil if path.nil? |
||
| 172 | cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1| |
||
| 173 | cmd_args << rev if rev |
||
| 174 | cmd_args << "--" << path unless path.empty? |
||
| 175 | lines = [] |
||
| 176 | git_cmd(cmd_args) { |io| lines = io.readlines }
|
||
| 177 | begin |
||
| 178 | id = lines[0].split[1] |
||
| 179 | author = lines[1].match('Author:\s+(.*)$')[1]
|
||
| 180 | time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
|
||
| 181 | |||
| 182 | Revision.new({
|
||
| 183 | :identifier => id, |
||
| 184 | :scmid => id, |
||
| 185 | :author => author, |
||
| 186 | :time => time, |
||
| 187 | :message => nil, |
||
| 188 | :paths => nil |
||
| 189 | }) |
||
| 190 | rescue NoMethodError => e |
||
| 191 | logger.error("The revision '#{path}' has a wrong format")
|
||
| 192 | return nil |
||
| 193 | end |
||
| 194 | rescue ScmCommandAborted |
||
| 195 | nil |
||
| 196 | end |
||
| 197 | |||
| 198 | def revisions(path, identifier_from, identifier_to, options={})
|
||
| 199 | revs = Revisions.new |
||
| 200 | cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin| |
||
| 201 | cmd_args << "--reverse" if options[:reverse] |
||
| 202 | cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
|
||
| 203 | cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty? |
||
| 204 | revisions = [] |
||
| 205 | if identifier_from || identifier_to |
||
| 206 | revisions << "" |
||
| 207 | revisions[0] << "#{identifier_from}.." if identifier_from
|
||
| 208 | revisions[0] << "#{identifier_to}" if identifier_to
|
||
| 209 | else |
||
| 210 | unless options[:includes].blank? |
||
| 211 | revisions += options[:includes] |
||
| 212 | end |
||
| 213 | unless options[:excludes].blank? |
||
| 214 | revisions += options[:excludes].map{|r| "^#{r}"}
|
||
| 215 | end |
||
| 216 | end |
||
| 217 | |||
| 218 | git_cmd(cmd_args, {:write_stdin => true}) do |io|
|
||
| 219 | io.binmode |
||
| 220 | io.puts(revisions.join("\n"))
|
||
| 221 | io.close_write |
||
| 222 | files=[] |
||
| 223 | changeset = {}
|
||
| 224 | parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files |
||
| 225 | |||
| 226 | io.each_line do |line| |
||
| 227 | if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/
|
||
| 228 | key = "commit" |
||
| 229 | value = $1 |
||
| 230 | parents_str = $2 |
||
| 231 | if (parsing_descr == 1 || parsing_descr == 2) |
||
| 232 | parsing_descr = 0 |
||
| 233 | revision = Revision.new({
|
||
| 234 | :identifier => changeset[:commit], |
||
| 235 | :scmid => changeset[:commit], |
||
| 236 | :author => changeset[:author], |
||
| 237 | :time => Time.parse(changeset[:date]), |
||
| 238 | :message => changeset[:description], |
||
| 239 | :paths => files, |
||
| 240 | :parents => changeset[:parents] |
||
| 241 | }) |
||
| 242 | if block_given? |
||
| 243 | yield revision |
||
| 244 | else |
||
| 245 | revs << revision |
||
| 246 | end |
||
| 247 | changeset = {}
|
||
| 248 | files = [] |
||
| 249 | end |
||
| 250 | changeset[:commit] = $1 |
||
| 251 | unless parents_str.nil? or parents_str == "" |
||
| 252 | changeset[:parents] = parents_str.strip.split(' ')
|
||
| 253 | end |
||
| 254 | elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/ |
||
| 255 | key = $1 |
||
| 256 | value = $2 |
||
| 257 | if key == "Author" |
||
| 258 | changeset[:author] = value |
||
| 259 | elsif key == "CommitDate" |
||
| 260 | changeset[:date] = value |
||
| 261 | end |
||
| 262 | elsif (parsing_descr == 0) && line.chomp.to_s == "" |
||
| 263 | parsing_descr = 1 |
||
| 264 | changeset[:description] = "" |
||
| 265 | elsif (parsing_descr == 1 || parsing_descr == 2) \ |
||
| 266 | && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/ |
||
| 267 | parsing_descr = 2 |
||
| 268 | fileaction = $1 |
||
| 269 | filepath = $2 |
||
| 270 | p = scm_iconv('UTF-8', @path_encoding, filepath)
|
||
| 271 | files << {:action => fileaction, :path => p}
|
||
| 272 | elsif (parsing_descr == 1 || parsing_descr == 2) \ |
||
| 273 | && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/ |
||
| 274 | parsing_descr = 2 |
||
| 275 | fileaction = $1 |
||
| 276 | filepath = $3 |
||
| 277 | p = scm_iconv('UTF-8', @path_encoding, filepath)
|
||
| 278 | files << {:action => fileaction, :path => p}
|
||
| 279 | elsif (parsing_descr == 1) && line.chomp.to_s == "" |
||
| 280 | parsing_descr = 2 |
||
| 281 | elsif (parsing_descr == 1) |
||
| 282 | changeset[:description] << line[4..-1] |
||
| 283 | end |
||
| 284 | end |
||
| 285 | |||
| 286 | if changeset[:commit] |
||
| 287 | revision = Revision.new({
|
||
| 288 | :identifier => changeset[:commit], |
||
| 289 | :scmid => changeset[:commit], |
||
| 290 | :author => changeset[:author], |
||
| 291 | :time => Time.parse(changeset[:date]), |
||
| 292 | :message => changeset[:description], |
||
| 293 | :paths => files, |
||
| 294 | :parents => changeset[:parents] |
||
| 295 | }) |
||
| 296 | if block_given? |
||
| 297 | yield revision |
||
| 298 | else |
||
| 299 | revs << revision |
||
| 300 | end |
||
| 301 | end |
||
| 302 | end |
||
| 303 | revs |
||
| 304 | rescue ScmCommandAborted => e |
||
| 305 | err_msg = "git log error: #{e.message}"
|
||
| 306 | logger.error(err_msg) |
||
| 307 | if block_given? |
||
| 308 | raise CommandFailed, err_msg |
||
| 309 | else |
||
| 310 | revs |
||
| 311 | end |
||
| 312 | end |
||
| 313 | |||
| 314 | def diff(path, identifier_from, identifier_to=nil) |
||
| 315 | path ||= '' |
||
| 316 | cmd_args = [] |
||
| 317 | if identifier_to |
||
| 318 | cmd_args << "diff" << "--no-color" << identifier_to << identifier_from |
||
| 319 | else |
||
| 320 | cmd_args << "show" << "--no-color" << identifier_from |
||
| 321 | end |
||
| 322 | cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty? |
||
| 323 | diff = [] |
||
| 324 | git_cmd(cmd_args) do |io| |
||
| 325 | io.each_line do |line| |
||
| 326 | diff << line |
||
| 327 | end |
||
| 328 | end |
||
| 329 | diff |
||
| 330 | rescue ScmCommandAborted |
||
| 331 | nil |
||
| 332 | end |
||
| 333 | |||
| 334 | def annotate(path, identifier=nil) |
||
| 335 | identifier = 'HEAD' if identifier.blank? |
||
| 336 | cmd_args = %w|blame| |
||
| 337 | cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path) |
||
| 338 | blame = Annotate.new |
||
| 339 | content = nil |
||
| 340 | git_cmd(cmd_args) { |io| io.binmode; content = io.read }
|
||
| 341 | # git annotates binary files |
||
| 342 | return nil if content.is_binary_data? |
||
| 343 | identifier = '' |
||
| 344 | # git shows commit author on the first occurrence only |
||
| 345 | authors_by_commit = {}
|
||
| 346 | content.split("\n").each do |line|
|
||
| 347 | if line =~ /^([0-9a-f]{39,40})\s.*/
|
||
| 348 | identifier = $1 |
||
| 349 | elsif line =~ /^author (.+)/ |
||
| 350 | authors_by_commit[identifier] = $1.strip |
||
| 351 | elsif line =~ /^\t(.*)/ |
||
| 352 | blame.add_line($1, Revision.new( |
||
| 353 | :identifier => identifier, |
||
| 354 | :revision => identifier, |
||
| 355 | :scmid => identifier, |
||
| 356 | :author => authors_by_commit[identifier] |
||
| 357 | )) |
||
| 358 | identifier = '' |
||
| 359 | author = '' |
||
| 360 | end |
||
| 361 | end |
||
| 362 | blame |
||
| 363 | rescue ScmCommandAborted |
||
| 364 | nil |
||
| 365 | end |
||
| 366 | |||
| 367 | def cat(path, identifier=nil) |
||
| 368 | if identifier.nil? |
||
| 369 | identifier = 'HEAD' |
||
| 370 | end |
||
| 371 | cmd_args = %w|show --no-color| |
||
| 372 | cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
|
||
| 373 | cat = nil |
||
| 374 | git_cmd(cmd_args) do |io| |
||
| 375 | io.binmode |
||
| 376 | cat = io.read |
||
| 377 | end |
||
| 378 | cat |
||
| 379 | rescue ScmCommandAborted |
||
| 380 | nil |
||
| 381 | end |
||
| 382 | |||
| 383 | class Revision < Redmine::Scm::Adapters::Revision |
||
| 384 | # Returns the readable identifier |
||
| 385 | def format_identifier |
||
| 386 | identifier[0,8] |
||
| 387 | end |
||
| 388 | end |
||
| 389 | |||
| 390 | def git_cmd(args, options = {}, &block)
|
||
| 391 | repo_path = root_url || url |
||
| 392 | full_args = ['--git-dir', repo_path] |
||
| 393 | if self.class.client_version_above?([1, 7, 2]) |
||
| 394 | full_args << '-c' << 'core.quotepath=false' |
||
| 395 | full_args << '-c' << 'log.decorate=no' |
||
| 396 | end |
||
| 397 | full_args += args |
||
| 398 | ret = shellout( |
||
| 399 | self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
|
||
| 400 | options, |
||
| 401 | &block |
||
| 402 | ) |
||
| 403 | if $? && $?.exitstatus != 0 |
||
| 404 | raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
|
||
| 405 | end |
||
| 406 | ret |
||
| 407 | end |
||
| 408 | private :git_cmd |
||
| 409 | end |
||
| 410 | end |
||
| 411 | end |
||
| 412 | end |