accept general apply edit

accept general apply edit

defmodule Admin.EventsChannel do
  use Admin, :channel
  alias Admin.{Webhooks}
  use Guardian.Channel
  alias Admin.{CheckoutAgent}
  import Guardian.Phoenix.Socket

  @attrs ~w(
    id start_date end_date featured_image_url location summary title name
    type status description contact type tags instructions attendances
    rsvp_download_url browser_url organizer_edit_url
  )a

  @instance Application.get_env(:admin, :instance, "jd")

  def join("events", %{"organizer_token" => token}, socket) do
    case token |> URI.encode_www_form() |> Cipher.decrypt() do
      {:error, message} -> {:error, message}
      organizer_id -> {:ok, assign(socket, :organizer_id, organizer_id)}
    end
  end

  def join("events", _payload, socket) do
    {:ok, socket}
  end

  # Starting sending events
  def handle_in("ready", %{"page" => "esm"}, socket) do
    send_esm_events(socket)
    {:noreply, socket}
  end

  def handle_in("ready", %{"page" => "list"}, socket) do
    send_list_events(socket)
    {:noreply, socket}
  end

  def handle_in("ready", %{"page" => "my-events"}, socket) do
    send_my_events(socket)
    {:noreply, socket}
  end

  # Relay checking out
  def handle_in("checkout-" <> id, _, socket) do
    actor = current_resource(socket)
    CheckoutAgent.register(id, actor)
    broadcast(socket, "checkout", %{id: id, actor: actor})
    {:noreply, socket}
  end

  # Relay checking in
  def handle_in("checkin-" <> id, _, socket) do
    CheckoutAgent.free(id)
    broadcast(socket, "checkin", %{id: id})
    {:noreply, socket}
  end

  # Handle two attribute edit
  def handle_in("edit-" <> id, [["start_date", start_date], ["end_date", end_date]], socket) do
    edits = Map.new([{"start_date", start_date}, {"end_date", end_date}])
    insert_edit(%{event_id: id, edit: edits, actor: current_resource(socket)})

    new_event = apply_edit(id, edits)

    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  # Implement simple edit, contact edit (embeded), or location edit (associative)
  def handle_in("edit-" <> id, [key, value], socket) do
    new_event =
      case key do
        "location." <> _rest -> apply_location_edit(id, [key, value])
        "contact." <> _rest -> apply_contact_edit(id, [key, value])
        _ -> apply_edit(id, [key, value])
      end

    insert_edit(%{event_id: id, edit: Map.new([{key, value}]), actor: current_resource(socket)})

    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  # Implement standard tags change
  def handle_in("tags-" <> id, tags, socket) do
    insert_edit(%{event_id: id, edit: Map.new([{"tags", tags}]), actor: current_resource(socket)})

    %{body: event} = Proxy.get("events/#{id}")
    calendar_tags = Enum.filter(event.tags, &String.contains?(&1, "Calendar: "))

    new_tags = Enum.concat(tags, calendar_tags)
    new_event = edit_tags_and_fetch(id, new_tags)

    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  def handle_in("calendars-" <> id, calendars, socket) do
    insert_edit(%{
      event_id: id,
      edit: Map.new([{"calendars", calendars}]),
      actor: current_resource(socket)
    })

    %{body: event} = Proxy.get("events/#{id}")

    as_tags = Enum.map(calendars, &"Calendar: #{&1}")
    regular_tags = Enum.reject(event.tags, &String.contains?(&1, "Calendar: "))

    new_tags = Enum.concat(regular_tags, as_tags)
    new_event = edit_tags_and_fetch(id, new_tags)

    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  # Handle status changes
  def handle_in("action-" <> id, payload = %{"status" => status}, socket) do
    insert_edit(%{event_id: id, edit: %{"status" => status}, actor: current_resource(socket)})
    new_event = set_status(id, status)

    Webhooks.on(status, %{
      event: new_event,
      team_member: current_resource(socket),
      reason: payload["message"]
    })

    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  # Handle action registers
  def handle_in("action-" <> id, %{"action" => action}, socket) do
    insert_edit(%{event_id: id, edit: %{"action" => action}, actor: current_resource(socket)})
    new_event = mark_action(id, action)
    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  def handle_in("duplicate-" <> id, _payload, socket) do
    new_event = %{id: new_id} = duplicate(id)
    push(socket, "event", %{id: new_id, event: new_event})
    broadcast(socket, "event", %{id: new_id, event: new_event})
    {:noreply, socket}
  end

  defp send_esm_events(socket) do
    Proxy.stream("events")
    |> Flow.from_enumerable()
    |> Flow.map(&async_rsvp_count_fetch/1)
    |> Flow.map(fn task -> Task.await(task, 60_000) end)
    |> Flow.map(&event_pipeline/1)
    |> Flow.each(fn event ->
         id = event.identifiers |> List.first() |> String.split(":") |> List.last()
         push(socket, "event", %{id: id, event: event})
       end)
    |> Flow.run()
  end

  defp send_list_events(socket) do
    Proxy.stream("events")
    |> Flow.from_enumerable()
    |> Flow.filter(&(&1.status == "confirmed" and &1.end_date > DateTime.utc_now()))
    |> Flow.map(&async_rsvp_count_fetch/1)
    |> Flow.map(&Task.await/1)
    |> Flow.map(&event_pipeline/1)
    |> Flow.each(fn event ->
         id = event.identifiers |> List.first() |> String.split(":") |> List.last()
         push(socket, "event", %{id: event.id, event: event})
       end)
    |> Flow.run()
  end

  defp send_my_events(socket = %{assigns: %{organizer_id: organizer_id}}) do
    Proxy.stream("events")
    |> Flow.from_enumerable()
    |> Flow.filter(&(&1.organizer_id == organizer_id))
    |> Flow.map(&async_rsvp_count_fetch/1)
    |> Flow.map(&Task.await/1)
    |> Flow.map(&event_pipeline/1)
    |> Flow.each(fn event ->
         id = event.identifiers |> List.first() |> String.split(":") |> List.last()
         push(socket, "event", %{id: id, event: event})
       end)
    |> Flow.run()
  end

  defp event_pipeline(event) do
    event
    |> add_rsvp_download_url()
    |> add_organizer_edit_url()
  end

  defp to_map(event = %{tags: tags}) do
    event
    |> Map.from_struct()
    |> Map.take(@attrs)
    |> Map.put(:tags, tags |> Enum.map(& &1.name))
  end

  defp add_rsvp_download_url(event) do
    id = event.identifiers |> List.first() |> String.split(":") |> List.last()
    encrypted_id = Cipher.encrypt(id)

    Map.put(
      event,
      :rsvp_download_url,
      "https://admin.justicedemocrats.com/rsvps/#{encrypted_id}"
    )
  end

  defp add_organizer_edit_url(event) do
    organizer_edit_hash = Cipher.encrypt("#{event.organizer_id}")

    Map.put(
      event,
      :organizer_edit_url,
      "https://admin.justicedemocrats.com/my-events/#{organizer_edit_hash}"
    )
  end

  defp apply_edit(id, [key, value]) do
    change = Map.put(%{}, key, value)
    Proxy.post("events/#{id}", body: change)
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  defp apply_edit(id, change) when is_map(change) do
    Proxy.post("events/#{id}", body: change)
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  defp apply_contact_edit(id, [raw_key, value]) do
    "contact." <> key = raw_key
    contact_change = Map.put(%{}, key, value)
    Proxy.post("events/#{id}", body: %{contact: contact_change})
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  defp apply_location_edit(id, [raw_key, raw_value]) do
    {key, value} =
      case raw_key do
        "location.address_lines[0]" -> {:address_lines, [raw_value]}
        "location." <> key -> {key, raw_value}
      end

    location_change = Map.put(%{}, key, value)
    Proxy.post("events/#{id}", body: %{location: location_change})
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  def edit_tags_and_fetch(id, tags) do
    Proxy.post("events/#{id}", body: %{tags: tags})
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  defp set_status(id, status) do
    Proxy.post("events/#{id}", body: %{status: status})
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  defp mark_action(id, action) do
    %{body: %{tags: current_tags}} = Proxy.get("events/#{id}")
    tag = "Event: Action: #{String.capitalize(action)}"
    new_tags = Enum.concat(current_tags, [tag])
    Proxy.post("events/#{id}", body: %{tags: new_tags})
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  defp duplicate(id) do
    %{body: old} = Proxy.get("events/#{id}")

    to_create =
      old
      |> Map.put(:start_date, old.start_date |> Timex.shift(days: 7))
      |> Map.put(:end_date, old.end_date |> Timex.shift(days: 7))
      |> Map.put(:status, "tentative")
      |> Map.drop([:identifiers])

    new = Proxy.post("event", body: to_create)
  end

  defp insert_edit(%{event_id: event_id, edit: edit, actor: actor}) do
    Mongo.insert_one(:mongo, "esm_actions_#{@instance}", %{
      event_id: event_id,
      edit: edit,
      actor: actor,
      edited_at: Timex.now()
    })

    case edit do
      %{"status" => _} -> nil
      %{"tags" => _} -> nil
      %{"calendar" => _} -> nil
      %{"action" => _} -> nil
      _ -> Admin.EditAgent.record_edit(event_id)
    end
  end

  defp async_rsvp_count_fetch(event) do
    Task.async(fn ->
      try do
        id = event.identifiers |> List.first() |> String.split(":") |> List.last()
        %{body: %{count: num_rsvps}} = Proxy.get("events/#{id}/rsvp-count")
        Map.put(event, :attendance_count, num_rsvps)
      rescue
        _e -> Map.put(event, :attendance_count, 0)
      end
    end)
  end
