Skip to content

Instantly share code, notes, and snippets.

@shamshirz
Created July 27, 2023 18:31
Show Gist options
  • Save shamshirz/c955b689b251d499effcdde0dd4c635a to your computer and use it in GitHub Desktop.
Save shamshirz/c955b689b251d499effcdde0dd4c635a to your computer and use it in GitHub Desktop.
Elixir Script to parse local gitlog data and produce aggregate data
# `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([<<hash::binary-size(10), rest::binary>>, diff_string]) do
# Not working yet
number =
case Regex.named_captures(~r/\(?<number>#\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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment