lib/rubycritic/source_control_systems/git.rb



# frozen_string_literal: true

require 'tty/which'
require 'rubycritic/source_control_systems/git/churn'

module RubyCritic
  module SourceControlSystem
    class Git < Base
      register_system
      GIT_EXECUTABLE = TTY::Which.which('git')

      def self.git(arg)
        if Gem.win_platform?
          `\"#{GIT_EXECUTABLE}\" #{arg}`
        else
          `#{GIT_EXECUTABLE} #{arg}`
        end
      end

      def git(arg)
        self.class.git(arg)
      end

      # :reek:DuplicateMethodCall
      # :reek:NilCheck
      def self.supported?
        return true if git('branch 2>&1') && $CHILD_STATUS.success?

        return false if Config.paths.nil? || Config.paths.empty?

        Config.paths.any? do |path|
          absolute_path = File.expand_path(path)
          check_git_repository?(absolute_path)
        end
      end

      # :reek:DuplicateMethodCall
      def self.check_git_repository?(path)
        current_path = File.expand_path(path)
        while current_path != File.dirname(current_path)
          return true if Dir.exist?(File.join(current_path, '.git'))

          current_path = File.dirname(current_path)
        end
        false
      end

      def self.to_s
        'Git'
      end

      def churn
        @churn ||= Churn.new(churn_after: Config.churn_after, paths: Config.paths)
      end

      def revisions_count(path)
        churn.revisions_count(path)
      end

      def date_of_last_commit(path)
        churn.date_of_last_commit(path)
      end

      def revision?
        head_reference && $CHILD_STATUS.success?
      end

      def head_reference
        git('rev-parse --verify HEAD').chomp!
      end

      def travel_to_head
        stash_successful = stash_changes?
        yield
      ensure
        travel_to_original_state if stash_successful
      end

      def self.switch_branch(branch)
        dirty = !uncommitted_changes.empty?
        abort("Uncommitted changes are present: #{uncommitted_changes}") if dirty

        git("checkout #{branch}")
      end

      def self.uncommitted_changes
        return @uncommitted_changes if defined? @uncommitted_changes

        @uncommitted_changes = git('diff-index HEAD --').chomp! || ''
      end

      def self.modified_files
        modified_files = `git diff --name-status #{Config.base_branch} #{Config.feature_branch}`
        modified_files.split("\n").filter_map do |line|
          next if line.start_with?('D')

          file_name = line.split("\t")[1]
          file_name
        end
      end

      def self.current_branch
        branch_list = `git branch`
        branch = branch_list.match(/\*.*$/)[0].gsub('* ', '')
        branch = branch.gsub(/\(HEAD detached at (.*)\)$/, '\1') if /\(HEAD detached at (.*)\)$/.match?(branch)
        branch
      end

      private

      def stash_changes?
        stashes_count_before = stashes_count
        git('stash')
        stashes_count_after = stashes_count
        stashes_count_after > stashes_count_before
      end

      def stashes_count
        git('stash list --format=%h').count("\n")
      end

      def travel_to_original_state
        git('stash pop')
      end
    end
  end
end