end

defmodule Admin.EventsChannel do
  use Admin, :channel
  alias Admin.{Webhooks}
  use Guardian.Channel
  alias Admin.{CheckoutAgent}
  import Guardian.Phoenix.Socket

  @attrs ~w(
    id start_date end_date featured_image_url location summary title name
    type status description contact type tags instructions attendances
    rsvp_download_url browser_url organizer_edit_url
  )a

  @instance Application.get_env(:admin, :instance, "jd")

  def join("events", %{"organizer_token" => token}, socket) do
    case token |> URI.encode_www_form() |> Cipher.decrypt() do
      {:error, message} -> {:error, message}
      organizer_id -> {:ok, assign(socket, :organizer_id, organizer_id)}
    end
  end

  def join("events", _payload, socket) do
    {:ok, socket}
  end

  # Starting sending events
  def handle_in("ready", %{"page" => "esm"}, socket) do
    send_esm_events(socket)
    {:noreply, socket}
  end

  def handle_in("ready", %{"page" => "list"}, socket) do
    send_list_events(socket)
    {:noreply, socket}
  end

  def handle_in("ready", %{"page" => "my-events"}, socket) do
    send_my_events(socket)
    {:noreply, socket}
  end

  # Relay checking out
  def handle_in("checkout-" <> id, _, socket) do
    actor = current_resource(socket)
    CheckoutAgent.register(id, actor)
    broadcast(socket, "checkout", %{id: id, actor: actor})
    {:noreply, socket}
  end

  # Relay checking in
  def handle_in("checkin-" <> id, _, socket) do
    CheckoutAgent.free(id)
    broadcast(socket, "checkin", %{id: id})
    {:noreply, socket}
  end

  # Handle two attribute edit
  def handle_in("edit-" <> id, [["start_date", start_date], ["end_date", end_date]], socket) do
    edits = Map.new([{"start_date", start_date}, {"end_date", end_date}])
    insert_edit(%{event_id: id, edit: edits, actor: current_resource(socket)})

    new_event = apply_edit(id, edits)

    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  # Implement simple edit, contact edit (embeded), or location edit (associative)
  def handle_in("edit-" <> id, [key, value], socket) do
    new_event =
      case key do
        "location." <> _rest -> apply_location_edit(id, [key, value])
        "contact." <> _rest -> apply_contact_edit(id, [key, value])
        _ -> apply_edit(id, [key, value])
      end

    insert_edit(%{event_id: id, edit: Map.new([{key, value}]), actor: current_resource(socket)})

    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  # Implement standard tags change
  def handle_in("tags-" <> id, tags, socket) do
    insert_edit(%{event_id: id, edit: Map.new([{"tags", tags}]), actor: current_resource(socket)})

    %{body: event} = Proxy.get("events/#{id}")
    calendar_tags = Enum.filter(event.tags, &String.contains?(&1, "Calendar: "))

    new_tags = Enum.concat(tags, calendar_tags)
    new_event = edit_tags_and_fetch(id, new_tags)

    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  def handle_in("calendars-" <> id, calendars, socket) do
    insert_edit(%{
      event_id: id,
      edit: Map.new([{"calendars", calendars}]),
      actor: current_resource(socket)
    })

    %{body: event} = Proxy.get("events/#{id}")

    as_tags = Enum.map(calendars, &"Calendar: #{&1}")
    regular_tags = Enum.reject(event.tags, &String.contains?(&1, "Calendar: "))

    new_tags = Enum.concat(regular_tags, as_tags)
    new_event = edit_tags_and_fetch(id, new_tags)

    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  # Handle status changes
  def handle_in("action-" <> id, payload = %{"status" => status}, socket) do
    insert_edit(%{event_id: id, edit: %{"status" => status}, actor: current_resource(socket)})
    new_event = set_status(id, status)

    Webhooks.on(status, %{
      event: new_event,
      team_member: current_resource(socket),
      reason: payload["message"]
    })

    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  # Handle action registers
  def handle_in("action-" <> id, %{"action" => action}, socket) do
    insert_edit(%{event_id: id, edit: %{"action" => action}, actor: current_resource(socket)})
    new_event = mark_action(id, action)
    push(socket, "event", %{id: id, event: new_event})
    broadcast(socket, "event", %{id: id, event: new_event})
    {:noreply, socket}
  end

  def handle_in("duplicate-" <> id, _payload, socket) do
    new_event = %{id: new_id} = duplicate(id)
    push(socket, "event", %{id: new_id, event: new_event})
    broadcast(socket, "event", %{id: new_id, event: new_event})
    {:noreply, socket}
  end

  defp send_esm_events(socket) do
    Proxy.stream("events")
    |> Flow.from_enumerable()
    |> Flow.map(&async_rsvp_count_fetch/1)
    |> Flow.map(fn task -> Task.await(task, 60_000) end)
    |> Flow.map(&event_pipeline/1)
    |> Flow.each(fn event ->
         id = event.identifiers |> List.first() |> String.split(":") |> List.last()
         push(socket, "event", %{id: id, event: event})
       end)
    |> Flow.run()
  end

  defp send_list_events(socket) do
    Proxy.stream("events")
    |> Flow.from_enumerable()
    |> Flow.filter(&(&1.status == "confirmed" and &1.end_date > DateTime.utc_now()))
    |> Flow.map(&async_rsvp_count_fetch/1)
    |> Flow.map(&Task.await/1)
    |> Flow.map(&event_pipeline/1)
    |> Flow.each(fn event ->
         id = event.identifiers |> List.first() |> String.split(":") |> List.last()
         push(socket, "event", %{id: event.id, event: event})
       end)
    |> Flow.run()
  end

  defp send_my_events(socket = %{assigns: %{organizer_id: organizer_id}}) do
    Proxy.stream("events")
    |> Flow.from_enumerable()
    |> Flow.filter(&(&1.organizer_id == organizer_id))
    |> Flow.map(&async_rsvp_count_fetch/1)
    |> Flow.map(&Task.await/1)
    |> Flow.map(&event_pipeline/1)
    |> Flow.each(fn event ->
         id = event.identifiers |> List.first() |> String.split(":") |> List.last()
         push(socket, "event", %{id: id, event: event})
       end)
    |> Flow.run()
  end

  defp event_pipeline(event) do
    event
    |> add_rsvp_download_url()
    |> add_organizer_edit_url()
  end

  defp to_map(event = %{tags: tags}) do
    event
    |> Map.from_struct()
    |> Map.take(@attrs)
    |> Map.put(:tags, tags |> Enum.map(& &1.name))
  end

  defp add_rsvp_download_url(event) do
    id = event.identifiers |> List.first() |> String.split(":") |> List.last()
    encrypted_id = Cipher.encrypt(id)

    Map.put(
      event,
      :rsvp_download_url,
      "https://admin.justicedemocrats.com/rsvps/#{encrypted_id}"
    )
  end

  defp add_organizer_edit_url(event) do
    organizer_edit_hash = Cipher.encrypt("#{event.organizer_id}")

    Map.put(
      event,
      :organizer_edit_url,
      "https://admin.justicedemocrats.com/my-events/#{organizer_edit_hash}"
    )
  end

  defp apply_edit(id, [key, value]) do
    change = Map.put(%{}, key, value)
    Proxy.post("events/#{id}", body: change)
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  defp apply_contact_edit(id, [raw_key, value]) do
    "contact." <> key = raw_key
    contact_change = Map.put(%{}, key, value)
    Proxy.post("events/#{id}", body: %{contact: contact_change})
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  defp apply_location_edit(id, [raw_key, raw_value]) do
    {key, value} =
      case raw_key do
        "location.address_lines[0]" -> {:address_lines, [raw_value]}
        "location." <> key -> {key, raw_value}
      end

    location_change = Map.put(%{}, key, value)
    Proxy.post("events/#{id}", body: %{location: location_change})
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  def edit_tags_and_fetch(id, tags) do
    Proxy.post("events/#{id}", body: %{tags: tags})
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  defp set_status(id, status) do
    Proxy.post("events/#{id}", body: %{status: status})
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  defp mark_action(id, action) do
    %{body: %{tags: current_tags}} = Proxy.get("events/#{id}")
    tag = "Event: Action: #{String.capitalize(action)}"
    new_tags = Enum.concat(current_tags, [tag])
    Proxy.post("events/#{id}", body: %{tags: new_tags})
    %{body: event} = Proxy.get("events/#{id}")
    event_pipeline(event)
  end

  defp duplicate(id) do
    %{body: old} = Proxy.get("events/#{id}")

    to_create =
      old
      |> Map.put(:start_date, old.start_date |> Timex.shift(days: 7))
      |> Map.put(:end_date, old.end_date |> Timex.shift(days: 7))
      |> Map.put(:status, "tentative")
      |> Map.drop([:identifiers])

    new = Proxy.post("event", body: to_create)
  end

  defp insert_edit(%{event_id: event_id, edit: edit, actor: actor}) do
    Mongo.insert_one(:mongo, "esm_actions_#{@instance}", %{
      event_id: event_id,
      edit: edit,
      actor: actor,
      edited_at: Timex.now()
    })

    case edit do
      %{"status" => _} -> nil
      %{"tags" => _} -> nil
      %{"calendar" => _} -> nil
      %{"action" => _} -> nil
      _ -> Admin.EditAgent.record_edit(event_id)
    end
  end

  defp async_rsvp_count_fetch(event) do
    Task.async(fn ->
      try do
        id = event.identifiers |> List.first() |> String.split(":") |> List.last()
        %{body: %{count: num_rsvps}} = Proxy.get("events/#{id}/rsvp-count")
        Map.put(event, :attendance_count, num_rsvps)
      rescue
        _e -> Map.put(event, :attendance_count, 0)
      end
    end)
  end
