Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package elixir for openSUSE:Factory checked in at 2025-11-28 16:54:15 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/elixir (Old) and /work/SRC/openSUSE:Factory/.elixir.new.14147 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "elixir" Fri Nov 28 16:54:15 2025 rev:45 rq:1320429 version:1.19.4 Changes: -------- --- /work/SRC/openSUSE:Factory/elixir/elixir.changes 2025-11-14 16:22:03.783358909 +0100 +++ /work/SRC/openSUSE:Factory/.elixir.new.14147/elixir.changes 2025-11-28 16:56:41.391778608 +0100 @@ -1,0 +2,6 @@ +Fri Nov 28 08:40:24 UTC 2025 - Alessio Biancalana <[email protected]> + +- Upgrade to Elixir 1.19.4: + * Changelog available at https://hexdocs.pm/elixir/1.19.4/changelog.html + +------------------------------------------------------------------- Old: ---- elixir-1.19.3-doc.zip elixir-1.19.3.tar.gz New: ---- elixir-1.19.4-doc.zip elixir-1.19.4.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ elixir.spec ++++++ --- /var/tmp/diff_new_pack.6ylWYQ/_old 2025-11-28 16:56:42.435822567 +0100 +++ /var/tmp/diff_new_pack.6ylWYQ/_new 2025-11-28 16:56:42.439822735 +0100 @@ -18,7 +18,7 @@ %define elixirdir %{_prefix}/lib/elixir Name: elixir -Version: 1.19.3 +Version: 1.19.4 Release: 0 Summary: Functional meta-programming aware language built atop Erlang License: Apache-2.0 ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.6ylWYQ/_old 2025-11-28 16:56:42.579828630 +0100 +++ /var/tmp/diff_new_pack.6ylWYQ/_new 2025-11-28 16:56:42.591829135 +0100 @@ -1,6 +1,6 @@ -mtime: 1763057439 -commit: 6b8813e4b10bf2f8abee141647b9fe693b54932a8c6eede84f1493b2169d6853 +mtime: 1764319463 +commit: a85fc128d402651f852787b6a2f57d17c2aff5586354e645033c644cea34e928 url: https://src.opensuse.org/erlang/elixir.git -revision: 6b8813e4b10bf2f8abee141647b9fe693b54932a8c6eede84f1493b2169d6853 +revision: a85fc128d402651f852787b6a2f57d17c2aff5586354e645033c644cea34e928 projectscmsync: https://src.opensuse.org/erlang/_ObsPrj.git ++++++ build.specials.obscpio ++++++ ++++++ build.specials.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.gitignore new/.gitignore --- old/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/.gitignore 2025-11-28 10:09:43.000000000 +0100 @@ -0,0 +1 @@ +.osc ++++++ elixir-1.19.3-doc.zip -> elixir-1.19.4-doc.zip ++++++ ++++ 72567 lines of diff (skipped) ++++++ elixir-1.19.3.tar.gz -> elixir-1.19.4.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/CHANGELOG.md new/elixir-1.19.4/CHANGELOG.md --- old/elixir-1.19.3/CHANGELOG.md 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/CHANGELOG.md 2025-11-27 16:50:26.000000000 +0100 @@ -234,6 +234,34 @@ This work was performed by [Jonatan Männchen](https://maennchen.dev) and sponsored by the [Erlang Ecosystem Foundation](https://erlef.org). +## v1.19.4 (2025-11-27) + +### 1. Enhancements + +#### Mix + + * [mix xref] Add `--min-cycle-label` to help projects adapt to the more precise `mix xref graph` reports in Elixir v1.19. In previous versions, Elixir would break a large compilation cycle into several smaller ones, and therefore developers would check for `--min-cycle-size` on CI. However, the issue is not the size of the cycle (it has no implication in the amount of compiled files), but how many compile-time dependencies (aka compile labels) in a cycle. The new option allows developers to filter on the label parameter + +### 2. Bug fixes + +#### Elixir + + * [File] Ensure `File.cp_r/3` reports non-existing destination properly (instead of source) + +#### ExUnit + + * [ExUnit] Fix formatter crash when diffing takes too long + * [ExUnit] Ensure parallel matches in `assert` propagate type information + +#### Logger + + * [Logger] Fix regression where formatter would crash when given chardata (the crash would happen when logging non-ASCII characters) + +#### Mix + + * [mix help] Ensure `app:APP` works when the project or its dependencies were not yet compiled + * [mix escript.build] Ensure the `hex` application can be included in escripts + ## v1.19.3 (2025-11-13) ### 1. Enhancements diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/VERSION new/elixir-1.19.4/VERSION --- old/elixir-1.19.3/VERSION 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/VERSION 2025-11-27 16:50:26.000000000 +0100 @@ -1 +1 @@ -1.19.3 +1.19.4 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/bin/elixir new/elixir-1.19.4/bin/elixir --- old/elixir-1.19.3/bin/elixir 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/bin/elixir 2025-11-27 16:50:26.000000000 +0100 @@ -6,7 +6,7 @@ set -e -ELIXIR_VERSION=1.19.3 +ELIXIR_VERSION=1.19.4 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <<USAGE >&2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/bin/elixir.bat new/elixir-1.19.4/bin/elixir.bat --- old/elixir-1.19.3/bin/elixir.bat 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/bin/elixir.bat 2025-11-27 16:50:26.000000000 +0100 @@ -4,7 +4,7 @@ :: SPDX-FileCopyrightText: 2021 The Elixir Team :: SPDX-FileCopyrightText: 2012 Plataformatec -set ELIXIR_VERSION=1.19.3 +set ELIXIR_VERSION=1.19.4 if ""%1""=="""" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/elixir/lib/code.ex new/elixir-1.19.4/lib/elixir/lib/code.ex --- old/elixir-1.19.3/lib/elixir/lib/code.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/elixir/lib/code.ex 2025-11-27 16:50:26.000000000 +0100 @@ -1090,7 +1090,7 @@ @doc since: "1.6.0" @spec format_string!(binary, [format_opt]) :: iodata def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do - line_length = Keyword.get(opts, :line_length, 98) + {line_length, opts} = Keyword.pop(opts, :line_length, 98) to_quoted_opts = [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/elixir/lib/file.ex new/elixir-1.19.4/lib/elixir/lib/file.ex --- old/elixir-1.19.3/lib/elixir/lib/file.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/elixir/lib/file.ex 2025-11-27 16:50:26.000000000 +0100 @@ -1224,7 +1224,13 @@ defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do case :elixir_utils.read_link_type(src) do {:ok, :regular} -> - do_cp_file(src, dest, on_conflict, acc) + case do_cp_file(src, dest, on_conflict, acc) do + # we don't have a way to make a distinction between a non-existing src + # or dest being a non-existing dir in the case of :enoent, + # but we already know that src exists here. + {:error, :enoent, _} -> {:error, :enoent, dest} + other -> other + end {:ok, :symlink} -> case :file.read_link(src) do diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/elixir/lib/macro.ex new/elixir-1.19.4/lib/elixir/lib/macro.ex --- old/elixir-1.19.3/lib/elixir/lib/macro.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/elixir/lib/macro.ex 2025-11-27 16:50:26.000000000 +0100 @@ -959,8 +959,9 @@ backwards compatibility purposes. In future releases, Elixir may introduce truly required struct - fields, and therefore only one of required or default will be - present. Your code should prepare for such scenario accordingly. + fields, the required field will be removed and default will be + present only if the field is optional. Your code should prepare + for such scenario accordingly. """ @doc since: "1.18.0" @spec struct_info!(module(), Macro.Env.t()) :: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/elixir/lib/module.ex new/elixir-1.19.4/lib/elixir/lib/module.ex --- old/elixir-1.19.3/lib/elixir/lib/module.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/elixir/lib/module.ex 2025-11-27 16:50:26.000000000 +0100 @@ -402,30 +402,21 @@ Accepts the function name (as an atom) of a function in the current module. The function must have an arity of 0 (no arguments). If the function does - not return `:ok`, the loading of the module will be aborted. - For example: + not return `:ok`, the loading of the module will be aborted. Its primary + use case is to load [NIFs](https://www.erlang.org/doc/man/erl_nif): defmodule MyModule do - @on_load :load_check + @on_load :load_external_code - def load_check do - if some_condition() do - :ok - else - :abort - end - end - - def some_condition do - false + def load_external_code do + :erlang.load_nif(~c"path/to/extension.so_or_dll") end end The function given to `on_load` should avoid calling functions from - other modules. If you must call functions in other modules and those - modules are defined within the same project, the called modules must - have the `@compile {:autoload, true}` annotation, so they are loaded - upfront (and not from within the `@on_load` callback). + other modules. This is because, when running a `mix release`, + `on_load` runs extremely early, before any application starts running, + and therefore even systems like the `Logger` and `IO` are not yet available. ### `@vsn` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/elixir/lib/record.ex new/elixir-1.19.4/lib/elixir/lib/record.ex --- old/elixir-1.19.3/lib/elixir/lib/record.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/elixir/lib/record.ex 2025-11-27 16:50:26.000000000 +0100 @@ -27,8 +27,9 @@ ## Types - Types can be defined for tuples with the `record/2` macro (only available in - typespecs). This macro will expand to a tuple as seen in the example below: + Types can be defined for tuples with the `record/2` construct (which is only + available in typespecs), with the record name as an atom and a keyword list + of fields and their types as argument: defmodule MyModule do require Record diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/elixir/lib/regex.ex new/elixir-1.19.4/lib/elixir/lib/regex.ex --- old/elixir-1.19.3/lib/elixir/lib/regex.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/elixir/lib/regex.ex 2025-11-27 16:50:26.000000000 +0100 @@ -101,7 +101,7 @@ * `:ungreedy` (U) - inverts the "greediness" of the regexp (the previous `r` option is deprecated in favor of `U`) - * `:export` (E) (since Elixir 1.20) - uses an exported pattern + * `:export` (E) (since Elixir 1.19.3) - uses an exported pattern which can be shared across nodes or through config, at the cost of a runtime overhead every time to re-import it every time it is executed. This modifier only has an effect starting on Erlang/OTP 28, and it is ignored @@ -597,6 +597,7 @@ end @compile {:inline, maybe_import_pattern: 1} + @compile {:no_warn_undefined, {:re, :import, 1}} defp maybe_import_pattern({:re_exported_pattern, _, _, _, _} = exported), do: :re.import(exported) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md new/elixir-1.19.4/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md --- old/elixir-1.19.3/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md 2025-11-27 16:50:26.000000000 +0100 @@ -228,7 +228,7 @@ ```elixir defp loop_acceptor(socket) do {:ok, client} = :gen_tcp.accept(socket) - {:ok, pid} = Task.Supervisor.start_child(KV.BucketSupervisor, fn -> serve(client) end) + {:ok, pid} = Task.Supervisor.start_child(KV.ServerSupervisor, fn -> serve(client) end) :ok = :gen_tcp.controlling_process(client, pid) loop_acceptor(socket) end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/elixir/test/elixir/file_test.exs new/elixir-1.19.4/lib/elixir/test/elixir/file_test.exs --- old/elixir-1.19.3/lib/elixir/test/elixir/file_test.exs 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/elixir/test/elixir/file_test.exs 2025-11-27 16:50:26.000000000 +0100 @@ -787,6 +787,18 @@ end end + test "cp_r! with file src and dest unknown" do + src = fixture_path("cp_r/a/1.txt") + dest = tmp_path("tmp/unknown/") + + message = + "could not copy recursively from #{inspect(src)} to #{inspect(dest)}. #{dest}: no such file or directory" + + assert_raise File.CopyError, message, fn -> + File.cp_r!(src, dest) + end + end + test "cp preserves mode" do File.mkdir_p!(tmp_path("tmp")) src = fixture_path("cp_mode") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/elixir/test/elixir/fixtures/dialyzer/regressions.ex new/elixir-1.19.4/lib/elixir/test/elixir/fixtures/dialyzer/regressions.ex --- old/elixir-1.19.3/lib/elixir/test/elixir/fixtures/dialyzer/regressions.ex 1970-01-01 01:00:00.000000000 +0100 +++ new/elixir-1.19.4/lib/elixir/test/elixir/fixtures/dialyzer/regressions.ex 2025-11-27 16:50:26.000000000 +0100 @@ -0,0 +1,14 @@ +defmodule Dialyzer.Regressions do + def io_inspect_opts do + IO.inspect(123, label: "foo", limit: :infinity) + end + + def format_opts do + Code.format_string!("", + line_length: 120, + force_do_end_blocks: true, + locals_without_parens: true, + migrate: true + ) + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/elixir/test/elixir/kernel/dialyzer_test.exs new/elixir-1.19.4/lib/elixir/test/elixir/kernel/dialyzer_test.exs --- old/elixir-1.19.3/lib/elixir/test/elixir/kernel/dialyzer_test.exs 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/elixir/test/elixir/kernel/dialyzer_test.exs 2025-11-27 16:50:26.000000000 +0100 @@ -185,6 +185,11 @@ assert_dialyze_no_warnings!(context) end + test "no warning in various non-regression cases", context do + copy_beam!(context, Dialyzer.Regressions) + assert_dialyze_no_warnings!(context) + end + defp copy_beam!(context, module) do name = "#{module}.beam" File.cp!(Path.join(context.base_dir, name), Path.join(context.outdir, name)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/ex_unit/lib/ex_unit/assertions.ex new/elixir-1.19.4/lib/ex_unit/lib/ex_unit/assertions.ex --- old/elixir-1.19.3/lib/ex_unit/lib/ex_unit/assertions.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/ex_unit/lib/ex_unit/assertions.ex 2025-11-27 16:50:26.000000000 +0100 @@ -160,6 +160,7 @@ end end + {left, right} = move_match(left, right) __match__(left, right, code, check, __CALLER__) end @@ -380,6 +381,12 @@ {ExUnit.AssertionError.no_value(), expr} end + defp move_match(left, {:=, meta, [middle, right]}), + do: move_match({:=, meta, [left, middle]}, right) + + defp move_match(left, right), + do: {left, right} + @doc false def __match__({:when, _, _} = left, right, _, _, _) do suggestion = @@ -409,7 +416,7 @@ case right do unquote(left) -> unquote(check) - unquote(mark_as_generated(vars)) + {unquote_splicing(mark_as_generated(vars))} _ -> left = unquote(Macro.escape(left)) @@ -427,7 +434,7 @@ quote do right = unquote(right) expr = unquote(code) - unquote(vars) = unquote(match_expr) + {unquote_splicing(vars)} = unquote(match_expr) right end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/ex_unit/lib/ex_unit/doc_test.ex new/elixir-1.19.4/lib/ex_unit/lib/ex_unit/doc_test.ex --- old/elixir-1.19.3/lib/ex_unit/lib/ex_unit/doc_test.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/ex_unit/lib/ex_unit/doc_test.ex 2025-11-27 16:50:26.000000000 +0100 @@ -964,7 +964,14 @@ @doc false defmacro __assert__({:=, _, [left, right]} = assertion) do + {left, right} = move_match(left, right) code = Macro.escape(assertion, prune_metadata: true) ExUnit.Assertions.__match__(left, right, code, :ok, __CALLER__) end + + defp move_match(left, {:=, meta, [middle, right]}), + do: move_match({:=, meta, [left, middle]}, right) + + defp move_match(left, right), + do: {left, right} end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/ex_unit/lib/ex_unit/formatter.ex new/elixir-1.19.4/lib/ex_unit/lib/ex_unit/formatter.ex --- old/elixir-1.19.3/lib/ex_unit/lib/ex_unit/formatter.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/ex_unit/lib/ex_unit/formatter.ex 2025-11-27 16:50:26.000000000 +0100 @@ -513,10 +513,15 @@ defp format_diff(left, right, context, formatter) do if has_value?(left) and has_value?(right) do - {result, env} = find_diff(left, right, context) - result = if formatter.(:diff_enabled?, false), do: result - hints = Enum.map(env.hints, &{:hint, format_hint(&1)}) - {result, hints} + case find_diff(left, right, context) do + {result, env} -> + result = if formatter.(:diff_enabled?, false), do: result + hints = Enum.map(env.hints, &{:hint, format_hint(&1)}) + {result, hints} + + nil -> + {nil, []} + end else {nil, []} end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/ex_unit/test/ex_unit/assertions_test.exs new/elixir-1.19.4/lib/ex_unit/test/ex_unit/assertions_test.exs --- old/elixir-1.19.3/lib/ex_unit/test/ex_unit/assertions_test.exs 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/ex_unit/test/ex_unit/assertions_test.exs 2025-11-27 16:50:26.000000000 +0100 @@ -246,6 +246,16 @@ end end + test "assert parallel match" do + assert %URI{} = uri = apply(URI, :parse, ["/foo"]) + # This should not warn + assert %URI{uri | path: "/bar"}.path == "/bar" + + assert uri = %URI{} = apply(URI, :parse, ["/foo"]) + # This should not warn + assert %URI{uri | path: "/bar"}.path == "/bar" + end + test "assert match with pinned variable" do a = 1 {2, 1} = assert {2, ^a} = Value.tuple() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/logger/lib/logger/formatter.ex new/elixir-1.19.4/lib/logger/lib/logger/formatter.ex --- old/elixir-1.19.3/lib/logger/lib/logger/formatter.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/logger/lib/logger/formatter.ex 2025-11-27 16:50:26.000000000 +0100 @@ -244,26 +244,38 @@ case if(is_function(enabled, 0), do: enabled.(), else: enabled) do true -> color = md[:ansi_color] || Map.fetch!(colors, level) - fragment = IO.ANSI.format_fragment(color, true) - data = IO.iodata_to_binary(data) - size = byte_size(data) - - cond do - :binary.at(data, size - 2) == ?\r and :binary.at(data, size - 1) == ?\n -> - [fragment, binary_part(data, 0, size - 2), IO.ANSI.reset() | "\r\n"] + [IO.ANSI.format_fragment(color, true), add_reset(data)] - :binary.at(data, size - 1) == ?\n -> - [fragment, binary_part(data, 0, size - 1), IO.ANSI.reset(), ?\n] + false -> + data + end + end + + defp add_reset(binary) when is_binary(binary) do + size = byte_size(binary) - true -> - [fragment, data | IO.ANSI.reset()] + cond do + binary == "" -> + IO.ANSI.reset() + + :binary.at(binary, size - 1) == ?\n -> + if size > 1 and :binary.at(binary, size - 2) == ?\r do + [binary_part(binary, 0, size - 2), IO.ANSI.reset() | "\r\n"] + else + [binary_part(binary, 0, size - 1), IO.ANSI.reset(), ?\n] end - false -> - data + true -> + [binary | IO.ANSI.reset()] end end + defp add_reset([?\r, ?\n]), do: [IO.ANSI.reset(), ?\r, ?\n] + defp add_reset([?\n]), do: [IO.ANSI.reset(), ?\n] + defp add_reset([last]), do: add_reset(last) + defp add_reset([h | t]), do: [h | add_reset(t)] + defp add_reset(rest), do: [rest | IO.ANSI.reset()] + @doc """ Formats the message of a log event. """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/logger/test/logger/formatter_test.exs new/elixir-1.19.4/lib/logger/test/logger/formatter_test.exs --- old/elixir-1.19.3/lib/logger/test/logger/formatter_test.exs 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/logger/test/logger/formatter_test.exs 2025-11-27 16:50:26.000000000 +0100 @@ -105,6 +105,14 @@ colors: [enabled: true] ) + assert %{level: :warning, msg: {:string, ""}, meta: %{}} + |> format(formatter) + |> IO.chardata_to_string() == "\e[33m\e[0m" + + assert %{level: :warning, msg: {:string, "s"}, meta: %{}} + |> format(formatter) + |> IO.chardata_to_string() == "\e[33ms\e[0m" + assert %{level: :warning, msg: {:string, "message"}, meta: %{}} |> format(formatter) |> IO.chardata_to_string() == "\e[33mmessage\e[0m" @@ -116,6 +124,18 @@ assert %{level: :warning, msg: {:string, "message\r\n"}, meta: %{}} |> format(formatter) |> IO.chardata_to_string() == "\e[33mmessage\e[0m\r\n" + + assert %{level: :warning, msg: {:string, [?é, ["message", [?\n]]]}, meta: %{}} + |> format(formatter) + |> IO.chardata_to_string() == "\e[33mémessage\e[0m\n" + + assert %{level: :warning, msg: {:string, [?é, "message", ?\r, ?\n]}, meta: %{}} + |> format(formatter) + |> IO.chardata_to_string() == "\e[33mémessage\e[0m\r\n" + + assert %{level: :warning, msg: {:string, [?é, "message" | "\r\n"]}, meta: %{}} + |> format(formatter) + |> IO.chardata_to_string() == "\e[33mémessage\e[0m\r\n" end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/mix/lib/mix/tasks/cmd.ex new/elixir-1.19.4/lib/mix/lib/mix/tasks/cmd.ex --- old/elixir-1.19.3/lib/mix/lib/mix/tasks/cmd.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/mix/lib/mix/tasks/cmd.ex 2025-11-27 16:50:26.000000000 +0100 @@ -26,7 +26,7 @@ Your shell will expand "lib/*" and then pass multiple arguments to `mix cmd`, which in turn passes them to `echo`. Note that, `mix cmd` by itself, does not perform any shell expansion. This means that, - if you invoke `mix cmd` programatically, as in: + if you invoke `mix cmd` programmatically, as in: Mix.Task.run("cmd", ["echo", "lib/*"]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/mix/lib/mix/tasks/escript.build.ex new/elixir-1.19.4/lib/mix/lib/mix/tasks/escript.build.ex --- old/elixir-1.19.3/lib/mix/lib/mix/tasks/escript.build.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/mix/lib/mix/tasks/escript.build.ex 2025-11-27 16:50:26.000000000 +0100 @@ -271,7 +271,8 @@ [] end - defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix] do + # We include hex in here as it is always an archive and it cannot be depended on + defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix, :hex] do [app] end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/mix/lib/mix/tasks/help.ex new/elixir-1.19.4/lib/mix/lib/mix/tasks/help.ex --- old/elixir-1.19.3/lib/mix/lib/mix/tasks/help.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/mix/lib/mix/tasks/help.ex 2025-11-27 16:50:26.000000000 +0100 @@ -112,6 +112,7 @@ def run(["app:" <> app]) do loadpaths!() + app = String.to_atom(app) # If the application is not available, attempt to load it from Erlang/Elixir @@ -119,7 +120,11 @@ try do Mix.ensure_application!(app) rescue - _ -> :ok + _ -> + # Otherwise, it may be a dep or the current project not yet compiled + if Mix.Project.get() do + Mix.Task.run("compile") + end end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/mix/lib/mix/tasks/xref.ex new/elixir-1.19.4/lib/mix/lib/mix/tasks/xref.ex --- old/elixir-1.19.3/lib/mix/lib/mix/tasks/xref.ex 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/mix/lib/mix/tasks/xref.ex 2025-11-27 16:50:26.000000000 +0100 @@ -44,12 +44,22 @@ └── lib/a.ex Because you have a compile-time dependency, any of the files `lib/a.ex`, - `lib/b.ex`, and `lib/c.ex` depend on will cause the whole cycle to - recompile. Therefore, your first priority to reduce compile times is - to remove such cycles. You can spot them by running: + `lib/b.ex`, and `lib/c.ex` depend on will cause `lib/a.ex` to recompile. + In other words, whenever you have a cycle, **a change to any file in the + cycle will cause all compile-time deps to recompile**. Therefore, your + first priority to reduce constant recompilations is to remove them. + You can spot them by running: $ mix xref graph --format cycles --label compile-connected + > #### Use the --label option + > + > The job of `mix xref` is to explore relationships between files + > and it is expected that most of your files are either directly + > or indirectly connected. For this reason, it is strongly advised + > to pass the `--label` option to filter the amount of data, + > specifically with `compile-connected` (or `compile`) as values. + Whenever you find a compile-time dependency, such as `lib/a.ex` pointing to `lib/b.ex`, there are two ways to remove them: @@ -208,13 +218,11 @@ * `--exclude` - path to exclude. Can be repeated to exclude multiple paths. * `--label` - only shows relationships with the given label. - The labels are "compile", "export" and "runtime". By default, the `--label` - option does not change how the graph is computed, it simply filters the - printed graph to show only relationships with the given label. However, - you can pass `--only-direct` to trim the graph to only the nodes that - have the direct relationship given by label. There is also a special - label called "compile-connected" that keeps only compile-time files with - at least one transitive dependency. See "Dependency types" section below. + The labels are "compile-connected", "compile", "export" and "runtime". + By default, the `--label` option does not change how the graph is computed, + it simply filters the printed graph to show only relationships with the given + label. However, you can pass `--only-direct` to trim the graph to only the + nodes that have the direct relationship given by label. * `--group` - provide comma-separated paths to consider as a group. Dependencies from and into multiple files of the group are considered a single dependency. @@ -239,6 +247,9 @@ * `--min-cycle-size` - controls the minimum cycle size on formats like `stats` and `cycles` + * `--min-cycle-label` - controls the minimum number of dependencies + with the given `--label` on a cycle + * `--format` - can be set to one of: * `pretty` - prints the graph to the terminal using Unicode characters. @@ -435,6 +446,7 @@ sink: :keep, source: :keep, min_cycle_size: :integer, + min_cycle_label: :integer, output: :string ] @@ -1218,9 +1230,22 @@ cycles end - # :compile_connected is the same + min_cycle_label = + if integer = opts[:min_cycle_label] do + if filter == :all do + Mix.raise("--min-cycle-label requires the --label option to be given") + end + + integer + else + 1 + end + + # :compile_connected is the same as :compile if cycle_fn = cycle_filter_fn(filter) do - Enum.filter(cycles, fn {_length, cycle} -> Enum.any?(cycle, cycle_fn) end) + Enum.filter(cycles, fn {_length, cycle} -> + Enum.count_until(cycle, cycle_fn, min_cycle_label) == min_cycle_label + end) else cycles end @@ -1269,11 +1294,35 @@ shell.info("#{length(cycles)} cycles found. Showing them in decreasing size:\n") for {length, cycle} <- cycles do - shell.info("Cycle of length #{length}:\n") + meta = + cycle + |> Enum.reduce({0, 0}, fn + {_, :compile}, {compile, export} -> {compile + 1, export} + {_, :export}, {compile, export} -> {compile, export + 1} + {_, _}, {compile, export} -> {compile, export} + end) + |> case do + {0, 0} -> + "" + + {compile, export} -> + info = + if(compile > 0, do: ["#{compile} compile"], else: []) ++ + if(export > 0, do: ["#{export} export"], else: []) + + " (" <> Enum.join(info, ", ") <> ")" + end + + shell.info("Cycle of length #{length}#{meta}:\n") for {node, type} <- cycle do - type = if type, do: " (#{type})", else: "" - shell.info(" " <> node <> type) + shell.info( + case type do + :compile -> [:red, " #{node} (compile)"] + :export -> " #{node} (export)" + _ -> " #{node}" + end + ) end shell.info("") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/mix/test/mix/tasks/help_test.exs new/elixir-1.19.4/lib/mix/test/mix/tasks/help_test.exs --- old/elixir-1.19.3/lib/mix/test/mix/tasks/help_test.exs 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/mix/test/mix/tasks/help_test.exs 2025-11-27 16:50:26.000000000 +0100 @@ -222,6 +222,41 @@ end) end + defmodule ExampleProject do + def project do + [ + app: :sample, + version: "0.1.0" + ] + end + end + + test "help app:APP for current project", context do + in_tmp(context.test, fn -> + Mix.Project.push(ExampleProject) + File.mkdir_p!("lib") + + File.write!("lib/example.ex", ~s''' + defmodule Example do + @moduledoc """ + This is an example module. + """ + end + ''') + + output = + capture_io(fn -> + Mix.Tasks.Help.run(["app:sample"]) + end) + + assert output =~ """ + # Example + + This is an example module. + """ + end) + end + test "help Elixir MODULE", context do in_tmp(context.test, fn -> output = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.19.3/lib/mix/test/mix/tasks/xref_test.exs new/elixir-1.19.4/lib/mix/test/mix/tasks/xref_test.exs --- old/elixir-1.19.3/lib/mix/test/mix/tasks/xref_test.exs 2025-11-13 17:33:43.000000000 +0100 +++ new/elixir-1.19.4/lib/mix/test/mix/tasks/xref_test.exs 2025-11-27 16:50:26.000000000 +0100 @@ -500,7 +500,7 @@ assert_graph(["--format", "cycles"], """ 1 cycles found. Showing them in decreasing size: - Cycle of length 2: + Cycle of length 2 (1 compile): lib/a.ex (compile) lib/b.ex @@ -512,7 +512,7 @@ assert_graph(["--format", "cycles", "--label", "compile"], """ 1 cycles found. Showing them in decreasing size: - Cycle of length 2: + Cycle of length 2 (1 compile): lib/a.ex (compile) lib/b.ex @@ -524,7 +524,7 @@ assert_graph(["--format", "cycles", "--label", "compile-connected"], """ 1 cycles found. Showing them in decreasing size: - Cycle of length 2: + Cycle of length 2 (1 compile): lib/a.ex (compile) lib/b.ex @@ -539,7 +539,7 @@ assert_graph(["--format", "cycles", "--fail-above", "0"], """ 1 cycles found. Showing them in decreasing size: - Cycle of length 2: + Cycle of length 2 (1 compile): lib/a.ex (compile) lib/b.ex @@ -548,11 +548,23 @@ end end + test "cycles with min_cycle_label matching actual length" do + assert_graph(["--format", "cycles", "--label", "compile", "--min-cycle-label", "1"], """ + 1 cycles found. Showing them in decreasing size: + + Cycle of length 2 (1 compile): + + lib/a.ex (compile) + lib/b.ex + + """) + end + test "cycles with min_cycle_size matching actual length" do assert_graph(["--format", "cycles", "--min-cycle-size", "2"], """ 1 cycles found. Showing them in decreasing size: - Cycle of length 2: + Cycle of length 2 (1 compile): lib/a.ex (compile) lib/b.ex @@ -560,6 +572,12 @@ """) end + test "cycles with min_cycle_label greater than actual length" do + assert_graph(["--format", "cycles", "--label", "compile", "--min-cycle-label", "2"], """ + No cycles found + """) + end + test "cycles with min_cycle_size greater than actual length" do assert_graph(["--format", "cycles", "--min-cycle-size", "3"], """ No cycles found
