ianheggie/cruisecontrol.rb

View on GitHub
lib/source_control/git.rb

Summary

Maintainability
A
35 mins
Test Coverage
module SourceControl

  class Git < AbstractAdapter

    attr_accessor :repository

    def initialize(options)
      options = options.dup
      @path = options.delete(:path) || "."
      @error_log = options.delete(:error_log)
      @interactive = options.delete(:interactive)
      @repository = options.delete(:repository)
      @branch = options.delete(:branch)
      @watch_for_changes_in = options.delete(:watch_for_changes_in)
      raise "don't know how to handle '#{options.keys.first}'" if options.length > 0
    end

    def checkout(revision = nil, stdout = $stdout, checkout_path = path)
      raise 'Repository location is not specified' unless @repository

      raise "#{checkout_path} is not empty, cannot clone a project into it" unless (Dir.entries(checkout_path) - ['.', '..']).empty?
      FileUtils.rm_rf(checkout_path)

      # need to read from command output, because otherwise tests break
      git('clone', [@repository, checkout_path], :execute_in_project_directory => false)

      if @branch and !@branch.empty? and @branch != current_branch
        git('branch', ['--track', @branch, "origin/#{@branch}"])
        git('checkout', ['-q', @branch]) # git prints 'Switched to branch "branch"' to stderr unless you pass -q 
      end
      git("reset", ['--hard', revision.number]) if revision
    end

    def clean_checkout(revision = nil, stdout = $stdout)
      # (-f) Forcing the clean in case git clean.requireForce is set to true
      # (-x) Remove untracked files including build products
      # (-d) Directory clean
      # (-q) Quiet, prevent git from writing unnecessary information to stdout/stderr 
      git('clean', ['-q', '-d', '-x', '-f'])
    end

    def latest_commiter_email
      load_new_changesets_from_origin
      git_output = git('log', ['-1', '--pretty=format:%ce', "origin/#{current_branch}"])
      git_output.to_s
    end

    def latest_revision
      load_new_changesets_from_origin
      git_output = git('log', ['-1', '--pretty=raw', '--stat', "origin/#{current_branch}"])
      Git::LogParser.new.parse(git_output).first
    end

    def update(revision = nil)
      if revision
        git("reset", ["--hard", revision.number])
      else
        git("reset", ["--hard"])
      end
      git_update_submodule
    end

    def up_to_date?(reasons = [])
      _new_revisions = new_revisions
      if _new_revisions.empty?
        return true
      else
        reasons.concat(_new_revisions)
        return false
      end
    end

    def creates_ordered_build_labels?() false end

    def new_revisions
      load_new_changesets_from_origin
      git_output = git('log', ['--pretty=raw', '--stat', "HEAD..origin/#{current_branch}"])
      revisions = Git::LogParser.new.parse(git_output)
      revisions = filter_revisions_by_subdirectory(revisions, @watch_for_changes_in) if @watch_for_changes_in
      revisions
    end

    def current_branch
      git('branch') do |io|
        branch = io.readlines.grep(/^\* .*$/).first[2..-1].strip
        return branch
      end
    end

    protected
    
    def filter_revisions_by_subdirectory(revisions, subdir)
      revisions.find_all do |revision|
        if revision.changeset
          revision.changeset = revision.changeset.find_all do |change|
            change.starts_with?(subdir)
          end
          !revision.changeset.empty?
        else
          true
        end
      end
    end

    def load_new_changesets_from_origin
      # NOTE: Git network problems may cause CruiseControl.rb to hang unless you install the system_timer gem.
      # See: https://cruisecontrolrb.lighthouseapp.com/projects/9150-cruise-control-rb/tickets/229-sometimes-git-hangs
      
      MyTimer.timeout(CruiseControl::Configuration.git_load_new_changesets_timeout.to_f) do
        git("fetch", ["origin"])
      end
    rescue Timeout::Error => e
      raise BuilderError.new("Timeout in 'git fetch origin'")
    end

    def git(operation, arguments = [], options = {}, &block)
      command = ["git", operation] + arguments.compact
# TODO: figure out how to handle the same thing with git
#      command << "--non-interactive" unless @interactive

      execute_in_local_copy(command, options, &block)
    end
    
    private
    
    def git_update_submodule
      git("submodule", ["init"])
      git("submodule", ["update"])
    end

  end

end