end


fix initial definition

fix initial definition

fooling around and breaking rules forces db rebuild

defmodule Saints.Repo.Migrations.CreateUser do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string
      add :username, :string, null: false
      add :password_hash, :string
      add :email, :string
      add :phone, :string

      timestamps
    end

    create index(:users, [:username])
  end
end

defmodule Saints.Repo.Migrations.CreateUser do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string
      add :username, :string, null: false
      add :password_hash, :string
      add :email, :string
      add :phone, :string

      timestamps
    end

    create unique_index(:users, [:username])
  end
end


lock ector

lock ector

defmodule Betterdev.Mixfile do
  use Mix.Project

  def project do
    [app: :betterdev,
     version: "0.0.1",
     elixir: "~> 1.4",
     elixirc_paths: elixirc_paths(Mix.env),
     compilers: [:phoenix, :gettext] ++ Mix.compilers,
     start_permanent: Mix.env == :prod,
     aliases: aliases(),
     deps: deps()]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [mod: {Betterdev.Application, []},
     extra_applications: [:logger, :runtime_tools, :exbot, :algolia, :exq, :exq_ui]]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_),     do: ["lib"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [{:phoenix, "~> 1.3.0-rc"},
     {:phoenix_pubsub, "~> 1.0"},
     {:phoenix_ecto, "~> 3.2"},
     {:ecto, "~> 2.0"},
     {:mariaex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.6"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.11"},
     {:joken, "~> 1.1"},
     {:exbot, ">= 0.0.1"},
     {:scrape, "~> 2.0.0"},
     {:kerosene, "~> 0.7.0"},
     {:exq, "~> 0.9.0"},
     {:exq_ui, "~> 0.9.0"},
		 {:algolia, git: "https://github.com/yeolabs/algolia-elixir.git"},
     {:cowboy, "~> 1.0"}]
  end

  # Aliases are shortcuts or tasks specific to the current project.
  # For example, to create, migrate and run the seeds file at once:
  #
  #     $ mix ecto.setup
  #
  # See the documentation for `Mix` for more info on aliases.
  defp aliases do
    ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
     "ecto.reset": ["ecto.drop", "ecto.setup"],
     "test": ["ecto.create --quiet", "ecto.migrate", "test"]]
  end
