# `elixir git_log_cruncher.exs` Mix.install([ {:timex, "~> 3.5"} ]) defmodule PR do defstruct [:hash, :number, :title, :file_count, :insertions, :deletions] @type t() :: %{ hash: String.t(), number: String.t(), title: String.t(), file_count: String.t(), insertions: String.t(), deletions: String.t() } @spec to_string(PR.t()) :: String.t() def to_string(%PR{} = pr), do: "##{pr.number} - #{pr.title}" @doc """ Sample response [ "17b1f342a0 🔥 Remove unused fixtures to impr coverage (#12345)", " 2 files changed, 25 insertions(+), 195 deletions(-)" ] """ @spec parse([String.t()]) :: PR.t() def parse([<>, diff_string]) do # Not working yet number = case Regex.named_captures(~r/\(?#\d+\)/, rest) do nil -> nil %{"number" => number} -> number end title = if number, do: String.trim_trailing(rest, number), else: rest [files | [insertions | maybe_deletions]] = diff_string |> String.split(", ") |> Enum.map(&String.trim/1) file_count = files |> String.split() |> Enum.at(0) insertion_count = insertions |> String.split() |> Enum.at(0) deletions_count = if Enum.empty?(maybe_deletions), do: "0", else: hd(maybe_deletions) |> String.split() |> Enum.at(0) %PR{ hash: hash, number: number || "", title: title, file_count: elem(Integer.parse(file_count), 0), insertions: elem(Integer.parse(insertion_count), 0), deletions: elem(Integer.parse(deletions_count), 0) } end def parse(other) do IO.warn("Couldn't parse #{inspect(other)}") %PR{ hash: "", number: "", title: "", file_count: 0, insertions: 0, deletions: 0 } end end defmodule Aggregate do defstruct pr_count: 0, file_count: 0, insertions: 0, deletions: 0 @type t() :: %{ pr_count: integer(), file_count: integer(), insertions: integer(), deletions: integer() } @spec to_string(Aggregate.t()) :: String.t() def to_string(%Aggregate{} = agg) do "PRs: #{agg.pr_count}, Files: #{agg.file_count}, Insertions: #{agg.insertions}, Deletions: #{agg.deletions}" end @spec add(Aggregate.t(), PR.t()) :: Aggregate.t() def add(%PR{} = pr, %Aggregate{} = agg) do %Aggregate{ pr_count: agg.pr_count + 1, file_count: agg.file_count + pr.file_count, insertions: agg.insertions + pr.insertions, deletions: agg.deletions + pr.deletions } end def new, do: %Aggregate{} defp format_date(date), do: Timex.format!(date, "%Y-%m-%d", :strftime) @doc """ Sun - Sat date ranges, inclusive. """ @spec week_date_range(Date.t()) :: {String.t(), String.t()} def week_date_range(date) do sat_end_of_week = date |> Timex.end_of_week() |> Timex.shift(days: -1) sunday_start_of_week = sat_end_of_week |> Timex.shift(days: -6) {format_date(sunday_start_of_week), format_date(sat_end_of_week)} end end today = Date.utc_today() for weeks_ago <- 0..24, date = Timex.shift(today, days: -7 * weeks_ago), {start_date, end_date} = Aggregate.week_date_range(date) do args = ["log", ~s{--after="#{start_date}"}, ~s{--before="#{end_date}"}, "--shortstat", "--oneline"] # IO.puts("git #{Enum.join(args, " ")}") {response, _exit_code = 0} = System.cmd("git", args) response |> String.split("\n") |> Enum.chunk_every(2) |> Enum.reject(fn [a, b] -> a == "" || b == "" [""] -> true end) |> Enum.map(&PR.parse/1) |> Enum.reduce(Aggregate.new(), &Aggregate.add/2) |> IO.inspect(label: "#{start_date} - #{end_date}") end