diff lib/redmine/scm/adapters/git_adapter.rb @ 0:513646585e45

* Import Redmine trunk SVN rev 3859
author Chris Cannam
date Fri, 23 Jul 2010 15:52:44 +0100
parents
children 1d32c0a0efbf
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/redmine/scm/adapters/git_adapter.rb	Fri Jul 23 15:52:44 2010 +0100
@@ -0,0 +1,270 @@
+# redMine - project management software
+# Copyright (C) 2006-2007  Jean-Philippe Lang
+#
+# 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.
+# 
+# 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.
+# 
+# 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.
+
+require 'redmine/scm/adapters/abstract_adapter'
+
+module Redmine
+  module Scm
+    module Adapters    
+      class GitAdapter < AbstractAdapter
+        # Git executable name
+        GIT_BIN = "git"
+
+        def info
+          begin
+            Info.new(:root_url => url, :lastrev => lastrev('',nil))
+          rescue
+            nil
+          end
+        end
+
+        def branches
+          return @branches if @branches
+          @branches = []
+          cmd = "#{GIT_BIN} --git-dir #{target('')} branch"
+          shellout(cmd) do |io|
+            io.each_line do |line|
+              @branches << line.match('\s*\*?\s*(.*)$')[1]
+            end
+          end
+          @branches.sort!
+        end
+
+        def tags
+          return @tags if @tags
+          cmd = "#{GIT_BIN} --git-dir #{target('')} tag"
+          shellout(cmd) do |io|
+            @tags = io.readlines.sort!.map{|t| t.strip}
+          end
+        end
+
+        def default_branch
+          branches.include?('master') ? 'master' : branches.first 
+        end
+        
+        def entries(path=nil, identifier=nil)
+          path ||= ''
+          entries = Entries.new
+          cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
+          cmd << shell_quote("HEAD:" + path) if identifier.nil?
+          cmd << shell_quote(identifier + ":" + path) if identifier
+          shellout(cmd)  do |io|
+            io.each_line do |line|
+              e = line.chomp.to_s
+              if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\s+(.+)$/
+                type = $1
+                sha = $2
+                size = $3
+                name = $4
+                full_path = path.empty? ? name : "#{path}/#{name}"
+                entries << Entry.new({:name => name,
+                 :path => full_path,
+                 :kind => (type == "tree") ? 'dir' : 'file',
+                 :size => (type == "tree") ? nil : size,
+                 :lastrev => lastrev(full_path,identifier)
+                }) unless entries.detect{|entry| entry.name == name}
+              end
+            end
+          end
+          return nil if $? && $?.exitstatus != 0
+          entries.sort_by_name
+        end
+
+        def lastrev(path,rev)
+          return nil if path.nil?
+          cmd = "#{GIT_BIN} --git-dir #{target('')} log --pretty=fuller --no-merges -n 1 "
+          cmd << " #{shell_quote rev} " if rev 
+          cmd <<  "-- #{path} " unless path.empty?
+          shellout(cmd) do |io|
+            begin
+              id = io.gets.split[1]
+              author = io.gets.match('Author:\s+(.*)$')[1]
+              2.times { io.gets }
+              time = io.gets.match('CommitDate:\s+(.*)$')[1]
+
+              Revision.new({
+                :identifier => id,
+                :scmid => id,
+                :author => author, 
+                :time => time,
+                :message => nil, 
+                :paths => nil 
+              })
+            rescue NoMethodError => e
+              logger.error("The revision '#{path}' has a wrong format")
+              return nil
+            end
+          end
+        end
+
+        def revisions(path, identifier_from, identifier_to, options={})
+          revisions = Revisions.new
+
+          cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw --date=iso --pretty=fuller"
+          cmd << " --reverse" if options[:reverse]
+          cmd << " --all" if options[:all]
+          cmd << " -n #{options[:limit]} " if options[:limit]
+          cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from
+          cmd << " #{shell_quote identifier_to} " if identifier_to
+          cmd << " --since=#{shell_quote(options[:since].strftime("%Y-%m-%d %H:%M:%S"))}" if options[:since]
+          cmd << " -- #{path}" if path && !path.empty?
+
+          shellout(cmd) do |io|
+            files=[]
+            changeset = {}
+            parsing_descr = 0  #0: not parsing desc or files, 1: parsing desc, 2: parsing files
+            revno = 1
+
+            io.each_line do |line|
+              if line =~ /^commit ([0-9a-f]{40})$/
+                key = "commit"
+                value = $1
+                if (parsing_descr == 1 || parsing_descr == 2)
+                  parsing_descr = 0
+                  revision = Revision.new({
+                    :identifier => changeset[:commit],
+                    :scmid => changeset[:commit],
+                    :author => changeset[:author],
+                    :time => Time.parse(changeset[:date]),
+                    :message => changeset[:description],
+                    :paths => files
+                  })
+                  if block_given?
+                    yield revision
+                  else
+                    revisions << revision
+                  end
+                  changeset = {}
+                  files = []
+                  revno = revno + 1
+                end
+                changeset[:commit] = $1
+              elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
+                key = $1
+                value = $2
+                if key == "Author"
+                  changeset[:author] = value
+                elsif key == "CommitDate"
+                  changeset[:date] = value
+                end
+              elsif (parsing_descr == 0) && line.chomp.to_s == ""
+                parsing_descr = 1
+                changeset[:description] = ""
+              elsif (parsing_descr == 1 || parsing_descr == 2) \
+              && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
+                parsing_descr = 2
+                fileaction = $1
+                filepath = $2
+                files << {:action => fileaction, :path => filepath}
+              elsif (parsing_descr == 1 || parsing_descr == 2) \
+              && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\s+(.+)$/
+                parsing_descr = 2
+                fileaction = $1
+                filepath = $3
+                files << {:action => fileaction, :path => filepath}
+              elsif (parsing_descr == 1) && line.chomp.to_s == ""
+                parsing_descr = 2
+              elsif (parsing_descr == 1)
+                changeset[:description] << line[4..-1]
+              end
+            end 
+
+            if changeset[:commit]
+              revision = Revision.new({
+                :identifier => changeset[:commit],
+                :scmid => changeset[:commit],
+                :author => changeset[:author],
+                :time => Time.parse(changeset[:date]),
+                :message => changeset[:description],
+                :paths => files
+              })
+
+              if block_given?
+                yield revision
+              else
+                revisions << revision
+              end
+            end
+          end
+
+          return nil if $? && $?.exitstatus != 0
+          revisions
+        end
+
+        def diff(path, identifier_from, identifier_to=nil)
+          path ||= ''
+
+          if identifier_to
+            cmd = "#{GIT_BIN} --git-dir #{target('')} diff #{shell_quote identifier_to} #{shell_quote identifier_from}" 
+          else
+            cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote identifier_from}"
+          end
+
+          cmd << " -- #{shell_quote path}" unless path.empty?
+          diff = []
+          shellout(cmd) do |io|
+            io.each_line do |line|
+              diff << line
+            end
+          end
+          return nil if $? && $?.exitstatus != 0
+          diff
+        end
+        
+        def annotate(path, identifier=nil)
+          identifier = 'HEAD' if identifier.blank?
+          cmd = "#{GIT_BIN} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
+          blame = Annotate.new
+          content = nil
+          shellout(cmd) { |io| io.binmode; content = io.read }
+          return nil if $? && $?.exitstatus != 0
+          # git annotates binary files
+          return nil if content.is_binary_data?
+          identifier = ''
+          # git shows commit author on the first occurrence only
+          authors_by_commit = {}
+          content.split("\n").each do |line|
+            if line =~ /^([0-9a-f]{39,40})\s.*/
+              identifier = $1
+            elsif line =~ /^author (.+)/
+              authors_by_commit[identifier] = $1.strip
+            elsif line =~ /^\t(.*)/
+              blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
+              identifier = ''
+              author = ''
+            end
+          end
+          blame
+        end
+        
+        def cat(path, identifier=nil)
+          if identifier.nil?
+            identifier = 'HEAD'
+          end
+          cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote(identifier + ':' + path)}"
+          cat = nil
+          shellout(cmd) do |io|
+            io.binmode
+            cat = io.read
+          end
+          return nil if $? && $?.exitstatus != 0
+          cat
+        end
+      end
+    end
+  end
+end