Repository: elixir-ecto/ecto (elixir)

Issue:
`mix ecto.migrations` ignore the migrated versions while migration file is deleted
### Environment

* Elixir version (elixir -v): 1.5.2 (Erlang/OTP 20)
* Database and version (PostgreSQL 9.4, MongoDB 3.2, etc.): PostgreSQL 10.1
* Ecto version (mix deps): 2.2.7
* Database adapter and version (mix deps): postgrex 0.13.3
* Operating system: macOS

### Current behavior
After migrate the database, if any migration file is deleted, then `mix ecto.migrations` won't list corresponded migration, even though they're actually `:up` in the database.

To reporduce:
1. Generate a migration then run `mix ecto.migrate`
2. Delete the migration file
3. Run `mix ecto.migrations`
4. the report list is empty.
5. Check the `schema_migrations` table in database, there are actually one migration record there.

### Expected behavior

If the migration file is deleted, list them as :up with "FILE NOT FOUND" description


Pull request description:
List migrated versions even if the migration file is deleted
This PR resolves #2347. 


Patch:
diff --git a/lib/ecto/migrator.ex b/lib/ecto/migrator.ex
index a18925f1..18ed1416 100644
--- a/lib/ecto/migrator.ex
+++ b/lib/ecto/migrator.ex
@@ -209,15 +209,10 @@ defmodule Ecto.Migrator do
 
   """
   def migrations(repo, directory) do
-    versions = migrated_versions(repo)
-
-    Enum.map(pending_in_direction(versions, directory, :down) |> Enum.reverse, fn {a, b, _}
-      -> {:up, a, b}
-    end)
-    ++
-    Enum.map(pending_in_direction(versions, directory, :up), fn {a, b, _} ->
-      {:down, a, b}
-    end)
+    repo
+    |> migrated_versions
+    |> collect_migrations(directory)
+    |> Enum.sort_by(fn {_, version, _} -> version end)
   end
 
   defp lock_for_migrations(repo, opts, fun) do
@@ -268,6 +263,34 @@ defmodule Ecto.Migrator do
     |> Enum.reverse
   end
 
+  defp collect_migrations(versions, migration_source) do
+    ups_with_file =
+      versions
+      |> pending_in_direction(migration_source, :down)
+      |> Enum.map(fn {version, name, _} -> {:up, version, name} end)
+
+    ups_without_file =
+      versions
+      |> versions_without_file(migration_source)
+      |> Enum.map(fn version -> {:up, version, "** FILE NOT FOUND **"} end)
+
+    downs =
+      versions
+      |> pending_in_direction(migration_source, :up)
+      |> Enum.map(fn {version, name, _} -> {:down, version, name} end)
+
+    ups_with_file ++ ups_without_file ++ downs
+  end
+
+  defp versions_without_file(versions, migration_source) do
+    versions_with_file =
+      migration_source
+      |> migrations_for
+      |> Enum.map(&elem(&1, 0))
+
+    versions -- versions_with_file
+  end
+
   # This function will match directories passed into `Migrator.run`.
   defp migrations_for(migration_source) when is_binary(migration_source) do
     query = Path.join(migration_source, "*")


Test patch:
diff --git a/test/ecto/migrator_test.exs b/test/ecto/migrator_test.exs
index 6a014987..4bb72072 100644
--- a/test/ecto/migrator_test.exs
+++ b/test/ecto/migrator_test.exs
@@ -303,6 +303,26 @@ defmodule Ecto.MigratorTest do
     end
   end
 
+  test "migrations will give the migration status while file is deleted" do
+    in_tmp fn path ->
+      create_migration "1_up_migration_1.exs"
+      create_migration "2_up_migration_2.exs"
+      create_migration "3_up_migration_3.exs"
+      create_migration "4_down_migration_1.exs"
+
+      File.rm("2_up_migration_2.exs")
+
+      expected_result = [
+        {:up, 1, "up_migration_1"},
+        {:up, 2, "** FILE NOT FOUND **"},
+        {:up, 3, "up_migration_3"},
+        {:down, 4, "down_migration_1"},
+      ]
+
+      assert migrations(TestRepo, path) == expected_result
+    end
+  end
+
   test "migrations run inside a transaction if the adapter supports ddl transactions" do
     capture_log fn ->
       put_test_adapter_config(supports_ddl_transaction?: true, test_process: self())