end

defmodule Betterdev.Mixfile do
  use Mix.Project

  def project do
    [app: :betterdev,
     version: "0.0.1",
     elixir: "~> 1.4",
     elixirc_paths: elixirc_paths(Mix.env),
     compilers: [:phoenix, :gettext] ++ Mix.compilers,
     start_permanent: Mix.env == :prod,
     aliases: aliases(),
     deps: deps()]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [mod: {Betterdev.Application, []},
     extra_applications: [:logger, :runtime_tools, :exbot, :algolia, :exq, :exq_ui]]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_),     do: ["lib"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [{:phoenix, "~> 1.3.0-rc"},
     {:phoenix_pubsub, "~> 1.0"},
     {:phoenix_ecto, "~> 3.2"},
     {:mariaex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.6"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.11"},
     {:joken, "~> 1.1"},
     {:exbot, ">= 0.0.1"},
     {:scrape, "~> 2.0.0"},
     {:kerosene, "~> 0.7.0"},
     {:exq, "~> 0.9.0"},
     {:exq_ui, "~> 0.9.0"},
		 {:algolia, git: "https://github.com/yeolabs/algolia-elixir.git"},
     {:cowboy, "~> 1.0"}]
  end

  # Aliases are shortcuts or tasks specific to the current project.
  # For example, to create, migrate and run the seeds file at once:
  #
  #     $ mix ecto.setup
  #
  # See the documentation for `Mix` for more info on aliases.
  defp aliases do
    ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
     "ecto.reset": ["ecto.drop", "ecto.setup"],
     "test": ["ecto.create --quiet", "ecto.migrate", "test"]]
  end
end


API call typo

API call typo

