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