Do not trigger DuplicatedCode for module attributes

defmodule Credo.Check.Design.DuplicatedCode do
  @moduledoc """
  Code should not be copy-pasted in a codebase when there is room to abstract
  the copied functionality in a meaningful way.

  That said, you should by no means "ABSTRACT ALL THE THINGS!".

  Sometimes it can serve a purpose to have code be explicit in two places, even
  if it means the snippets are nearly identical. A good example for this are
  Database Adapters in a project like Ecto, where you might have nearly
  identical functions for things like `order_by` or `limit` in both the
  Postgres and MySQL adapters.

  In this case, introducing an `AbstractAdapter` just to avoid code duplication
  might cause more trouble down the line than having a bit of duplicated code.

  Like all `Software Design` issues, this is just advice and might not be
  applicable to your project/situation.
  """

  @explanation [
    check: @moduledoc,
    params: [
      mass_threshold: "The minimum mass which a part of code has to have to qualify for this check.",
      nodes_threshold: "The number of nodes that need to be found to raise an issue."
    ]
  ]
  @default_params [
    mass_threshold: 16,
    nodes_threshold: 2,
  ]

  alias Credo.SourceFile
  alias Credo.Issue
  alias Credo.Check.CodeHelper

  use Credo.Check, run_on_all: true, base_priority: :higher

  def run(source_files, params \\ []) when is_list(source_files) do
    mass_threshold = params |> Params.get(:mass_threshold, @default_params)
    nodes_threshold = params |> Params.get(:nodes_threshold, @default_params)

    source_files
    |> duplicate_nodes(mass_threshold)
    |> append_issues_via_issue_service(source_files, nodes_threshold, params)

    :ok
  end

  defp append_issues_via_issue_service(found_hashes, source_files, nodes_threshold, params) when is_map(found_hashes) do
    Enum.each(found_hashes, fn({_hash, nodes}) ->
      filenames = nodes |> Enum.map(&(&1.filename))
      Enum.each(source_files, fn(source_file) ->
        if Enum.member?(filenames, source_file.filename) do
          this_node = Enum.find(nodes, &(&1.filename == source_file.filename))
          other_nodes = List.delete(nodes, this_node)

          issue_meta = IssueMeta.for(source_file, params)
          issue = issue_for(issue_meta, this_node, other_nodes, nodes_threshold)

          if issue do
            Credo.Service.SourceFileIssues.append(source_file, issue)
          end
        end
      end)
    end)
  end

  defp duplicate_nodes(source_files, mass_threshold) do
    source_files
    |> Enum.reduce(%{}, fn(source_file, acc) ->
        calculate_hashes(source_file.ast, acc, source_file.filename, mass_threshold)
      end)
    |> prune_hashes
    |> add_masses
  end

  def add_masses(found_hashes) do
    found_hashes
    |> Enum.map(&add_mass_to_subnode/1)
    |> Enum.into(%{})
  end

  defp add_mass_to_subnode({hash, node_items}) do
    node_items =
      node_items
      |> Enum.map(fn(node_item) ->
          %{node_item | mass: mass(node_item.node)}
         end)

    {hash, node_items}
  end

  @doc """
  Takes a map of hashes to nodes and prunes those nodes that are just
  subnodes of others in the same set.

  Returns the resulting map.
  """
  def prune_hashes(given_hashes) do
    # remove entries containing a single node
    hashes_with_multiple_nodes =
      given_hashes
      |> Enum.filter(fn {_hash, node_items} -> Enum.count(node_items) > 1 end)
      |> Enum.into(%{})

    hashes_to_prune =
      Enum.flat_map(hashes_with_multiple_nodes, &collect_subhashes/1)

    delete_keys(hashes_to_prune, hashes_with_multiple_nodes)
  end

  defp delete_keys([], acc), do: acc
  defp delete_keys([head | tail], acc) do
    delete_keys(tail, Map.delete(acc, head))
  end

  defp collect_subhashes({_hash, node_items}) do
    %{node: first_node, filename: filename} = Enum.at(node_items, 0)

    my_hash = first_node |> CodeHelper.remove_metadata |> to_hash
    subhashes =
      first_node
      |> calculate_hashes(%{}, filename)
      |> Map.keys
      |> List.delete(my_hash) # don't count self

    subhashes
  end

  @doc """
  Calculates hash values for all sub nodes in a given +ast+.

  Returns a map with the hashes as keys and the nodes as values.
  """
  def calculate_hashes(ast, existing_hashes \\ %{}, filename \\ "foo.ex", mass_threshold \\ @default_params[:mass_threshold]) when is_map(existing_hashes) do
    Credo.Code.traverse(ast,
                &collect_hashes(&1, &2, filename, mass_threshold), existing_hashes)
  end

  defp collect_hashes(ast, acc, filename, mass_threshold) do
    if mass(ast) < mass_threshold do
      {ast, acc}
    else
      hash = ast |> CodeHelper.remove_metadata |> to_hash
      node_item = %{node: ast, filename: filename, mass: nil}
      node_items = Map.get(acc, hash, [])
      acc = Map.put(acc, hash, node_items ++ [node_item])
      {ast, acc}
    end
  end

  @doc """
  Returns a hash-value for a given +ast+.
  """
  def to_hash(ast) do
    string =
      ast
      |> Inspect.Algebra.to_doc(%Inspect.Opts{})
      |> Inspect.Algebra.format(80)
      |> Enum.join("")

    :sha256
    |> :crypto.hash(string)
    |> Base.encode16
  end

  @doc """
  Returns the mass (count of instructions) for an AST.
  """
  def mass(ast) do
    Credo.Code.traverse(ast, &calc_mass/2, 0)
  end

  defp calc_mass(ast, acc) when is_tuple(ast) do
    {ast, acc + 1}
  end
  defp calc_mass(ast, acc) do
    {ast, acc}
  end

  defp issue_for(issue_meta, this_node, other_nodes, nodes_threshold) do
    if Enum.count(other_nodes) >= nodes_threshold - 1 do
      filenames =
        other_nodes
        |> Enum.map(fn(other_node) -> "#{other_node.filename}:#{line_no_for(other_node.node)}" end)
        |> Enum.join(", ")
      node_mass = this_node.mass
      line_no = line_no_for(this_node.node)

      if create_issue?(this_node.node) do
        format_issue issue_meta,
          message: "Duplicate code found in #{filenames} (mass: #{node_mass}).",
          line_no: line_no,
          severity: Severity.compute(1 + Enum.count(other_nodes), 1)
      end
    end
  end

  # ignore similar module attributes, no matter how complex
  def create_issue?({:@, _, _}), do: false
  def create_issue?(_ast), do: true

  # TODO: Put in AST helper

  def line_no_for({:__block__, _meta, arguments}) do
    arguments |> line_no_for()
  end
  def line_no_for({:do, arguments}) do
    arguments |> line_no_for()
  end
  def line_no_for({atom, meta, _}) when is_atom(atom) do
    meta[:line]
  end
  def line_no_for(list) when is_list(list) do
    list |> Enum.find_value(&line_no_for/1)
  end
  def line_no_for(nil), do: nil
  def line_no_for(block) do
    block
    |> CodeHelper.do_block_for!
    |> line_no_for
  end
end

