#coding=utf-8 # require 'redmine/scm/adapters/abstract_adapter' require 'base64' module Redmine module Scm module Adapters class GitlabAdapter < AbstractAdapter class GitBranch < Branch attr_accessor :is_default end def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil) super @g = Gitlab.client @project = Repository.find_by_url(url).project.gpid @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding end def path_encoding @path_encoding end def info begin Info.new(:root_url => url, :lastrev => lastrev('',nil)) rescue nil end end def branches return @branches if @branches @branches = [] branches = @g.branches(@project) branches.each do |line| name = line.name scmid = line.commit.id bran = GitBranch.new(name) bran.revision = scmid bran.scmid = name bran.is_default = true #TODO @branches << bran end @branches.sort! rescue ScmCommandAborted nil end def tags return @tags if @tags tags = @g.tags(@project) @tags = [] tags.each do |tag| @tags << tag.name end rescue ScmCommandAborted nil end def default_branch bras = self.branches return nil if bras.nil? default_bras = bras.select{|x| x.is_default == true} return default_bras.first.to_s if ! default_bras.empty? master_bras = bras.select{|x| x.to_s == 'master'} master_bras.empty? ? bras.first.to_s : 'master' end def entry(path=nil, identifier=nil) parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?} search_path = parts[0..-2].join('/') search_name = parts[-1] if search_path.blank? && search_name.blank? # Root entry Entry.new(:path => '', :kind => 'dir') else # Search for the entry in the parent directory es = entries(search_path, identifier, options = {:report_last_commit => false}) es ? es.detect {|e| e.name == search_name} : nil end end def entries(path=nil, identifier=nil, options={}) entries = Entries.new trees = @g.trees(@project, path: path, ref_name: identifier) trees.each do |tree| entries << Entry.new({ :name => tree.name, :path => File.join(path,tree.name), :kind => tree.type == 'tree' ? 'dir' : 'file', :size => nil, :lastrev => nil }) end entries.sort_by_name rescue ScmCommandAborted nil rescue Exception nil end def lastrev(path, rev) return nil if path.nil? cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1| cmd_args << rev if rev cmd_args << "--" << path unless path.empty? lines = [] git_cmd(cmd_args) { |io| lines = io.readlines } begin id = lines[0].split[1] author = lines[1].match('Author:\s+(.*)$')[1] time = Time.parse(lines[4].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 rescue ScmCommandAborted nil end def revisions(path, identifier_from, identifier_to, options={}) revs = Revisions.new cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin| cmd_args << "--reverse" if options[:reverse] cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit] cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty? revisions = [] if identifier_from || identifier_to revisions << "" revisions[0] << "#{identifier_from}.." if identifier_from revisions[0] << "#{identifier_to}" if identifier_to else unless options[:includes].blank? revisions += options[:includes] end unless options[:excludes].blank? revisions += options[:excludes].map{|r| "^#{r}"} end end commits = @g.commits(@project, {ref_name: identifier_to}) commits.each do |commit| revision = Revision.new({ :identifier => commit.id, :scmid => commit.id, :author => commit.author_name, :time => Time.parse(commit.created_at), :message => commit.message, :paths => nil, :parents => nil }) revs << revision end revs rescue ScmCommandAborted => e err_msg = "git log error: #{e.message}" logger.error(err_msg) if block_given? raise CommandFailed, err_msg else revs end end def diff(path, identifier_from, identifier_to=nil) path ||= '' cmd_args = [] if identifier_to cmd_args << "diff" << "--no-color" << identifier_to << identifier_from else cmd_args << "show" << "--no-color" << identifier_from end cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty? diff = [] git_cmd(cmd_args) do |io| io.each_line do |line| diff << line end end diff rescue ScmCommandAborted nil end def annotate(path, identifier=nil) identifier = 'HEAD' if identifier.blank? cmd_args = %w|blame| cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path) blame = Annotate.new content = nil git_cmd(cmd_args) { |io| io.binmode; content = io.read } # 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, :revision => identifier, :scmid => identifier, :author => authors_by_commit[identifier] )) identifier = '' author = '' end end blame rescue ScmCommandAborted nil end def cat(path, identifier=nil) if identifier.nil? identifier = 'HEAD' end file = @g.files(@project, path, identifier) cat = Base64.decode64 file.content rescue ScmCommandAborted nil end def parse_commit(commits) sum = {file: 0, insertion: 0, deletion: 0} commits.split("\n").each do |commit| if /(\d+)\s+?file/ =~ commit sum[:file] += $1 .to_i end if /(\d+)\s+?insertion/ =~ commit sum[:insertion] += $1.to_i end if /(\d+)\s+?deletion/ =~ commit sum[:deletion] += $1.to_i end end sum[:insertion] + sum[:deletion] end def commits(authors, start_date, end_date, branch='master') rs = [] authors.each do |author| cmd_args = %W|log #{branch} --pretty=tformat: --shortstat --author=#{author} --since=#{start_date} --until=#{end_date}| commits = '' git_cmd(cmd_args) do |io| commits = io.read end logger.info "git log output for #{author} #{commits}" rs << {author: author, num: parse_commit(commits)} end rs end class Revision < Redmine::Scm::Adapters::Revision # Returns the readable identifier def format_identifier identifier[0,8] end end def git_cmd(args, options = {}, &block) logger.info "git cmd: #{args.join(' ')}" end private :git_cmd end end end end