To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / lib / redmine / scm / adapters / .svn / text-base / git_adapter.rb.svn-base @ 441:cbce1fd3b1b7

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