module Gitlab
  module Satellite
    # GitLab server-side merge
    class MergeAction < Action
      attr_accessor :merge_request

      def initialize(user, merge_request)
        super user, merge_request.target_project
        @merge_request = merge_request
      end

      # Checks if a merge request can be executed without user interaction
      def can_be_merged?
        in_locked_and_timed_satellite do |merge_repo|
          prepare_satellite!(merge_repo)
          merge_in_satellite!(merge_repo)
        end
      end

      # Merges the source branch into the target branch in the satellite and
      # pushes it back to the repository.
      # It also removes the source branch if requested in the merge request (and this is permitted by the merge request).
      #
      # Returns false if the merge produced conflicts
      # Returns false if pushing from the satellite to the repository failed or was rejected
      # Returns true otherwise
      def merge!
        in_locked_and_timed_satellite do |merge_repo|
          prepare_satellite!(merge_repo)
          if merge_in_satellite!(merge_repo)
            # push merge back to Gitolite
            # will raise CommandFailed when push fails
            merge_repo.git.push(default_options, :origin, merge_request.target_branch)
            # remove source branch
            if merge_request.should_remove_source_branch && !project.root_ref?(merge_request.source_branch)
              # will raise CommandFailed when push fails
              merge_repo.git.push(default_options, :origin, ":#{merge_request.source_branch}")
            end
            # merge, push and branch removal successful
            true
          end
        end
      rescue Grit::Git::CommandFailed => ex
        Gitlab::GitLogger.error(ex.message)
        false
      end


      # Get a raw diff of the source to the target
      def diff_in_satellite
        in_locked_and_timed_satellite do |merge_repo|
          prepare_satellite!(merge_repo)

          update_satellite_source_and_target!(merge_repo)
          if merge_request.for_fork?
            diff = merge_repo.git.native(:diff, default_options, "origin/#{merge_request.target_branch}", "source/#{merge_request.source_branch}")
          else
            diff = merge_repo.git.native(:diff, default_options, "#{merge_request.target_branch}", "#{merge_request.source_branch}")

          end
          return diff
        end
      rescue Grit::Git::CommandFailed => ex
        Gitlab::GitLogger.error(ex.message)
        false
      end

      # Only show what is new in the source branch compared to the target branch, not the other way around.
      # The line below with merge_base is equivalent to diff with three dots (git diff branch1...branch2)
      # From the git documentation: "git diff A...B" is equivalent to "git diff $(git-merge-base A B) B"
      def diffs_between_satellite
        in_locked_and_timed_satellite do |merge_repo|
          prepare_satellite!(merge_repo)
          update_satellite_source_and_target!(merge_repo)
          if merge_request.for_fork?
            common_commit = merge_repo.git.native(:merge_base, default_options, ["origin/#{merge_request.target_branch}", "source/#{merge_request.source_branch}"]).strip
            diffs = merge_repo.diff(default_options, common_commit, "source/#{merge_request.source_branch}")
          else
            common_commit = merge_repo.git.native(:merge_base, default_options, ["#{merge_request.target_branch}", "#{merge_request.source_branch}"]).strip
            diffs = merge_repo.diff(default_options, common_commit, "#{merge_request.source_branch}")
          end
          return diffs
        end
      rescue Grit::Git::CommandFailed => ex
        Gitlab::GitLogger.error(ex.message)
        false
      end

      # Get commit as an email patch
      def format_patch
        in_locked_and_timed_satellite do |merge_repo|
          prepare_satellite!(merge_repo)
          update_satellite_source_and_target!(merge_repo)
          if (merge_request.for_fork?)
            patch = merge_repo.git.format_patch(default_options({stdout: true}), "origin/#{merge_request.target_branch}...source/#{merge_request.source_branch}")
          else
            patch = merge_repo.git.format_patch(default_options({stdout: true}), "#{merge_request.target_branch}...#{merge_request.source_branch}")
          end
          return patch
        end
      rescue Grit::Git::CommandFailed => ex
        Gitlab::GitLogger.error(ex.message)
        false
      end

      # Retrieve an array of commits between the source and the target
      def commits_between
        in_locked_and_timed_satellite do |merge_repo|
          prepare_satellite!(merge_repo)
          update_satellite_source_and_target!(merge_repo)
          if (merge_request.for_fork?)
            commits = merge_repo.commits_between("origin/#{merge_request.target_branch}", "source/#{merge_request.source_branch}")
          else
            commits = merge_repo.commits_between("#{merge_request.target_branch}", "#{merge_request.source_branch}")
          end
          return commits

        end
      rescue Grit::Git::CommandFailed => ex
        Gitlab::GitLogger.error(ex.message)
        false
      end

      private

      # Merges the source_branch into the target_branch in the satellite.
      #
      # Note: it will clear out the satellite before doing anything
      #
      # Returns false if the merge produced conflicts
      # Returns true otherwise
      def merge_in_satellite!(repo)
        update_satellite_source_and_target!(repo)

        # merge the source branch into the satellite
        # will raise CommandFailed when merge fails
        if merge_request.for_fork?
          repo.git.pull(default_options({no_ff: true}), 'source', merge_request.source_branch)
        else
          repo.git.pull(default_options({no_ff: true}), 'origin', merge_request.source_branch)
        end
      rescue Grit::Git::CommandFailed => ex
        Gitlab::GitLogger.error(ex.message)
        false
      end

      # Assumes a satellite exists that is a fresh clone of the projects repo, prepares satellite for merges, diffs etc
      def update_satellite_source_and_target!(repo)
        if  merge_request.for_fork?
          repo.remote_add('source', merge_request.source_project.repository.path_to_repo)
          repo.remote_fetch('source')
          repo.git.checkout(default_options({b: true}), merge_request.target_branch, "origin/#{merge_request.target_branch}")
        else
          # We can't trust the input here being branch names, we can't always check it out because it could be a relative ref i.e. HEAD~3
          # we could actually remove the if true, because it should never ever happen (as long as the satellite has been prepared)
          repo.git.checkout(default_options, "#{merge_request.source_branch}")
          repo.git.checkout(default_options, "#{merge_request.target_branch}")
        end
      rescue Grit::Git::CommandFailed => ex
        Gitlab::GitLogger.error(ex.message)
        false
      end


    end
  end
end