defmodule Credo.Check.Design.DuplicatedCode do
  @moduledoc """
  Code should not be copy-pasted in a codebase when there is room to abstract
  the copied functionality in a meaningful way.

  That said, you should by no means "ABSTRACT ALL THE THINGS!".

  Sometimes it can serve a purpose to have code be explicit in two places, even
  if it means the snippets are nearly identical. A good example for this are
  Database Adapters in a project like Ecto, where you might have nearly
  identical functions for things like `order_by` or `limit` in both the
  Postgres and MySQL adapters.

  In this case, introducing an `AbstractAdapter` just to avoid code duplication
  might cause more trouble down the line than having a bit of duplicated code.

  Like all `Software Design` issues, this is just advice and might not be
  applicable to your project/situation.
  """

  @explanation [
    check: @moduledoc,
    params: [
      mass_threshold: "The minimum mass which a part of code has to have to qualify for this check.",
      nodes_threshold: "The number of nodes that need to be found to raise an issue."
    ]
  ]
  @default_params [
    mass_threshold: 16,
    nodes_threshold: 2,
  ]

  alias Credo.SourceFile
  alias Credo.Issue
  alias Credo.Check.CodeHelper

  use Credo.Check, run_on_all: true, base_priority: :higher

  def run(source_files, params \\ []) when is_list(source_files) do
    mass_threshold = params |> Params.get(:mass_threshold, @default_params)
    nodes_threshold = params |> Params.get(:nodes_threshold, @default_params)

    source_files
    |> duplicate_nodes(mass_threshold)
    |> append_issues_via_issue_service(source_files, nodes_threshold, params)

    :ok
  end

  defp append_issues_via_issue_service(found_hashes, source_files, nodes_threshold, params) when is_map(found_hashes) do
    Enum.each(found_hashes, fn({_hash, nodes}) ->
      filenames = nodes |> Enum.map(&(&1.filename))
      Enum.each(source_files, fn(source_file) ->
        if Enum.member?(filenames, source_file.filename) do
          this_node = Enum.find(nodes, &(&1.filename == source_file.filename))
          other_nodes = List.delete(nodes, this_node)

          issue_meta = IssueMeta.for(source_file, params)
          issue = issue_for(issue_meta, this_node, other_nodes, nodes_threshold)

          Credo.Service.SourceFileIssues.append(source_file, issue)
        end
      end)
    end)
  end

  defp duplicate_nodes(source_files, mass_threshold) do
    source_files
    |> Enum.reduce(%{}, fn(source_file, acc) ->
        calculate_hashes(source_file.ast, acc, source_file.filename, mass_threshold)
      end)
    |> prune_hashes
    |> add_masses
  end

  def add_masses(found_hashes) do
    found_hashes
    |> Enum.map(&add_mass_to_subnode/1)
    |> Enum.into(%{})
  end

  defp add_mass_to_subnode({hash, node_items}) do
    node_items =
      node_items
      |> Enum.map(fn(node_item) ->
          %{node_item | mass: mass(node_item.node)}
         end)

    {hash, node_items}
  end

  @doc """
  Takes a map of hashes to nodes and prunes those nodes that are just
  subnodes of others in the same set.

  Returns the resulting map.
  """
  def prune_hashes(given_hashes) do
    # remove entries containing a single node
    hashes_with_multiple_nodes =
      given_hashes
      |> Enum.filter(fn {_hash, node_items} -> Enum.count(node_items) > 1 end)
      |> Enum.into(%{})

    hashes_to_prune =
      Enum.flat_map(hashes_with_multiple_nodes, &collect_subhashes/1)

    delete_keys(hashes_to_prune, hashes_with_multiple_nodes)
  end

  defp delete_keys([], acc), do: acc
  defp delete_keys([head | tail], acc) do
    delete_keys(tail, Map.delete(acc, head))
  end

  defp collect_subhashes({_hash, node_items}) do
    %{node: first_node, filename: filename} = Enum.at(node_items, 0)

    my_hash = first_node |> CodeHelper.remove_metadata |> to_hash
    subhashes =
      first_node
      |> calculate_hashes(%{}, filename)
      |> Map.keys
      |> List.delete(my_hash) # don't count self

    subhashes
  end

  @doc """
  Calculates hash values for all sub nodes in a given +ast+.

  Returns a map with the hashes as keys and the nodes as values.
  """
  def calculate_hashes(ast, existing_hashes \\ %{}, filename \\ "foo.ex", mass_threshold \\ @default_params[:mass_threshold]) when is_map(existing_hashes) do
    Credo.Code.traverse(ast,
                &collect_hashes(&1, &2, filename, mass_threshold), existing_hashes)
  end

  defp collect_hashes(ast, acc, filename, mass_threshold) do
    if mass(ast) < mass_threshold do
      {ast, acc}
    else
      hash = ast |> CodeHelper.remove_metadata |> to_hash
      node_item = %{node: ast, filename: filename, mass: nil}
      node_items = Map.get(acc, hash, [])
      acc = Map.put(acc, hash, node_items ++ [node_item])
      {ast, acc}
    end
  end

  @doc """
  Returns a hash-value for a given +ast+.
  """
  def to_hash(ast) do
    string =
      ast
      |> Inspect.Algebra.to_doc(%Inspect.Opts{})
      |> Inspect.Algebra.format(80)
      |> Enum.join("")

    :sha256
    |> :crypto.hash(string)
    |> Base.encode16
  end

  @doc """
  Returns the mass (count of instructions) for an AST.
  """
  def mass(ast) do
    Credo.Code.traverse(ast, &calc_mass/2, 0)
  end

  defp calc_mass(ast, acc) when is_tuple(ast) do
    {ast, acc + 1}
  end
  defp calc_mass(ast, acc) do
    {ast, acc}
  end

  defp issue_for(issue_meta, this_node, other_nodes, nodes_threshold) do
    if Enum.count(other_nodes) >= nodes_threshold - 1 do
      filenames =
        other_nodes
        |> Enum.map(fn(other_node) -> "#{other_node.filename}:#{line_no_for(other_node.node)}" end)
        |> Enum.join(", ")
      node_mass = this_node.mass
      line_no = line_no_for(this_node.node)

      format_issue issue_meta,
        message: "Duplicate code found in #{filenames} (mass: #{node_mass}).",
        line_no: line_no,
        severity: Severity.compute(1 + Enum.count(other_nodes), 1)
    end
  end

  # TODO: Put in AST helper

  def line_no_for({:__block__, _meta, arguments}) do
    arguments |> line_no_for()
  end
  def line_no_for({:do, arguments}) do
    arguments |> line_no_for()
  end
  def line_no_for({atom, meta, _}) when is_atom(atom) do
    meta[:line]
  end
  def line_no_for(list) when is_list(list) do
    list |> Enum.find_value(&line_no_for/1)
  end
  def line_no_for(nil), do: nil
  def line_no_for(block) do
    block
    |> CodeHelper.do_block_for!
    |> line_no_for
  end
end


Only link checker process when we have to do some work