defmodule Blockytalky.DSL do
  alias Blockytalky.UserState, as: US
  alias Blockytalky.BrickPi, as: BP
  alias Blockytalky.Music, as: Music
  alias Blockytalky.SonicPi, as: SP
  alias Blockytalky.CommsModule, as: CM
  #alias Blockytalky.CommsChannel, as: CC

  require Logger
  @moduledoc """
  The function-version of the Blockytalky.DSL as an intermediate representation
  (or library if you prefer)
  can be many lines to one "instruction"

  Mainly interoperates with the other APIs: Hardware, Music, Comms, and UserState
  We ``should`` do type checking / correctness checking here!
  """
  ## Macros

  defmacro __using__(_) do
    #expand when student code does `use Blockytalky.DSL`
    #register all the @attributes that will accumulate the macro function definitions
    quote do
      import Blockytalky.DSL
      require Blockytalky.DSL
      require Logger
      def init(_) do
        set(:sys_timer,[])
        GenServer.call(Blockytalky.UserState, {:get_funs,:init})
        |> Enum.map(fn x -> x.() end)
      end
      def loop() do
        #loop over timer stack global var and see if it is time to do those lambdas
        process_time_events
        dequeued_message = get_message() #get the latest message for all of the receive if blocks
        GenServer.call(Blockytalky.UserState, {:get_funs,:loop})
        |> Enum.map(fn x -> x.() end)
      end
    end
  end

  ####
  # The entry points of the user program. These get stored as asts in an @attribute (:once or :everytime)
  # and then run as lambas (anonymous callbacks) either in the init function or the loop function.
  defmacro start(do: body) do
    quote do
      GenServer.call(Blockytalky.UserState, {:push_fun, :init, fn -> unquote(body) end})
    end
  end
  defmacro repeatedly(do: body) do
    quote do
      GenServer.call(Blockytalky.UserState, {:push_fun, :loop, fn -> unquote(body) end})
    end
  end
  defmacro in_time(seconds, do: body) do
    quote do
      time = (:calendar.universal_time |> :calendar.datetime_to_gregorian_seconds) + round(unquote(seconds))
      push_time_event({:at_time,time, fn -> unquote(body) end})
      time = nil
    end
  end
  defmacro for_time(seconds,clauses) do
    do_body = Keyword.get(clauses, :do, nil)
    after_body = Keyword.get(clauses, :after, :ok)
    quote do
      time = (:calendar.universal_time |> :calendar.datetime_to_gregorian_seconds) + round(unquote(seconds))
      push_time_event({:until_time, time, fn -> unquote(do_body) end, fn -> unquote(after_body) end})
      time = nil
    end
  end
  @doc """
  ## Example
      iex> when_sensor "PORT_1" > 100, do: set("item",10)
  """
  defmacro when_sensor({op,_m,[port_id,value]}, do: body) do
    #need to swap varable calls for when in case the variable changes between loops,
    #but the sensor port doesn't change
    value = case value do
      {:get, meta, opts} -> {:get_var_history, meta, opts}
      _ -> value
    end
    compare = case op do
      :> -> quote do fn x,y -> x > y end end
      :>= -> quote do fn x,y -> x >= y end end
      :< -> quote do fn x,y -> x < y end end
      :<= -> quote do fn x,y -> x <= y end end
      :== -> quote do fn x,y -> x == y end end
      :!= -> quote do fn x,y -> x != y end end
    end
    quote do
      GenServer.call(Blockytalky.UserState, {:push_fun, :loop, fn ->
        when_sensor_value_compare(unquote(port_id),
          unquote(compare),
          unquote(value), #could be a constant or a var/var history [{iteration,value}...]
          fn -> unquote(body) end
          )
        end})
    end
  end
  @doc """
  ## Example
      iex> when_sensor "PORT_1" in 1..get("my_var"), do: 1 + 1
  """
  defmacro when_sensor({:in, _m, [port_id, range={:.., _m2, [_left,_right]}]}, do: body) do
  quote do
    GenServer.call(Blockytalky.UserState, {:push_fun, :loop, fn ->
      when_sensor_value_range(unquote(port_id),
        unquote(range), #could be a constant or a var
        fn -> unquote(body) end)
        end}
      )
    end
  end
  defmacro when_sensor({:not_in, _m, [port_id, range={:.., _m2, [_left,_right]}]}, do: body) do
    quote do
      GenServer.call(Blockytalky.UserState, {:push_fun, :loop, fn ->
        when_sensor_value_range(unquote(port_id),
          unquote(range), #could be a constant or a var
          fn -> unquote(body) end,
          true) #not in flag
        end}
      )
    end
  end
  defmacro while_sensor({op,_,[port_id,value]}, do: body) do
    compare = case op do
      :> -> quote do fn x,y -> x > y end end
      :>= -> quote do fn x,y -> x >= y end end
      :< -> quote do fn x,y -> x < y end end
      :<= -> quote do fn x,y -> x <= y end end
      :== -> quote do fn x,y -> x == y end end
      :!= -> quote do fn x,y -> x != y end end
    end
    quote do
      GenServer.call(Blockytalky.UserState, {:push_fun, :loop, fn ->
        while_sensor_value_compare(unquote(port_id),
          unquote(compare),
          unquote(value), #could be a constant or a var
          fn -> unquote(body) end
          )
        end}
      )
    end
  end
  defmacro while_sensor({:in, _m, [port_id, range={:.., _m2, [_left,_right]}]}, do: body) do
    quote do
      GenServer.call(Blockytalky.UserState, {:push_fun, :loop, fn ->
        while_sensor_value_range(unquote(port_id),
          unquote(range), #could be a constant or a var
          fn -> unquote(body) end)
          end}
        )
      end
  end
  defmacro while_sensor({:not_in, _m, [port_id, range={:.., _m2, [_left,_right]}]}, do: body) do
    quote do
      GenServer.call(Blockytalky.UserState, {:push_fun, :loop, fn ->
        while_sensor_value_range(unquote(port_id),
          unquote(range), #could be a constant or a var
          fn -> unquote(body) end,
          true) #not in flag
        end}
      )
    end
  end
  defmacro when_receive(msg, do: body) do
    #msg could be get("var")
    quote do
      GenServer.call(Blockytalky.UserState, {:push_fun, :loop, fn ->
        if(unquote(msg) == dequeued_message) do #dequeue message is set at the beginning of the loop
          unquote(body)
        end
      end})
    end
  end
  ## Events
  ## Variables
  def set(var_name, value), do: US.set_var(var_name, value)
  def get(var_name) do
    case US.get_var(var_name) do #runtime error message
    nil ->
      Blockytalky.Endpoint.broadcast! "uc:command", "error", %{body: "#{var_name} has not been set!"}
      nil
    v -> v
    end
  end
  @doc """
  [{iteration, var_value}, ...]
  """
  def get_var_history(var_name) do
    case US.get_var_history(var_name) do #runtime error message
      nil ->
        Blockytalky.Endpoint.broadcast! "uc:command", "error", %{"body" => "#{var_name} has not been set!"}
        nil
      v -> v
    end
 end
  ## HW

  #get sensor value
  def get_sensor_value(port_num) do
     US.get_value(port_num)
  end

  ####
  # Helper functions called by macros
  # timer Events
  def process_time_events() do
    stack = get_sys_timer
      |> Enum.filter(fn x -> do_timer_action(x) end)
    set(:sys_timer, stack)
  end
  def push_time_event(event) do
    stack = get_sys_timer
    new_stack  = [event | stack]
    set(:sys_timer, new_stack)
  end
  defp get_sys_timer do
    case get(:sys_timer) do
      nil -> []
      v -> v
    end
  end
  @doc """
  In both of the following cases, if they return false, pop off of the stack
  """
  def do_timer_action({:at_time, time, fun}) do
    now = :calendar.universal_time |> :calendar.datetime_to_gregorian_seconds
    if now > time do
      fun.()
      false
    else
      true
    end
  end
  def do_timer_action({:until_time, time, do_fun}) do
    now = :calendar.universal_time |> :calendar.datetime_to_gregorian_seconds
    if now < time do
      do_fun.()
      true
    else
      false
    end

  end
  def do_timer_action({:until_time, time, do_fun, then_fun}) do
    now = :calendar.universal_time |> :calendar.datetime_to_gregorian_seconds
    if now < time do
      do_fun.()
      true
    else
      then_fun.()
      false
    end

  end
  #when sensor value is <comp> <value>, do: <body>
  def when_sensor_value_compare(port_id, comp_fun, value, body_fun) do
    #this is confusing, refactor this to be less awful.
    #Purpose: compare the change in the sensor value to a variable or constant, and if true, apply some body function
    #handles the edge case that the sensor stayed the same, but the value being compared against changed thus making the "when" trigger.
    {apply_fun, value} = case value do
      [{i, latest_value},{j, previous_value} | _ ] ->
        {fn(x,y,z) -> x != nil and comp_fun.(x,z) and (not comp_fun.(y,z) or not comp_fun.(y,previous_value)) end, latest_value}
      [{_, only_value}] ->
        {fn(x,y,z) -> x != nil and comp_fun.(x,z) and not comp_fun.(y,z) end, only_value}
      v ->
        {fn(x,y,z) -> x != nil and comp_fun.(x,z) and not comp_fun.(y,z) end, v}
    end
    is_valid = US.apply(
                apply_fun,
                port_id,
                value
                )
    if is_valid, do: body_fun.()
  end
  def while_sensor_value_compare(port_num, comp_fun, value, fun) do
    is_valid = US.apply(
                (fn(x,_y,z) -> x != nil and comp_fun.(x,z) end ),
                port_num,
                value
                )
    if is_valid, do: fun.()
  end
  #when sensor value is <within | out of> range, do: <body>
  def when_sensor_value_range(port_num, range, fun, not_in \\ false) do
    check_function = if not_in do
       fn(x,y,z) -> x != nil and not x in z and  y in z end
     else
       fn(x,y,z) -> x != nil and x in z and not y in z end
     end
    is_valid = US.apply(
                check_function,
                port_num,
                range
                )
    if is_valid, do: fun.()
  end
  def while_sensor_value_range(port_num, range, fun, not_in \\ false) do
    check_function = if not_in do
       fn(x,_y,z) -> x != nil and not x in z end
     else
       fn(x,_y,z) -> x != nil and x in z end
     end
    is_valid = US.apply(
                (fn(x,_y,z) -> x != nil and x in z end),
                port_num,
                range
                )
    if is_valid, do: fun.()
  end
  #set motor speed
  def bp_set_motor_speed(port_num, value) do
    if value in -100..100 do
      BP.set_motor_value(port_num, value)
    else
      Blockytalky.Endpoint.broadcast "uc:command", "error", %{"body" => "Tried to set motor speed to value not in -100 to 100: #{inspect value}"}
    end
  end
  ## Grove Pi
  ## Comms
  # message <msg> to <unit>
  def send_message(msg, to), do: CM.send_message(msg,to)
  def get_message() do
    {_sender, msg} = US.dequeue_message
    msg
  end
  def say(msg) do
    Blockytalky.Endpoint.broadcast "comms:message", "say", %{"body" => msg}
  end
  ## Music
  # motif <motif name> do: <music program>
  # cue <motif> in <num> beats
  # (optional) cue <signal>
  # (optional) sync <signal>
