Repository: elixir-gettext/gettext (elixir)

Issue:
If changes to `.po` file are discarded (accidentally), they're not added back
### How to reproduce:

https://github.com/saveman71/gettext-repro-cases/tree/missing-po-if-checked-out

In `gettext_mwe.ex`, add a second `gettext` call to the `gettext` function:

```elixir
  gettext("First string")
  gettext("Second string")
```

Run `mix gettext.extract --merge` and observe that both `po` and `pot` files are updated.

Run `git checkout -- priv/gettext/fr/LC_MESSAGES/default.po` to "accidentally" reset the `po` file.

Run `mix gettext.extract --merge` and observe that nothing is updated.

From that point on, if you commit and push, gettext won't catch the new strings until the code is modified.

### Expected behaviour

If you change/discard the changes to `default.pot`, gettext will overwrite your changes based on what's found.
I expect the same thing if I change/discard changes in `default.po`.

Pull request description:
Run merge for `mix gettext.extract` pot files even if they are unchanged
Resolves #377

Patch:
diff --git a/lib/gettext/extractor.ex b/lib/gettext/extractor.ex
index bf6a500..c6a1d4b 100644
--- a/lib/gettext/extractor.ex
+++ b/lib/gettext/extractor.ex
@@ -222,7 +222,6 @@ defmodule Gettext.Extractor do
     #   %{path => {:merged, :unchanged | %Messages{}}, path => {:unmerged, :unchanged | %Messages{}}, path => {:new, %Messages{}}}
     Map.merge(pot_files, po_structs, &merge_existing_and_extracted(&1, &2, &3, gettext_config))
     |> Enum.map(&tag_files(&1, gettext_config))
-    |> Enum.reject(&match?({_, {_, :unchanged}}, &1))
     |> Enum.map(&dump_tagged_file/1)
   end
 
@@ -273,7 +272,8 @@ defmodule Gettext.Extractor do
   # This function "dumps" merged files and unmerged files without any changes,
   # and dumps new POT files adding an informative comment to them. This doesn't
   # write anything to disk, it just returns `{path, contents}` tuples.
-  defp dump_tagged_file({path, {_tag, po}}), do: {path, PO.compose(po)}
+  defp dump_tagged_file({path, {_tag, :unchanged}}), do: {path, :unchanged}
+  defp dump_tagged_file({path, {_tag, po}}), do: {path, {:changed, PO.compose(po)}}
 
   defp prune_unmerged(path, gettext_config) do
     merge_or_unchanged(path, %Messages{messages: []}, gettext_config)
diff --git a/lib/mix/tasks/gettext.extract.ex b/lib/mix/tasks/gettext.extract.ex
index 616fe62..347e0b7 100644
--- a/lib/mix/tasks/gettext.extract.ex
+++ b/lib/mix/tasks/gettext.extract.ex
@@ -75,7 +75,7 @@ defmodule Mix.Tasks.Gettext.Extract do
   end
 
   defp run_message_extraction(pot_files, opts, args) do
-    for {path, contents} <- pot_files do
+    for {path, {:changed, contents}} <- pot_files do
       File.mkdir_p!(Path.dirname(path))
       File.write!(path, contents)
       Mix.shell().info("Extracted #{Path.relative_to_cwd(path)}")
@@ -89,9 +89,9 @@ defmodule Mix.Tasks.Gettext.Extract do
   end
 
   defp run_up_to_date_check(pot_files) do
-    not_extracted_paths = for {path, _contents} <- pot_files, do: path
+    not_extracted_paths = for {path, {:changed, _contents}} <- pot_files, do: path
 
-    if pot_files == [] do
+    if not_extracted_paths == [] do
       :ok
     else
       Mix.raise("""


Test patch:
diff --git a/test/gettext/extractor_test.exs b/test/gettext/extractor_test.exs
index b12d859..d3009d1 100644
--- a/test/gettext/extractor_test.exs
+++ b/test/gettext/extractor_test.exs
@@ -33,9 +33,9 @@ defmodule Gettext.ExtractorTest do
         Extractor.merge_pot_files(extracted_po_structs, [paths.tomerge, paths.ignored], [])
 
       # Unchanged files are not returned
-      assert List.keyfind(structs, paths.ignored, 0) == nil
+      assert {_path, :unchanged} = List.keyfind(structs, paths.ignored, 0)
 
-      {_, contents} = List.keyfind(structs, paths.tomerge, 0)
+      assert {_, {:changed, contents}} = List.keyfind(structs, paths.tomerge, 0)
 
       assert IO.iodata_to_binary(contents) == """
              msgid "foo"
@@ -45,7 +45,7 @@ defmodule Gettext.ExtractorTest do
              msgstr ""
              """
 
-      {_, contents} = List.keyfind(structs, paths.new, 0)
+      assert {_, {:changed, contents}} = List.keyfind(structs, paths.new, 0)
       contents = IO.iodata_to_binary(contents)
 
       assert contents =~ """
@@ -385,7 +385,11 @@ defmodule Gettext.ExtractorTest do
     assert [] = Extractor.pot_files(:unknown, [])
 
     pot_files = Extractor.pot_files(:test_application, [])
-    dumped = Enum.map(pot_files, fn {k, v} -> {k, IO.iodata_to_binary(v)} end)
+
+    dumped =
+      pot_files
+      |> Enum.reject(&match?({_path, :unchanged}, &1))
+      |> Enum.map(fn {k, {:changed, v}} -> {k, IO.iodata_to_binary(v)} end)
 
     # We check that dumped strings end with the `expected` string because
     # there's the informative comment at the start of each dumped string.
diff --git a/test/mix/tasks/gettext.extract_test.exs b/test/mix/tasks/gettext.extract_test.exs
index a4eb5b8..7e1d702 100644
--- a/test/mix/tasks/gettext.extract_test.exs
+++ b/test/mix/tasks/gettext.extract_test.exs
@@ -60,6 +60,10 @@ defmodule Mix.Tasks.Gettext.ExtractTest do
            msgid "other"
            msgstr ""
            """
+
+    capture_io(fn ->
+      Mix.Project.in_project(test, tmp_dir, fn _module -> run(["--merge"]) end)
+    end) =~ "Wrote priv/gettext/it/LC_MESSAGES/my_domain.po"
   end
 
   test "--check-up-to-date should fail if no POT files have been created",