defmodule Module.ParallelChecker do
  @moduledoc false

  import Kernel, except: [spawn: 3]

  @type cache() :: {pid(), :ets.tid()}
  @type warning() :: term()
  @type kind() :: :def | :defmacro
  @type mode() :: :elixir | :erlang

  @doc """
  Initializes the parallel checker process.
  """
  def start_link(schedulers \\ nil) do
    :gen_server.start_link(__MODULE__, schedulers, [])
  end

  @doc """
  Stops the parallel checker process.
  """
  def stop(checker) do
    send(checker, {__MODULE__, :stop})
    :ok
  end

  @doc """
  Gets the parallel checker data from pdict.
  """
  def get do
    case :erlang.get(:elixir_checker_info) do
      {parent, nil} ->
        {:ok, checker} = start_link()
        put(parent, checker)
        {parent, checker}

      {parent, checker} ->
        {parent, checker}
    end
  end

  @doc """
  Stores the parallel checker information.
  """
  def put(pid, checker) when is_pid(pid) and is_pid(checker) do
    :erlang.put(:elixir_checker_info, {pid, checker})
  end

  @doc """
  Spawns a process that runs the parallel checker.
  """
  def spawn({pid, checker}, module, info) do
    ref = make_ref()

    spawned =
      spawn(fn ->
        mon_ref = Process.monitor(pid)

        receive do
          {^ref, :cache, ets} ->
            Process.link(pid)

            module_map =
              if is_map(info) do
                info
              else
                info |> File.read!() |> fetch_module_map!(module)
              end

            cache_from_module_map(ets, module_map)
            send(checker, {ref, :cached})

            receive do
              {^ref, :check} ->
                # Set the compiler info so we can collect warnings
                :erlang.put(:elixir_compiler_info, {pid, self()})
                warnings = check_module(module_map, {checker, ets})
                send(pid, {__MODULE__, module, warnings})
                send(checker, {__MODULE__, :done})
            end

          {:DOWN, ^mon_ref, _, _, _} ->
            :ok
        end
      end)

    register(checker, spawned, ref)
    :ok
  end

  @doc """
  Verifies the given compilation function
  by starting a checker if one does not exist.
  See `verify/3`.
  """
  def verify(fun) do
    case :erlang.get(:elixir_compiler_info) do
      :undefined ->
        previous = :erlang.put(:elixir_checker_info, {self(), nil})

        try do
          result = fun.()

          case :erlang.get(:elixir_checker_info) do
            {_, nil} -> :ok
            {_, checker} -> verify(checker, [])
          end

          result
        after
          {_, checker} = :erlang.get(:elixir_checker_info)

          if previous != :undefined do
            :erlang.put(:elixir_checker_info, previous)
          else
            :erlang.erase(:elixir_checker_info)
          end

          checker && stop(checker)
        end

      _ ->
        # If we are during compilation, then they will be
        # reported to the compiler, which will validate them.
        fun.()
    end
  end

  @doc """
  Receives pairs of module maps and BEAM binaries. In parallel it verifies
  the modules and adds the ExCk chunk to the binaries. Returns the updated
  list of warnings from the verification.
  """
  @spec verify(pid(), [{module(), binary()}]) :: [warning()]
  def verify(checker, runtime_files) do
    for {module, file} <- runtime_files do
      spawn({self(), checker}, module, file)
    end

    modules = :gen_server.call(checker, :start)
    collect_results(modules, [])
  end

  defp collect_results([], warnings) do
    warnings
  end

  defp collect_results(modules, warnings) do
    receive do
      {:warning, file, location, message} ->
        file = file && Path.absname(file)
        message = :unicode.characters_to_binary(message)
        warning = {file, location, message}
        collect_results(modules, [warning | warnings])

      {__MODULE__, _module, new_warnings} ->
        collect_results(tl(modules), new_warnings ++ warnings)
    end
  end

  @doc """
  Test cache.
  """
  def test_cache do
    {:ok, checker} = start_link()
    {checker, :gen_server.call(checker, :ets)}
  end

  @doc """
  Preloads a module into the cache. Call this function before any other
  cache lookups for the module.
  """
  @spec preload_module(cache(), module()) :: :ok
  def preload_module({server, ets}, module) do
    case :ets.lookup(ets, {:cached, module}) do
      [{_key, _}] -> :ok
      [] -> cache_module({server, ets}, module)
    end
  end

  @doc """
  Returns the export kind and deprecation reason for the given MFA from
  the cache. If the module does not exist return `{:error, :module}`,
  or if the function does not exist return `{:error, :function}`.
  """
  @spec fetch_export(cache(), module(), atom(), arity()) ::
          {:ok, mode(), kind(), binary() | nil} | {:error, :function | :module}
  def fetch_export({_server, ets}, module, fun, arity) do
    case :ets.lookup(ets, {:cached, module}) do
      [{_key, false}] ->
        {:error, :module}

      [{_key, mode}] ->
        case :ets.lookup(ets, {:export, module, {fun, arity}}) do
          [{_key, kind, reason}] -> {:ok, mode, kind, reason}
          [] -> {:error, :function}
        end
    end
  end

  @doc """
  Returns all exported functions and macros for the given module from
  the cache.
  """
  @spec all_exports(cache(), module()) :: [{atom(), arity()}]
  def all_exports({_server, ets}, module) do
    # This is only called after we get a deprecation notice
    # so we can assume it's a cached module
    ets
    |> :ets.match({{:export, module, :"$1"}, :_, :_})
    |> Enum.flat_map(& &1)
    |> Enum.sort()
  end

  ## Module checking

  defp check_module(module_map, cache) do
    %{module: module, file: file, compile_opts: compile_opts, definitions: definitions} =
      module_map

    no_warn_undefined =
      compile_opts
      |> extract_no_warn_undefined()
      |> merge_compiler_no_warn_undefined()

    warnings =
      module
      |> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
      |> group_warnings()
      |> emit_warnings()

    module_map
    |> Map.get(:after_verify, [])
    |> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)

    warnings
  end

  defp extract_no_warn_undefined(compile_opts) do
    for(
      {:no_warn_undefined, values} <- compile_opts,
      value <- List.wrap(values),
      do: value
    )
  end

  defp merge_compiler_no_warn_undefined(no_warn_undefined) do
    case Code.get_compiler_option(:no_warn_undefined) do
      :all -> :all
      list when is_list(list) -> no_warn_undefined ++ list
    end
  end

  ## Warning helpers

  def group_warnings(warnings) do
    warnings
    |> Enum.reduce(%{}, fn {module, warning, location}, acc ->
      locations = MapSet.new([location])
      Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
    end)
    |> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
    |> Enum.sort()
  end

  def emit_warnings(warnings) do
    Enum.flat_map(warnings, fn {module, warning, locations} ->
      message = module.format_warning(warning)
      print_warning([message, ?\n, format_locations(locations)])

      Enum.map(locations, fn {file, line, _mfa} ->
        {file, line, message}
      end)
    end)
  end

  defp format_locations([location]) do
    format_location(location)
  end

  defp format_locations(locations) do
    [
      "Invalid call found at #{length(locations)} locations:\n",
      Enum.map(locations, &format_location/1)
    ]
  end

  defp format_location({file, line, {module, fun, arity}}) do
    mfa = Exception.format_mfa(module, fun, arity)
    [format_file_line(file, line), ": ", mfa, ?\n]
  end

  defp format_location({file, line, nil}) do
    [format_file_line(file, line), ?\n]
  end

  defp format_location({file, line, module}) do
    [format_file_line(file, line), ": ", inspect(module), ?\n]
  end

  defp format_file_line(file, line) do
    file = Path.relative_to_cwd(file)
    line = if line > 0, do: [?: | Integer.to_string(line)], else: []
    ["  ", file, line]
  end

  defp print_warning(message) do
    IO.puts(:stderr, [:elixir_errors.warning_prefix(), message])
  end

  ## Cache

  defp cache_module({server, ets}, module) do
    if lock(server, module) do
      cache_from_chunk(ets, module) || cache_from_info(ets, module)
      unlock(server, module)
    end
  end

  defp cache_from_chunk(ets, module) do
    with {^module, binary, _filename} <- :code.get_object_code(module),
         {:ok, ^module} <- binary |> :beam_lib.info() |> Keyword.fetch(:module),
         {:ok, {_, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
         {:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
      cache_chunk(ets, module, contents.exports)
      true
    else
      _ -> false
    end
  end

  defp cache_from_info(ets, module) do
    if Code.ensure_loaded?(module) do
      {mode, exports} = info_exports(module)
      deprecated = info_deprecated(module)
      cache_info(ets, module, exports, deprecated, mode)
    else
      :ets.insert(ets, {{:cached, module}, false})
    end
  end

  defp info_exports(module) do
    map =
      Map.new(
        [{{:__info__, 1}, :def}] ++
          behaviour_exports(module) ++
          Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
          Enum.map(module.__info__(:functions), &{&1, :def})
      )

    {:elixir, map}
  rescue
    _ -> {:erlang, Map.new(Enum.map(module.module_info(:exports), &{&1, :def}))}
  end

  defp info_deprecated(module) do
    Map.new(module.__info__(:deprecated))
  rescue
    _ -> %{}
  end

  defp fetch_module_map!(binary, module) when is_binary(binary) do
    {:ok, {_, [debug_info: chunk]}} = :beam_lib.chunks(binary, [:debug_info])
    {:debug_info_v1, backend, data} = chunk
    {:ok, module_map} = backend.debug_info(:elixir_v1, module, data, [])
    module_map
  end

  defp cache_from_module_map(ets, map) do
    exports =
      [{{:__info__, 1}, :def}] ++
        behaviour_exports(map) ++
        definitions_to_exports(map.definitions)

    deprecated = Map.new(map.deprecated)
    cache_info(ets, map.module, exports, deprecated, :elixir)
  end

  defp cache_info(ets, module, exports, deprecated, mode) do
    Enum.each(exports, fn {{fun, arity}, kind} ->
      reason = Map.get(deprecated, {fun, arity})
      :ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
      {{fun, arity}, kind}
    end)

    :ets.insert(ets, {{:cached, module}, mode})
  end

  defp cache_chunk(ets, module, exports) do
    Enum.each(exports, fn {{fun, arity}, %{kind: kind, deprecated_reason: reason}} ->
      :ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})

      {{fun, arity}, kind}
    end)

    :ets.insert(ets, {{:export, module, {:__info__, 1}}, :def, nil})
    :ets.insert(ets, {{:cached, module}, :elixir})
  end

  defp behaviour_exports(%{is_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
  defp behaviour_exports(%{is_behaviour: false}), do: []

  defp behaviour_exports(module) when is_atom(module) do
    if function_exported?(module, :behaviour_info, 1) do
      [{{:behaviour_info, 1}, :def}]
    else
      []
    end
  end

  defp definitions_to_exports(definitions) do
    Enum.flat_map(definitions, fn {function, kind, _meta, _clauses} ->
      if kind in [:def, :defmacro] do
        [{function, kind}]
      else
        []
      end
    end)
  end

  defp lock(server, module) do
    :gen_server.call(server, {:lock, module}, :infinity)
  end

  defp unlock(server, module) do
    :gen_server.call(server, {:unlock, module}, :infinity)
  end

  defp register(server, pid, ref) do
    :gen_server.cast(server, {:register, pid, ref})
  end

  ## Server callbacks

  def init(schedulers) do
    ets = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])

    state = %{
      ets: ets,
      waiting: %{},
      modules: [],
      spawned: 0,
      schedulers: schedulers || max(:erlang.system_info(:schedulers_online), 2)
    }

    {:ok, state}
  end

  def handle_call(:start, _from, %{ets: ets, modules: modules} = state) do
    for {pid, ref} <- modules do
      send(pid, {ref, :cache, ets})
    end

    for {_pid, ref} <- modules do
      receive do
        {^ref, :cached} -> :ok
      end
    end

    {:reply, modules, run_checkers(state)}
  end

  def handle_call(:ets, _from, state) do
    {:reply, state.ets, state}
  end

  def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
    case waiting do
      %{^module => froms} ->
        waiting = Map.put(state.waiting, module, [from | froms])
        {:noreply, %{state | waiting: waiting}}

      %{} ->
        waiting = Map.put(state.waiting, module, [])
        {:reply, true, %{state | waiting: waiting}}
    end
  end

  def handle_call({:unlock, module}, _from, %{waiting: waiting} = state) do
    froms = Map.fetch!(waiting, module)
    Enum.each(froms, &:gen_server.reply(&1, false))
    waiting = Map.delete(waiting, module)
    {:reply, :ok, %{state | waiting: waiting}}
  end

  def handle_info({__MODULE__, :done}, state) do
    state = %{state | spawned: state.spawned - 1}
    {:noreply, run_checkers(state)}
  end

  def handle_info({__MODULE__, :stop}, state) do
    {:stop, :normal, state}
  end

  def handle_cast({:register, pid, ref}, %{modules: modules} = state) do
    {:noreply, %{state | modules: [{pid, ref} | modules]}}
  end

  defp run_checkers(%{modules: []} = state) do
    state
  end

  defp run_checkers(%{spawned: spawned, schedulers: schedulers} = state)
       when spawned >= schedulers do
    state
  end

  defp run_checkers(%{modules: [{pid, ref} | modules]} = state) do
    send(pid, {ref, :check})
    run_checkers(%{state | modules: modules, spawned: state.spawned + 1})
  end
end

defmodule Module.ParallelChecker do
  @moduledoc false

  import Kernel, except: [spawn: 3]

  @type cache() :: {pid(), :ets.tid()}
  @type warning() :: term()
  @type kind() :: :def | :defmacro
  @type mode() :: :elixir | :erlang

  @doc """
  Initializes the parallel checker process.
  """
  def start_link(schedulers \\ nil) do
    :gen_server.start_link(__MODULE__, schedulers, [])
  end

  @doc """
  Stops the parallel checker process.
  """
  def stop(checker) do
    send(checker, {__MODULE__, :stop})
    :ok
  end

  @doc """
  Gets the parallel checker data from pdict.
  """
  def get do
    case :erlang.get(:elixir_checker_info) do
      {parent, nil} ->
        {:ok, checker} = start_link()
        put(parent, checker)
        {parent, checker}

      {parent, checker} ->
        {parent, checker}
    end
  end

  @doc """
  Stores the parallel checker information.
  """
  def put(pid, checker) when is_pid(pid) and is_pid(checker) do
    :erlang.put(:elixir_checker_info, {pid, checker})
  end

  @doc """
  Spawns a process that runs the parallel checker.
  """
  def spawn({pid, checker}, module, info) do
    ref = make_ref()

    spawned =
      spawn(fn ->
        Process.link(pid)
        mon_ref = Process.monitor(pid)

        receive do
          {^ref, :cache, ets} ->
            module_map =
              if is_map(info) do
                info
              else
                info |> File.read!() |> fetch_module_map!(module)
              end

            cache_from_module_map(ets, module_map)
            send(checker, {ref, :cached})

            receive do
              {^ref, :check} ->
                # Set the compiler info so we can collect warnings
                :erlang.put(:elixir_compiler_info, {pid, self()})
                warnings = check_module(module_map, {checker, ets})
                send(pid, {__MODULE__, module, warnings})
                send(checker, {__MODULE__, :done})
            end

          {:DOWN, ^mon_ref, _, _, _} ->
            :ok
        end
      end)

    register(checker, spawned, ref)
    :ok
  end

  @doc """
  Verifies the given compilation function
  by starting a checker if one does not exist.
  See `verify/3`.
  """
  def verify(fun) do
    case :erlang.get(:elixir_compiler_info) do
      :undefined ->
        previous = :erlang.put(:elixir_checker_info, {self(), nil})

        try do
          result = fun.()

          case :erlang.get(:elixir_checker_info) do
            {_, nil} -> :ok
            {_, checker} -> verify(checker, [])
          end

          result
        after
          {_, checker} = :erlang.get(:elixir_checker_info)

          if previous != :undefined do
            :erlang.put(:elixir_checker_info, previous)
          else
            :erlang.erase(:elixir_checker_info)
          end

          checker && stop(checker)
        end

      _ ->
        # If we are during compilation, then they will be
        # reported to the compiler, which will validate them.
        fun.()
    end
  end

  @doc """
  Receives pairs of module maps and BEAM binaries. In parallel it verifies
  the modules and adds the ExCk chunk to the binaries. Returns the updated
  list of warnings from the verification.
  """
  @spec verify(pid(), [{module(), binary()}]) :: [warning()]
  def verify(checker, runtime_files) do
    for {module, file} <- runtime_files do
      spawn({self(), checker}, module, file)
    end

    modules = :gen_server.call(checker, :start)
    collect_results(modules, [])
  end

  defp collect_results([], warnings) do
    warnings
  end

  defp collect_results(modules, warnings) do
    receive do
      {:warning, file, location, message} ->
        file = file && Path.absname(file)
        message = :unicode.characters_to_binary(message)
        warning = {file, location, message}
        collect_results(modules, [warning | warnings])

      {__MODULE__, _module, new_warnings} ->
        collect_results(tl(modules), new_warnings ++ warnings)
    end
  end

  @doc """
  Test cache.
  """
  def test_cache do
    {:ok, checker} = start_link()
    {checker, :gen_server.call(checker, :ets)}
  end

  @doc """
  Preloads a module into the cache. Call this function before any other
  cache lookups for the module.
  """
  @spec preload_module(cache(), module()) :: :ok
  def preload_module({server, ets}, module) do
    case :ets.lookup(ets, {:cached, module}) do
      [{_key, _}] -> :ok
      [] -> cache_module({server, ets}, module)
    end
  end

  @doc """
  Returns the export kind and deprecation reason for the given MFA from
  the cache. If the module does not exist return `{:error, :module}`,
  or if the function does not exist return `{:error, :function}`.
  """
  @spec fetch_export(cache(), module(), atom(), arity()) ::
          {:ok, mode(), kind(), binary() | nil} | {:error, :function | :module}
  def fetch_export({_server, ets}, module, fun, arity) do
    case :ets.lookup(ets, {:cached, module}) do
      [{_key, false}] ->
        {:error, :module}

      [{_key, mode}] ->
        case :ets.lookup(ets, {:export, module, {fun, arity}}) do
          [{_key, kind, reason}] -> {:ok, mode, kind, reason}
          [] -> {:error, :function}
        end
    end
  end

  @doc """
  Returns all exported functions and macros for the given module from
  the cache.
  """
  @spec all_exports(cache(), module()) :: [{atom(), arity()}]
  def all_exports({_server, ets}, module) do
    # This is only called after we get a deprecation notice
    # so we can assume it's a cached module
    ets
    |> :ets.match({{:export, module, :"$1"}, :_, :_})
    |> Enum.flat_map(& &1)
    |> Enum.sort()
  end

  ## Module checking

  defp check_module(module_map, cache) do
    %{module: module, file: file, compile_opts: compile_opts, definitions: definitions} =
      module_map

    no_warn_undefined =
      compile_opts
      |> extract_no_warn_undefined()
      |> merge_compiler_no_warn_undefined()

    warnings =
      module
      |> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
      |> group_warnings()
      |> emit_warnings()

    module_map
    |> Map.get(:after_verify, [])
    |> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)

    warnings
  end

  defp extract_no_warn_undefined(compile_opts) do
    for(
      {:no_warn_undefined, values} <- compile_opts,
      value <- List.wrap(values),
      do: value
    )
  end

  defp merge_compiler_no_warn_undefined(no_warn_undefined) do
    case Code.get_compiler_option(:no_warn_undefined) do
      :all -> :all
      list when is_list(list) -> no_warn_undefined ++ list
    end
  end

  ## Warning helpers

  def group_warnings(warnings) do
    warnings
    |> Enum.reduce(%{}, fn {module, warning, location}, acc ->
      locations = MapSet.new([location])
      Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
    end)
    |> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
    |> Enum.sort()
  end

  def emit_warnings(warnings) do
    Enum.flat_map(warnings, fn {module, warning, locations} ->
      message = module.format_warning(warning)
      print_warning([message, ?\n, format_locations(locations)])

      Enum.map(locations, fn {file, line, _mfa} ->
        {file, line, message}
      end)
    end)
  end

  defp format_locations([location]) do
    format_location(location)
  end

  defp format_locations(locations) do
    [
      "Invalid call found at #{length(locations)} locations:\n",
      Enum.map(locations, &format_location/1)
    ]
  end

  defp format_location({file, line, {module, fun, arity}}) do
    mfa = Exception.format_mfa(module, fun, arity)
    [format_file_line(file, line), ": ", mfa, ?\n]
  end

  defp format_location({file, line, nil}) do
    [format_file_line(file, line), ?\n]
  end

  defp format_location({file, line, module}) do
    [format_file_line(file, line), ": ", inspect(module), ?\n]
  end

  defp format_file_line(file, line) do
    file = Path.relative_to_cwd(file)
    line = if line > 0, do: [?: | Integer.to_string(line)], else: []
    ["  ", file, line]
  end

  defp print_warning(message) do
    IO.puts(:stderr, [:elixir_errors.warning_prefix(), message])
  end

  ## Cache

  defp cache_module({server, ets}, module) do
    if lock(server, module) do
      cache_from_chunk(ets, module) || cache_from_info(ets, module)
      unlock(server, module)
    end
  end

  defp cache_from_chunk(ets, module) do
    with {^module, binary, _filename} <- :code.get_object_code(module),
         {:ok, ^module} <- binary |> :beam_lib.info() |> Keyword.fetch(:module),
         {:ok, {_, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
         {:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
      cache_chunk(ets, module, contents.exports)
      true
    else
      _ -> false
    end
  end

  defp cache_from_info(ets, module) do
    if Code.ensure_loaded?(module) do
      {mode, exports} = info_exports(module)
      deprecated = info_deprecated(module)
      cache_info(ets, module, exports, deprecated, mode)
    else
      :ets.insert(ets, {{:cached, module}, false})
    end
  end

  defp info_exports(module) do
    map =
      Map.new(
        [{{:__info__, 1}, :def}] ++
          behaviour_exports(module) ++
          Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
          Enum.map(module.__info__(:functions), &{&1, :def})
      )

    {:elixir, map}
  rescue
    _ -> {:erlang, Map.new(Enum.map(module.module_info(:exports), &{&1, :def}))}
  end

  defp info_deprecated(module) do
    Map.new(module.__info__(:deprecated))
  rescue
    _ -> %{}
  end

  defp fetch_module_map!(binary, module) when is_binary(binary) do
    {:ok, {_, [debug_info: chunk]}} = :beam_lib.chunks(binary, [:debug_info])
    {:debug_info_v1, backend, data} = chunk
    {:ok, module_map} = backend.debug_info(:elixir_v1, module, data, [])
    module_map
  end

  defp cache_from_module_map(ets, map) do
    exports =
      [{{:__info__, 1}, :def}] ++
        behaviour_exports(map) ++
        definitions_to_exports(map.definitions)

    deprecated = Map.new(map.deprecated)
    cache_info(ets, map.module, exports, deprecated, :elixir)
  end

  defp cache_info(ets, module, exports, deprecated, mode) do
    Enum.each(exports, fn {{fun, arity}, kind} ->
      reason = Map.get(deprecated, {fun, arity})
      :ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
      {{fun, arity}, kind}
    end)

    :ets.insert(ets, {{:cached, module}, mode})
  end

  defp cache_chunk(ets, module, exports) do
    Enum.each(exports, fn {{fun, arity}, %{kind: kind, deprecated_reason: reason}} ->
      :ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})

      {{fun, arity}, kind}
    end)

    :ets.insert(ets, {{:export, module, {:__info__, 1}}, :def, nil})
    :ets.insert(ets, {{:cached, module}, :elixir})
  end

  defp behaviour_exports(%{is_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
  defp behaviour_exports(%{is_behaviour: false}), do: []

  defp behaviour_exports(module) when is_atom(module) do
    if function_exported?(module, :behaviour_info, 1) do
      [{{:behaviour_info, 1}, :def}]
    else
      []
    end
  end

  defp definitions_to_exports(definitions) do
    Enum.flat_map(definitions, fn {function, kind, _meta, _clauses} ->
      if kind in [:def, :defmacro] do
        [{function, kind}]
      else
        []
      end
    end)
  end

  defp lock(server, module) do
    :gen_server.call(server, {:lock, module}, :infinity)
  end

  defp unlock(server, module) do
    :gen_server.call(server, {:unlock, module}, :infinity)
  end

  defp register(server, pid, ref) do
    :gen_server.cast(server, {:register, pid, ref})
  end

  ## Server callbacks

  def init(schedulers) do
    ets = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])

    state = %{
      ets: ets,
      waiting: %{},
      modules: [],
      spawned: 0,
      schedulers: schedulers || max(:erlang.system_info(:schedulers_online), 2)
    }

    {:ok, state}
  end

  def handle_call(:start, _from, %{ets: ets, modules: modules} = state) do
    for {pid, ref} <- modules do
      send(pid, {ref, :cache, ets})
    end

    for {_pid, ref} <- modules do
      receive do
        {^ref, :cached} -> :ok
      end
    end

    {:reply, modules, run_checkers(state)}
  end

  def handle_call(:ets, _from, state) do
    {:reply, state.ets, state}
  end

  def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
    case waiting do
      %{^module => froms} ->
        waiting = Map.put(state.waiting, module, [from | froms])
        {:noreply, %{state | waiting: waiting}}

      %{} ->
        waiting = Map.put(state.waiting, module, [])
        {:reply, true, %{state | waiting: waiting}}
    end
  end

  def handle_call({:unlock, module}, _from, %{waiting: waiting} = state) do
    froms = Map.fetch!(waiting, module)
    Enum.each(froms, &:gen_server.reply(&1, false))
    waiting = Map.delete(waiting, module)
    {:reply, :ok, %{state | waiting: waiting}}
  end

  def handle_info({__MODULE__, :done}, state) do
    state = %{state | spawned: state.spawned - 1}
    {:noreply, run_checkers(state)}
  end

  def handle_info({__MODULE__, :stop}, state) do
    {:stop, :normal, state}
  end

  def handle_cast({:register, pid, ref}, %{modules: modules} = state) do
    {:noreply, %{state | modules: [{pid, ref} | modules]}}
  end

  defp run_checkers(%{modules: []} = state) do
    state
  end

  defp run_checkers(%{spawned: spawned, schedulers: schedulers} = state)
       when spawned >= schedulers do
    state
  end

  defp run_checkers(%{modules: [{pid, ref} | modules]} = state) do
    send(pid, {ref, :check})
    run_checkers(%{state | modules: modules, spawned: state.spawned + 1})
  end
end


Update webhook_controller.ex

Update webhook_controller.ex

defmodule BorsNG.WebhookController do
  @moduledoc """
  The webhook controller responds to HTTP requests
  that are initiated from other services (currently, just GitHub).

  For example, I can run `iex -S mix phoenix.server` and do this:

      iex> # Push state to "GitHub"
      iex> alias BorsNG.GitHub
      iex> alias BorsNG.GitHub.ServerMock
      iex> alias BorsNG.Database
      iex> ServerMock.put_state(%{
      ...>   {:installation, 91} => %{ repos: [
      ...>     %GitHub.Repo{
      ...>       id: 14,
      ...>       name: "test/repo",
      ...>       owner: %{
      ...>         id: 6,
      ...>         login: "bors-fanboi",
      ...>         avatar_url: "data:image/svg+xml,<svg></svg>",
      ...>         type: :user
      ...>       }}
      ...>   ] },
      ...>   {{:installation, 91}, 14} => %{
      ...>     branches: %{},
      ...>     comments: %{1 => []},
      ...>     pulls: %{
      ...>       1 => %GitHub.Pr{
      ...>         number: 1,
      ...>         title: "Test",
      ...>         body: "Mess",
      ...>         state: :open,
      ...>         base_ref: "master",
      ...>         head_sha: "00000001",
      ...>         user: %GitHub.User{
      ...>           id: 6,
      ...>           login: "bors-fanboi",
      ...>           avatar_url: "data:image/svg+xml,<svg></svg>"}}},
      ...>     statuses: %{},
      ...>     files: %{}}})
      iex> # The installation now exists; notify bors about it.
      iex> BorsNG.WebhookController.do_webhook(%{
      ...>   body_params: %{
      ...>     "installation" => %{ "id" => 91 },
      ...>     "sender" => %{
      ...>       "id" => 6,
      ...>       "login" => "bors-fanboi",
      ...>       "avatar_url" => "" },
      ...>     "action" => "created" }}, "github", "installation")
      iex> # This starts a background sync process with the installation.
      iex> # Watch it happen in the user interface.
      iex> BorsNG.Worker.SyncerInstallation.wait_hot_spin_xref(91)
      iex> proj = Database.Repo.get_by!(Database.Project, repo_xref: 14)
      iex> proj.name
      "test/repo"
      iex> patch = Database.Repo.get_by!(Database.Patch, pr_xref: 1)
      iex> patch.title
      "Test"
  """

  use BorsNG.Web, :controller

  require Logger

  alias BorsNG.Worker.Attemptor
  alias BorsNG.Worker.Batcher
  alias BorsNG.Worker.BranchDeleter
  alias BorsNG.Command
  alias BorsNG.Database.Installation
  alias BorsNG.Database.Patch
  alias BorsNG.Database.Project
  alias BorsNG.Database.Repo
  alias BorsNG.GitHub
  alias BorsNG.Worker.Syncer
  alias BorsNG.Worker.SyncerInstallation

  @doc """
  This action is reached via `/webhook/:provider`
  """
  def webhook(conn, %{"provider" => "github"}) do
    event = hd(get_req_header(conn, "x-github-event"))
    do_webhook conn, "github", event
    conn
    |> send_resp(200, "")
  end

  def do_webhook(_conn, "github", "ping") do
    :ok
  end

  def do_webhook(conn, "github", "repository"), do: do_webhook_installation_sync(conn)
  def do_webhook(conn, "github", "member"), do: do_webhook_installation_sync(conn)
  def do_webhook(conn, "github", "membership"), do: do_webhook_installation_sync(conn)
  def do_webhook(conn, "github", "team"), do: do_webhook_installation_sync(conn)
  def do_webhook(conn, "github", "organization"), do: do_webhook_installation_sync(conn)

  def do_webhook(conn, "github", "installation") do
    payload = conn.body_params
    installation_xref = payload["installation"]["id"]
    case payload["action"] do
      "deleted" -> Repo.delete_all(from(
        i in Installation,
        where: i.installation_xref == ^installation_xref
      ))
      "created" -> SyncerInstallation.start_synchronize_installation(
        %Installation{
          installation_xref: installation_xref
        }
      )
      _ -> nil
    end
    :ok
  end

  def do_webhook(conn, "github", "installation_repositories") do
    SyncerInstallation.start_synchronize_installation(%Installation{
      installation_xref: conn.body_params["installation"]["id"]
    })
  end

  def do_webhook(conn, "github", "pull_request") do
    repo_xref = conn.body_params["repository"]["id"]
    project = Repo.get_by!(Project, repo_xref: repo_xref)
    pr = BorsNG.GitHub.Pr.from_json!(conn.body_params["pull_request"])
    patch = Syncer.sync_patch(project.id, pr)
    do_webhook_pr(conn, %{
      action: conn.body_params["action"],
      project: project,
      patch: patch,
      author: patch.author,
      pr: pr})
  end

  def do_webhook(conn, "github", "issue_comment") do
    is_created = conn.body_params["action"] == "created"
    is_pr = Map.has_key?(conn.body_params["issue"], "pull_request")
    if is_created and is_pr do
      project = Repo.get_by!(Project,
        repo_xref: conn.body_params["repository"]["id"])
      commenter = conn.body_params["comment"]["user"]
      |> GitHub.User.from_json!()
      |> Syncer.sync_user()
      comment = conn.body_params["comment"]["body"]
      %Command{
        project: project,
        commenter: commenter,
        comment: comment,
        pr_xref: conn.body_params["issue"]["number"]}
      |> Command.run()
    end
  end

  def do_webhook(conn, "github", "pull_request_review_comment") do
    is_created = conn.body_params["action"] == "created"
    if is_created do
      project = Repo.get_by!(Project,
        repo_xref: conn.body_params["repository"]["id"])
      commenter = conn.body_params["comment"]["user"]
      |> GitHub.User.from_json!()
      |> Syncer.sync_user()
      comment = conn.body_params["comment"]["body"]
      pr = GitHub.Pr.from_json!(conn.body_params["pull_request"])
      %Command{
        project: project,
        commenter: commenter,
        comment: comment,
        pr_xref: conn.body_params["pull_request"]["number"],
        pr: pr,
        patch: Syncer.sync_patch(project.id, pr)}
      |> Command.run()
    end
  end

  def do_webhook(conn, "github", "pull_request_review") do
    is_submitted = conn.body_params["action"] == "submitted"
    if is_submitted do
      project = Repo.get_by!(Project,
        repo_xref: conn.body_params["repository"]["id"])
      commenter = conn.body_params["review"]["user"]
      |> GitHub.User.from_json!()
      |> Syncer.sync_user()
      comment = conn.body_params["review"]["body"]
      pr = GitHub.Pr.from_json!(conn.body_params["pull_request"])
      %Command{
        project: project,
        commenter: commenter,
        comment: comment,
        pr_xref: conn.body_params["pull_request"]["number"],
        pr: pr,
        patch: Syncer.sync_patch(project.id, pr)}
      |> Command.run()
    end
  end

  # The check suite is automatically added by GitHub.
  # But don't start until the user writes "r+"
  def do_webhook(conn, "github", "check_suite") do
    repo_xref = conn.body_params["repository"]["id"]
    branch = conn.body_params["check_suite"]["head_branch"]
    project = Repo.get_by!(Project, repo_xref: repo_xref)
    action = conn.body_params["action"]
    staging_branch = project.staging_branch
    trying_branch = project.trying_branch
    case {action, branch} do
      {"completed", ^staging_branch} ->
        batcher = Batcher.Registry.get(project.id)
        Batcher.poll(batcher)
      {"completed", ^trying_branch} ->
        attemptor = Attemptor.Registry.get(project.id)
        Attemptor.poll(attemptor)
      _ -> :ok
    end
  end

  def do_webhook(conn, "github", "check_run") do
    :ok
  end

  def do_webhook(conn, "github", "status") do
    repo_xref = conn.body_params["repository"]["id"]
    commit = conn.body_params["commit"]["sha"]
    identifier = conn.body_params["context"]
    |> GitHub.map_changed_status()
    url = conn.body_params["target_url"]
    state = GitHub.map_state_to_status(conn.body_params["state"])
    project = Repo.get_by!(Project, repo_xref: repo_xref)
    batcher = Batcher.Registry.get(project.id)
    Batcher.status(batcher, {commit, identifier, state, url})
    attemptor = Attemptor.Registry.get(project.id)
    Attemptor.status(attemptor, {commit, identifier, state, url})
  end

  def do_webhook_installation_sync(conn) do
    payload = conn.body_params
    installation_xref = payload["installation"]["id"]
    SyncerInstallation.start_synchronize_installation(
      %Installation{
        installation_xref: installation_xref
      }
    )
  end

  def do_webhook_pr(conn, %{
    action: "opened",
    project: project,
    author: author,
    pr: pr,
    patch: patch
  }) do
    Project.ping!(project.id)
    %{
      "pull_request" => %{
        "body" => body,
        "number" => number,
      },
    } = conn.body_params
    %Command{
      project: project,
      commenter: author,
      comment: body,
      pr_xref: number,
      pr: pr,
      patch: patch}
    |> Command.run()
  end

  def do_webhook_pr(_conn, %{action: "closed", project: project, patch: p}) do
    Project.ping!(project.id)
    Repo.update!(Patch.changeset(p, %{open: false}))
    BranchDeleter.delete(p)
  end

  def do_webhook_pr(conn, %{action: "reopened", project: project, patch: p}) do
    Project.ping!(project.id)
    commit = conn.body_params["pull_request"]["head"]["sha"]
    Repo.update!(Patch.changeset(p, %{open: true, commit: commit}))
  end

  def do_webhook_pr(conn, %{action: "synchronize", project: pro, patch: p}) do
    batcher = Batcher.Registry.get(pro.id)
    Batcher.cancel(batcher, p.id)
    commit = conn.body_params["pull_request"]["head"]["sha"]
    Repo.update!(Patch.changeset(p, %{commit: commit}))
  end

  def do_webhook_pr(conn, %{action: "edited", patch: patch}) do
    %{
      "pull_request" => %{
        "title" => title,
        "body" => body,
        "base" => %{"ref" => base_ref},
      },
    } = conn.body_params
    Repo.update!(Patch.changeset(patch, %{
      title: title,
      body: body,
      into_branch: base_ref}))
  end

  def do_webhook_pr(_conn, %{action: action}) do
    Logger.info(["WebhookController: Got unknown action: ", action])
  end
end

defmodule BorsNG.WebhookController do
  @moduledoc """
  The webhook controller responds to HTTP requests
  that are initiated from other services (currently, just GitHub).

  For example, I can run `iex -S mix phoenix.server` and do this:

      iex> # Push state to "GitHub"
      iex> alias BorsNG.GitHub
      iex> alias BorsNG.GitHub.ServerMock
      iex> alias BorsNG.Database
      iex> ServerMock.put_state(%{
      ...>   {:installation, 91} => %{ repos: [
      ...>     %GitHub.Repo{
      ...>       id: 14,
      ...>       name: "test/repo",
      ...>       owner: %{
      ...>         id: 6,
      ...>         login: "bors-fanboi",
      ...>         avatar_url: "data:image/svg+xml,<svg></svg>",
      ...>         type: :user
      ...>       }}
      ...>   ] },
      ...>   {{:installation, 91}, 14} => %{
      ...>     branches: %{},
      ...>     comments: %{1 => []},
      ...>     pulls: %{
      ...>       1 => %GitHub.Pr{
      ...>         number: 1,
      ...>         title: "Test",
      ...>         body: "Mess",
      ...>         state: :open,
      ...>         base_ref: "master",
      ...>         head_sha: "00000001",
      ...>         user: %GitHub.User{
      ...>           id: 6,
      ...>           login: "bors-fanboi",
      ...>           avatar_url: "data:image/svg+xml,<svg></svg>"}}},
      ...>     statuses: %{},
      ...>     files: %{}}})
      iex> # The installation now exists; notify bors about it.
      iex> BorsNG.WebhookController.do_webhook(%{
      ...>   body_params: %{
      ...>     "installation" => %{ "id" => 91 },
      ...>     "sender" => %{
      ...>       "id" => 6,
      ...>       "login" => "bors-fanboi",
      ...>       "avatar_url" => "" },
      ...>     "action" => "created" }}, "github", "installation")
      iex> # This starts a background sync process with the installation.
      iex> # Watch it happen in the user interface.
      iex> BorsNG.Worker.SyncerInstallation.wait_hot_spin_xref(91)
      iex> proj = Database.Repo.get_by!(Database.Project, repo_xref: 14)
      iex> proj.name
      "test/repo"
      iex> patch = Database.Repo.get_by!(Database.Patch, pr_xref: 1)
      iex> patch.title
      "Test"
  """

  use BorsNG.Web, :controller

  require Logger

  alias BorsNG.Worker.Attemptor
  alias BorsNG.Worker.Batcher
  alias BorsNG.Worker.BranchDeleter
  alias BorsNG.Command
  alias BorsNG.Database.Installation
  alias BorsNG.Database.Patch
  alias BorsNG.Database.Project
  alias BorsNG.Database.Repo
  alias BorsNG.GitHub
  alias BorsNG.Worker.Syncer
  alias BorsNG.Worker.SyncerInstallation

  @doc """
  This action is reached via `/webhook/:provider`
  """
  def webhook(conn, %{"provider" => "github"}) do
    event = hd(get_req_header(conn, "x-github-event"))
    do_webhook conn, "github", event
    conn
    |> send_resp(200, "")
  end

  def do_webhook(_conn, "github", "ping") do
    :ok
  end

  def do_webhook(conn, "github", "repository"), do: do_webhook_installation_sync(conn)
  def do_webhook(conn, "github", "member"), do: do_webhook_installation_sync(conn)
  def do_webhook(conn, "github", "membership"), do: do_webhook_installation_sync(conn)
  def do_webhook(conn, "github", "team"), do: do_webhook_installation_sync(conn)
  def do_webhook(conn, "github", "organization"), do: do_webhook_installation_sync(conn)

  def do_webhook(conn, "github", "installation") do
    payload = conn.body_params
    installation_xref = payload["installation"]["id"]
    case payload["action"] do
      "deleted" -> Repo.delete_all(from(
        i in Installation,
        where: i.installation_xref == ^installation_xref
      ))
      "created" -> SyncerInstallation.start_synchronize_installation(
        %Installation{
          installation_xref: installation_xref
        }
      )
      _ -> nil
    end
    :ok
  end

  def do_webhook(conn, "github", "installation_repositories") do
    SyncerInstallation.start_synchronize_installation(%Installation{
      installation_xref: conn.body_params["installation"]["id"]
    })
  end

  def do_webhook(conn, "github", "pull_request") do
    repo_xref = conn.body_params["repository"]["id"]
    project = Repo.get_by!(Project, repo_xref: repo_xref)
    pr = BorsNG.GitHub.Pr.from_json!(conn.body_params["pull_request"])
    patch = Syncer.sync_patch(project.id, pr)
    do_webhook_pr(conn, %{
      action: conn.body_params["action"],
      project: project,
      patch: patch,
      author: patch.author,
      pr: pr})
  end

  def do_webhook(conn, "github", "issue_comment") do
    is_created = conn.body_params["action"] == "created"
    is_pr = Map.has_key?(conn.body_params["issue"], "pull_request")
    if is_created and is_pr do
      project = Repo.get_by!(Project,
        repo_xref: conn.body_params["repository"]["id"])
      commenter = conn.body_params["comment"]["user"]
      |> GitHub.User.from_json!()
      |> Syncer.sync_user()
      comment = conn.body_params["comment"]["body"]
      %Command{
        project: project,
        commenter: commenter,
        comment: comment,
        pr_xref: conn.body_params["issue"]["number"]}
      |> Command.run()
    end
  end

  def do_webhook(conn, "github", "pull_request_review_comment") do
    is_created = conn.body_params["action"] == "created"
    if is_created do
      project = Repo.get_by!(Project,
        repo_xref: conn.body_params["repository"]["id"])
      commenter = conn.body_params["comment"]["user"]
      |> GitHub.User.from_json!()
      |> Syncer.sync_user()
      comment = conn.body_params["comment"]["body"]
      pr = GitHub.Pr.from_json!(conn.body_params["pull_request"])
      %Command{
        project: project,
        commenter: commenter,
        comment: comment,
        pr_xref: conn.body_params["pull_request"]["number"],
        pr: pr,
        patch: Syncer.sync_patch(project.id, pr)}
      |> Command.run()
    end
  end

  def do_webhook(conn, "github", "pull_request_review") do
    is_submitted = conn.body_params["action"] == "submitted"
    if is_submitted do
      project = Repo.get_by!(Project,
        repo_xref: conn.body_params["repository"]["id"])
      commenter = conn.body_params["review"]["user"]
      |> GitHub.User.from_json!()
      |> Syncer.sync_user()
      comment = conn.body_params["review"]["body"]
      pr = GitHub.Pr.from_json!(conn.body_params["pull_request"])
      %Command{
        project: project,
        commenter: commenter,
        comment: comment,
        pr_xref: conn.body_params["pull_request"]["number"],
        pr: pr,
        patch: Syncer.sync_patch(project.id, pr)}
      |> Command.run()
    end
  end

  # The check suite is automatically added by GitHub.
  # But don't start until the user writes "r+"
  def do_webhook(_conn, "github", "check_suite") do
    repo_xref = conn.body_params["repository"]["id"]
    branch = conn.body_params["check_suite"]["head_branch"]
    project = Repo.get_by!(Project, repo_xref: repo_xref)
    action = conn.body_params["action"]
    staging_branch = project.staging_branch
    trying_branch = project.trying_branch
    case {action, branch} do
      {"completed", ^staging_branch} ->
        batcher = Batcher.Registry.get(project.id)
        Batcher.poll(batcher)
      {"completed", ^trying_branch} ->
        attemptor = Attemptor.Registry.get(project.id)
        Attemptor.poll(attemptor)
      _ -> :ok
    end
  end

  def do_webhook(conn, "github", "check_run") do
    :ok
  end

  def do_webhook(conn, "github", "status") do
    repo_xref = conn.body_params["repository"]["id"]
    commit = conn.body_params["commit"]["sha"]
    identifier = conn.body_params["context"]
    |> GitHub.map_changed_status()
    url = conn.body_params["target_url"]
    state = GitHub.map_state_to_status(conn.body_params["state"])
    project = Repo.get_by!(Project, repo_xref: repo_xref)
    batcher = Batcher.Registry.get(project.id)
    Batcher.status(batcher, {commit, identifier, state, url})
    attemptor = Attemptor.Registry.get(project.id)
    Attemptor.status(attemptor, {commit, identifier, state, url})
  end

  def do_webhook_installation_sync(conn) do
    payload = conn.body_params
    installation_xref = payload["installation"]["id"]
    SyncerInstallation.start_synchronize_installation(
      %Installation{
        installation_xref: installation_xref
      }
    )
  end

  def do_webhook_pr(conn, %{
    action: "opened",
    project: project,
    author: author,
    pr: pr,
    patch: patch
  }) do
    Project.ping!(project.id)
    %{
      "pull_request" => %{
        "body" => body,
        "number" => number,
      },
    } = conn.body_params
    %Command{
      project: project,
      commenter: author,
      comment: body,
      pr_xref: number,
      pr: pr,
      patch: patch}
    |> Command.run()
  end

  def do_webhook_pr(_conn, %{action: "closed", project: project, patch: p}) do
    Project.ping!(project.id)
    Repo.update!(Patch.changeset(p, %{open: false}))
    BranchDeleter.delete(p)
  end

  def do_webhook_pr(conn, %{action: "reopened", project: project, patch: p}) do
    Project.ping!(project.id)
    commit = conn.body_params["pull_request"]["head"]["sha"]
    Repo.update!(Patch.changeset(p, %{open: true, commit: commit}))
  end

  def do_webhook_pr(conn, %{action: "synchronize", project: pro, patch: p}) do
    batcher = Batcher.Registry.get(pro.id)
    Batcher.cancel(batcher, p.id)
    commit = conn.body_params["pull_request"]["head"]["sha"]
    Repo.update!(Patch.changeset(p, %{commit: commit}))
  end

  def do_webhook_pr(conn, %{action: "edited", patch: patch}) do
    %{
      "pull_request" => %{
        "title" => title,
        "body" => body,
        "base" => %{"ref" => base_ref},
      },
    } = conn.body_params
    Repo.update!(Patch.changeset(patch, %{
      title: title,
      body: body,
      into_branch: base_ref}))
  end

  def do_webhook_pr(_conn, %{action: action}) do
    Logger.info(["WebhookController: Got unknown action: ", action])
  end
end


Bump version to 1.0.2

Bump version to 1.0.2


Add comments to fallback controller, closes #3835

defmodule <%= inspect context.web_module %>.FallbackController do
  @moduledoc """
  Translates controller action results into valid `Plug.Conn` responses.

  See `Phoenix.Controller.action_fallback/1` for more details.
  """
  use <%= inspect context.web_module %>, :controller

  <%= if schema.generate? do %># This clause handles errors returned by Ecto's insert/update/delete.
  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> put_view(<%= inspect context.web_module %>.ChangesetView)
    |> render("error.json", changeset: changeset)
  end

  <% end %># This clause is an example of how to handle resources that cannot be found.
  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> put_view(<%= inspect context.web_module %>.ErrorView)
    |> render(:"404")
  end
end

defmodule <%= inspect context.web_module %>.FallbackController do
  @moduledoc """
  Translates controller action results into valid `Plug.Conn` responses.

  See `Phoenix.Controller.action_fallback/1` for more details.
  """
  use <%= inspect context.web_module %>, :controller

  <%= if schema.generate? do %>def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> put_view(<%= inspect context.web_module %>.ChangesetView)
    |> render("error.json", changeset: changeset)
  end

  <% end %>def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> put_view(<%= inspect context.web_module %>.ErrorView)
    |> render(:"404")
  end
end