diff --git a/README.md b/README.md index 0b70228..8edbf62 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ For an example, see this project's [CHANGELOG.md](https://github.com/zachdaniel/ Roadmap (in no particular order): * More tests -* Support multiple changes in a single commit via multiple conventional commits - in a single commit * Automatically parse issue numbers and github mentions into the correct format, linking the issue * A task to build a compliant commit diff --git a/lib/git_ops/commit.ex b/lib/git_ops/commit.ex index 33d2e01..53c4771 100644 --- a/lib/git_ops/commit.ex +++ b/lib/git_ops/commit.ex @@ -19,7 +19,8 @@ defmodule GitOps.Commit do # 40/41 are `(` and `)`, but syntax highlighters don't like ?( and ?) type = optional(whitespace) - |> tag(ascii_string([not: ?:, not: ?!, not: 40, not: 41], min: 1), :type) + |> optional(whitespace) + |> tag(ascii_string([not: ?:, not: ?!, not: 40, not: 41, not: 10, not: 32], min: 1), :type) |> optional(whitespace) scope = @@ -33,13 +34,30 @@ defmodule GitOps.Commit do message = tag(optional(whitespace), ascii_string([not: ?\n], min: 1), :message) - defparsecp( - :commit, + commit = type |> concat(optional(scope)) |> concat(optional(breaking_change_indicator)) |> ignore(ascii_char([?:])) - |> concat(message), + |> concat(message) + |> concat(optional(whitespace)) + |> concat(optional(ignore(ascii_string([10], min: 1)))) + + body = + [commit, eos()] + |> choice() + |> lookahead_not() + |> utf8_char([]) + |> repeat() + |> reduce({List, :to_string, []}) + |> tag(:body) + + defparsecp( + :commits, + commit + |> concat(body) + |> tag(:commit) + |> repeat(), inline: true ) @@ -77,26 +95,35 @@ defmodule GitOps.Commit do end def parse(text) do - case commit(text) do - {:ok, result, remaining, _state, _dunno, _also_dunno} -> - remaining_lines = - remaining - |> String.split("\n") - |> Enum.map(&String.trim/1) - |> Enum.reject(&Kernel.==(&1, "")) - - body = Enum.at(remaining_lines, 0) - footer = Enum.at(remaining_lines, 1) - - {:ok, - %__MODULE__{ - type: Enum.at(result[:type], 0), - scope: scopes(result[:scope]), - message: Enum.at(result[:message], 0), - body: body, - footer: footer, - breaking?: is_breaking?(result[:breaking?], body, footer) - }} + case commits(text) do + {:ok, [], _, _, _, _} -> + :error + + {:ok, results, _remaining, _state, _dunno, _also_dunno} -> + commits = + Enum.map(results, fn {:commit, result} -> + remaining_lines = + result[:body] + |> Enum.map(&String.trim/1) + |> Enum.join("\n") + |> String.split("\n") + |> Enum.map(&String.trim/1) + |> Enum.reject(&Kernel.==(&1, "")) + + body = Enum.at(remaining_lines, 0) + footer = Enum.at(remaining_lines, 1) + + %__MODULE__{ + type: Enum.at(result[:type], 0), + scope: scopes(result[:scope]), + message: Enum.at(result[:message], 0), + body: body, + footer: footer, + breaking?: is_breaking?(result[:breaking?], body, footer) + } + end) + + {:ok, commits} {:error, _message, _remaining, _state, _dunno, _also_dunno} -> :error diff --git a/lib/mix/tasks/git_ops.release.ex b/lib/mix/tasks/git_ops.release.ex index 846936c..61fc440 100644 --- a/lib/mix/tasks/git_ops.release.ex +++ b/lib/mix/tasks/git_ops.release.ex @@ -215,14 +215,8 @@ defmodule Mix.Tasks.GitOps.Release do defp parse_commit(text, config_types, log?) do case Commit.parse(text) do - {:ok, commit} -> - if Map.has_key?(config_types, String.downcase(commit.type)) do - [commit] - else - error_if_log("Commit with unknown type: #{text}", log?) - - [] - end + {:ok, commits} -> + commits_with_type(config_types, commits, text, log?) _ -> error_if_log("Unparseable commit: #{text}", log?) @@ -231,6 +225,18 @@ defmodule Mix.Tasks.GitOps.Release do end end + defp commits_with_type(config_types, commits, text, log?) do + Enum.flat_map(commits, fn commit -> + if Map.has_key?(config_types, String.downcase(commit.type)) do + [commit] + else + error_if_log("Commit with unknown type in: #{text}", log?) + + [] + end + end) + end + defp append_changes_to_message(message, _, {:error, :bad_replace}), do: message defp append_changes_to_message(message, file, changes) do diff --git a/test/commit_test.exs b/test/commit_test.exs index 116a77a..7019f9c 100644 --- a/test/commit_test.exs +++ b/test/commit_test.exs @@ -3,24 +3,30 @@ defmodule GitOps.Test.CommitTest do alias GitOps.Commit - defp format!(message) do + defp format_one!(message) do message - |> parse!() + |> parse_one!() |> Commit.format() end - defp parse!(message) do - {:ok, commit} = Commit.parse(message) + defp parse_one!(message) do + {:ok, [commit]} = Commit.parse(message) commit end + defp parse_many!(message) do + {:ok, commits} = Commit.parse(message) + + commits + end + test "a simple feature is parsed with the correct type" do - assert parse!("feat: An awesome new feature!").type == "feat" + assert parse_one!("feat: An awesome new feature!").type == "feat" end test "a simple feature is parsed with the correct message" do - assert parse!("feat: An awesome new feature!").message == "An awesome new feature!" + assert parse_one!("feat: An awesome new feature!").message == "An awesome new feature!" end @tag :regression @@ -29,18 +35,47 @@ defmodule GitOps.Test.CommitTest do end test "a breaking change via a postfixed exclamation mark is parsed as a breaking change" do - assert parse!("feat!: A breaking change").breaking? + assert parse_one!("feat!: A breaking change").breaking? end test "a breaking change via a postfixed exclamation mark after a scope is parsed as a breaking change" do - assert parse!("feat(stuff)!: A breaking change").breaking? + assert parse_one!("feat(stuff)!: A breaking change").breaking? end test "a simple feature is formatted correctly" do - assert format!("feat: An awesome new feature!") == "* An awesome new feature!" + assert format_one!("feat: An awesome new feature!") == "* An awesome new feature!" end test "a breaking change does not include the exclamation mark in the formatted version" do - assert format!("feat!: An awesome new feature!") == "* An awesome new feature!" + assert format_one!("feat!: An awesome new feature!") == "* An awesome new feature!" + end + + test "multiple messages can be parsed from a commit" do + text = """ + fix: fixed a bug + + some text about it + + some even more data about it + + improvement: improved a thing + + some other text about it + + some even more text about it + """ + + assert [ + %Commit{ + message: "fixed a bug", + body: "some text about it", + footer: "some even more data about it" + }, + %Commit{ + message: "improved a thing", + body: "some other text about it", + footer: "some even more text about it" + } + ] = parse_many!(text) end end