end

defmodule Blockytalky.DSL do
  alias Blockytalky.UserState, as: US
  alias Blockytalky.BrickPi, as: BP
  alias Blockytalky.Music, as: Music
  alias Blockytalky.SonicPi, as: SP
  alias Blockytalky.CommsModule, as: CM
  #alias Blockytalky.CommsChannel, as: CC

  require Logger
  @moduledoc """
  The function-version of the Blockytalky.DSL as an intermediate representation
  (or library if you prefer)
  can be many lines to one "instruction"

  Mainly interoperates with the other APIs: Hardware, Music, Comms, and UserState
  We ``should`` do type checking / correctness checking here!
  """
  ## Macros

  defmacro __using__(_) do
    #expand when student code does `use Blockytalky.DSL`
    #register all the @attributes that will accumulate the macro function definitions
    quote do
      import Blockytalky.DSL
      require Blockytalky.DSL
      require Logger
      def init(_) do
        set(:sys_timer,[])
        GenServer.call(Blockytalky.UserState, {:get_funs,:init})
        |> Enum.map(fn x -> x.() end)
      end
      def loop() do
        #loop over timer stack global var and see if it is time to do those lambdas
        process_time_events
        dequeued_message = get_message() #get the latest message for all of the receive if blocks
        GenServer.call(Blockytalky.UserState, {:get_funs,:loop})
        |> Enum.map(fn x -> x.() end)
      end
    end
  end

  ####
  # The entry points of the user program. These get stored as asts in an @attribute (:once or :everytime)
  # and then run as lambas (anonymous callbacks) either in the init function or the loop function.
  defmacro start(do: body) do
    quote do
      GenServer.call(Blockytalky.UserCode, {:push_fun, :init, fn -> unquote(body) end})
    end
  end
  defmacro repeatedly(do: body) do
    quote do
      GenServer.call(Blockytalky.UserCode, {:push_fun, :loop, fn -> unquote(body) end})
    end
  end
  defmacro in_time(seconds, do: body) do
    quote do
      time = (:calendar.universal_time |> :calendar.datetime_to_gregorian_seconds) + round(unquote(seconds))
      push_time_event({:at_time,time, fn -> unquote(body) end})
      time = nil
    end
  end
  defmacro for_time(seconds,clauses) do
    do_body = Keyword.get(clauses, :do, nil)
    after_body = Keyword.get(clauses, :after, :ok)
    quote do
      time = (:calendar.universal_time |> :calendar.datetime_to_gregorian_seconds) + round(unquote(seconds))
      push_time_event({:until_time, time, fn -> unquote(do_body) end, fn -> unquote(after_body) end})
      time = nil
    end
  end
  @doc """
  ## Example
      iex> when_sensor "PORT_1" > 100, do: set("item",10)
  """
  defmacro when_sensor({op,_m,[port_id,value]}, do: body) do
    #need to swap varable calls for when in case the variable changes between loops,
    #but the sensor port doesn't change
    value = case value do
      {:get, meta, opts} -> {:get_var_history, meta, opts}
      _ -> value
    end
    compare = case op do
      :> -> quote do fn x,y -> x > y end end
      :>= -> quote do fn x,y -> x >= y end end
      :< -> quote do fn x,y -> x < y end end
      :<= -> quote do fn x,y -> x <= y end end
      :== -> quote do fn x,y -> x == y end end
      :!= -> quote do fn x,y -> x != y end end
    end
    quote do
      GenServer.call(Blockytalky.UserCode, {:push_fun, :loop, fn ->
        when_sensor_value_compare(unquote(port_id),
          unquote(compare),
          unquote(value), #could be a constant or a var/var history [{iteration,value}...]
          fn -> unquote(body) end
          )
        end})
    end
  end
  @doc """
  ## Example
      iex> when_sensor "PORT_1" in 1..get("my_var"), do: 1 + 1
  """
  defmacro when_sensor({:in, _m, [port_id, range={:.., _m2, [_left,_right]}]}, do: body) do
  quote do
    GenServer.call(Blockytalky.UserCode, {:push_fun, :loop, fn ->
      when_sensor_value_range(unquote(port_id),
        unquote(range), #could be a constant or a var
        fn -> unquote(body) end)
        end}
      )
    end
  end
  defmacro when_sensor({:not_in, _m, [port_id, range={:.., _m2, [_left,_right]}]}, do: body) do
    quote do
      GenServer.call(Blockytalky.UserCode, {:push_fun, :loop, fn ->
        when_sensor_value_range(unquote(port_id),
          unquote(range), #could be a constant or a var
          fn -> unquote(body) end,
          true) #not in flag
        end}
      )
    end
  end
  defmacro while_sensor({op,_,[port_id,value]}, do: body) do
    compare = case op do
      :> -> quote do fn x,y -> x > y end end
      :>= -> quote do fn x,y -> x >= y end end
      :< -> quote do fn x,y -> x < y end end
      :<= -> quote do fn x,y -> x <= y end end
      :== -> quote do fn x,y -> x == y end end
      :!= -> quote do fn x,y -> x != y end end
    end
    quote do
      GenServer.call(Blockytalky.UserCode, {:push_fun, :loop, fn ->
        while_sensor_value_compare(unquote(port_id),
          unquote(compare),
          unquote(value), #could be a constant or a var
          fn -> unquote(body) end
          )
        end}
      )
    end
  end
  defmacro while_sensor({:in, _m, [port_id, range={:.., _m2, [_left,_right]}]}, do: body) do
    quote do
      GenServer.call(Blockytalky.UserCode, {:push_fun, :loop, fn ->
        while_sensor_value_range(unquote(port_id),
          unquote(range), #could be a constant or a var
          fn -> unquote(body) end)
          end}
        )
      end
  end
  defmacro while_sensor({:not_in, _m, [port_id, range={:.., _m2, [_left,_right]}]}, do: body) do
    quote do
      GenServer.call(Blockytalky.UserCode, {:push_fun, :loop, fn ->
        while_sensor_value_range(unquote(port_id),
          unquote(range), #could be a constant or a var
          fn -> unquote(body) end,
          true) #not in flag
        end}
      )
    end
  end
  defmacro when_receive(msg, do: body) do
    #msg could be get("var")
    quote do
      GenServer.call(Blockytalky.UserCode, {:push_fun, :loop, fn ->
        if(unquote(msg) == dequeued_message) do #dequeue message is set at the beginning of the loop
          unquote(body)
        end
      end})
    end
  end
  ## Events
  ## Variables
  def set(var_name, value), do: US.set_var(var_name, value)
  def get(var_name) do
    case US.get_var(var_name) do #runtime error message
    nil ->
      Blockytalky.Endpoint.broadcast! "uc:command", "error", %{body: "#{var_name} has not been set!"}
      nil
    v -> v
    end
  end
  @doc """
  [{iteration, var_value}, ...]
  """
  def get_var_history(var_name) do
    case US.get_var_history(var_name) do #runtime error message
      nil ->
        Blockytalky.Endpoint.broadcast! "uc:command", "error", %{"body" => "#{var_name} has not been set!"}
        nil
      v -> v
    end
 end
  ## HW

  #get sensor value
  def get_sensor_value(port_num) do
     US.get_value(port_num)
  end

  ####
  # Helper functions called by macros
  # timer Events
  def process_time_events() do
    stack = get_sys_timer
      |> Enum.filter(fn x -> do_timer_action(x) end)
    set(:sys_timer, stack)
  end
  def push_time_event(event) do
    stack = get_sys_timer
    new_stack  = [event | stack]
    set(:sys_timer, new_stack)
  end
  defp get_sys_timer do
    case get(:sys_timer) do
      nil -> []
      v -> v
    end
  end
  @doc """
  In both of the following cases, if they return false, pop off of the stack
  """
  def do_timer_action({:at_time, time, fun}) do
    now = :calendar.universal_time |> :calendar.datetime_to_gregorian_seconds
    if now > time do
      fun.()
      false
    else
      true
    end
  end
  def do_timer_action({:until_time, time, do_fun}) do
    now = :calendar.universal_time |> :calendar.datetime_to_gregorian_seconds
    if now < time do
      do_fun.()
      true
    else
      false
    end

  end
  def do_timer_action({:until_time, time, do_fun, then_fun}) do
    now = :calendar.universal_time |> :calendar.datetime_to_gregorian_seconds
    if now < time do
      do_fun.()
      true
    else
      then_fun.()
      false
    end

  end
  #when sensor value is <comp> <value>, do: <body>
  def when_sensor_value_compare(port_id, comp_fun, value, body_fun) do
    #this is confusing, refactor this to be less awful.
    #Purpose: compare the change in the sensor value to a variable or constant, and if true, apply some body function
    #handles the edge case that the sensor stayed the same, but the value being compared against changed thus making the "when" trigger.
    {apply_fun, value} = case value do
      [{i, latest_value},{j, previous_value} | _ ] ->
        {fn(x,y,z) -> x != nil and comp_fun.(x,z) and (not comp_fun.(y,z) or not comp_fun.(y,previous_value)) end, latest_value}
      [{_, only_value}] ->
        {fn(x,y,z) -> x != nil and comp_fun.(x,z) and not comp_fun.(y,z) end, only_value}
      v ->
        {fn(x,y,z) -> x != nil and comp_fun.(x,z) and not comp_fun.(y,z) end, v}
    end
    is_valid = US.apply(
                apply_fun,
                port_id,
                value
                )
    if is_valid, do: body_fun.()
  end
  def while_sensor_value_compare(port_num, comp_fun, value, fun) do
    is_valid = US.apply(
                (fn(x,_y,z) -> x != nil and comp_fun.(x,z) end ),
                port_num,
                value
                )
    if is_valid, do: fun.()
  end
  #when sensor value is <within | out of> range, do: <body>
  def when_sensor_value_range(port_num, range, fun, not_in \\ false) do
    check_function = if not_in do
       fn(x,y,z) -> x != nil and not x in z and  y in z end
     else
       fn(x,y,z) -> x != nil and x in z and not y in z end
     end
    is_valid = US.apply(
                check_function,
                port_num,
                range
                )
    if is_valid, do: fun.()
  end
  def while_sensor_value_range(port_num, range, fun, not_in \\ false) do
    check_function = if not_in do
       fn(x,_y,z) -> x != nil and not x in z end
     else
       fn(x,_y,z) -> x != nil and x in z end
     end
    is_valid = US.apply(
                (fn(x,_y,z) -> x != nil and x in z end),
                port_num,
                range
                )
    if is_valid, do: fun.()
  end
  #set motor speed
  def bp_set_motor_speed(port_num, value) do
    if value in -100..100 do
      BP.set_motor_value(port_num, value)
    else
      Blockytalky.Endpoint.broadcast "uc:command", "error", %{"body" => "Tried to set motor speed to value not in -100 to 100: #{inspect value}"}
    end
  end
  ## Grove Pi
  ## Comms
  # message <msg> to <unit>
  def send_message(msg, to), do: CM.send_message(msg,to)
  def get_message() do
    {_sender, msg} = US.dequeue_message
    msg
  end
  def say(msg) do
    Blockytalky.Endpoint.broadcast "comms:message", "say", %{"body" => msg}
  end
  ## Music
  # motif <motif name> do: <music program>
  # cue <motif> in <num> beats
  # (optional) cue <signal>
  # (optional) sync <signal>
end


Parse branch for build info

Parse branch for build info

defmodule Credo.BuildInfo do
  @moduledoc false

  # This is called at compile-time and should not be used at runtime!

  @doc false
  def version(mix_version) do
    version(mix_version, git_info())
  end

  defp version(mix_version, nil) do
    mix_version
  end

  defp version(mix_version, git_info) do
    version =
      if git_info.tag == "v#{mix_version}" do
        mix_version
      else
        branch = git_info.branch || "nobranch"
        commit = git_info.commit || "nocommit"

        "#{mix_version}-ref.#{branch}.#{commit}"
      end

    if git_info.dirty? do
      "#{version}+uncommittedchanges"
    else
      version
    end
  end

  defp git_info do
    if git_present?() do
      %{
        commit: git_commit(),
        branch: git_branch(),
        tag: git_tag(),
        dirty?: git_dirty?()
      }
    end
  end

  defp git_present? do
    {_, exit_status} = System.cmd("git", ["--help"])

    exit_status == 0
  end

  defp git_branch do
    case System.cmd("git", ["branch"]) do
      {output, 0} -> git_branch(output)
      {_, _} -> nil
    end
  end

  defp git_branch(output) do
    line_with_active_branch =
      output
      |> String.split("\n")
      |> Enum.find(fn
        "* " <> _ -> true
        _ -> false
      end)

    case line_with_active_branch do
      "* (HEAD detached at origin/" <> remote_branch_name ->
        String.replace(remote_branch_name, ~r/\)$/, "")

      "* (HEAD detached at " <> branch_name ->
        String.replace(branch_name, ~r/\)$/, "")

      "* " <> branch_name ->
        branch_name

      _ ->
        nil
    end
  end

  defp git_commit do
    case System.cmd("git", ["rev-parse", "--short", "HEAD"]) do
      {output, 0} -> String.trim(output)
      {_, _} -> nil
    end
  end

  defp git_tag do
    case System.cmd("git", ["tag", "--points-at", "HEAD"]) do
      {output, 0} -> String.trim(output)
      {_, _} -> nil
    end
  end

  defp git_dirty? do
    case System.cmd("git", ["status", "--short"]) do
      {output, 0} -> output |> String.trim() |> git_dirty?()
      {_, _} -> nil
    end
  end

  defp git_dirty?(""), do: false
  # Hex puts a `.fetch` file in the working dir when downloading deps via git
  defp git_dirty?("?? .fetch"), do: false
  defp git_dirty?(_), do: true
end

defmodule Credo.BuildInfo do
  @moduledoc false

  # This is called at compile-time and should not be used at runtime!

  @doc false
  def version(mix_version) do
    version(mix_version, git_info())
  end

  defp version(mix_version, nil) do
    mix_version
  end

  defp version(mix_version, git_info) do
    version =
      if git_info.tag == "v#{mix_version}" do
        mix_version
      else
        branch = git_info.branch || "nobranch"
        commit = git_info.commit || "nocommit"

        "#{mix_version}-ref.#{branch}.#{commit}"
      end

    if git_info.dirty? do
      "#{version}+uncommittedchanges"
    else
      version
    end
  end

  defp git_info do
    if git_present?() do
      %{
        commit: git_commit(),
        branch: git_branch(),
        tag: git_tag(),
        dirty?: git_dirty?()
      }
    end
  end

  defp git_present? do
    {_, exit_status} = System.cmd("git", ["--help"])

    exit_status == 0
  end

  defp git_branch do
    case System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]) do
      {output, 0} -> String.trim(output)
      {_, _} -> nil
    end
  end

  defp git_commit do
    case System.cmd("git", ["rev-parse", "--short", "HEAD"]) do
      {output, 0} -> String.trim(output)
      {_, _} -> nil
    end
  end

  defp git_tag do
    case System.cmd("git", ["tag", "--points-at", "HEAD"]) do
      {output, 0} -> String.trim(output)
      {_, _} -> nil
    end
  end

  defp git_dirty? do
    case System.cmd("git", ["status", "--short"]) do
      {output, 0} -> String.trim(output) != ""
      {_, _} -> nil
    end
  end
end