diff --git a/CHANGELOG.md b/CHANGELOG.md index f1069a211e..87a487f052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,12 @@ Pkg v1.8 Release Notes - New `outdated::Bool` kwarg to `Pkg.status` (`--outdated` or `-o` in the REPL mode) to show information about packages not at the latest version. -- Pkg now only tries to download pacakges from the package server in case the +- New `compat::Bool` kwarg to `Pkg.status` (`--compat` or `-c` in the REPL mode) to show any [compat] + entries in the Project.toml. +- New `pkg> compat` (and `Pkg.compat`) mode for setting Project compat entries. Provides an interactive editor + via `pkg> compat`, or direct entry manipulation via `pkg> Foo 0.4,0.5` which can load current entries via tab-completion. + i.e. `pkg> compat Fo` autocompletes to `pkg> Foo 0.4,0.5` so that the existing entry can be edited. +- Pkg now only tries to download packages from the package server in case the server tracks a registry that contains the package. Pkg v1.7 Release Notes @@ -14,7 +19,7 @@ Pkg v1.7 Release Notes Julia 1.6.2 is compatible with the new format. - Registries downloaded from the Pkg Server (not git) are no longer uncompressed into files but instead read directly from the compressed tarball into memory. This improves performance on filesystems which do not handle a large number of files well. To turn this feature off, set the environment variable `JULIA_PKG_UNPACK_REGISTRY=true`. -- It is now possible to use an external `git` executable instead of the default libgit2 library for +- It is now possible to use an external `git` executable instead of the default libgit2 library for the downloads that happen via the Git protocol by setting the environment variable `JULIA_PKG_USE_CLI_GIT=true`. - Registries downloaded from the Pkg Server (not git) is now assumed to be immutable. Manual changes to their files might not be picked up by a running Pkg session. - The number of packags precompiled in parallel are now limited to 16 unless the diff --git a/src/API.jl b/src/API.jl index b6406b44e8..d4b2bc99e1 100644 --- a/src/API.jl +++ b/src/API.jl @@ -9,6 +9,7 @@ using Dates import LibGit2 import Logging using Serialization +using REPL.TerminalMenus import ..depots, ..depots1, ..logdir, ..devdir, ..printpkgstyle import ..Operations, ..GitTools, ..Pkg, ..Registry @@ -1522,8 +1523,14 @@ end @deprecate status(mode::PackageMode) status(mode=mode) -function status(ctx::Context, pkgs::Vector{PackageSpec}; diff::Bool=false, mode=PKGMODE_PROJECT, outdated::Bool=false, io::IO=stdout, kwargs...) - Operations.status(ctx.env, ctx.registries, pkgs; mode, git_diff=diff, io, outdated) +function status(ctx::Context, pkgs::Vector{PackageSpec}; diff::Bool=false, mode=PKGMODE_PROJECT, outdated::Bool=false, compat::Bool=false, io::IO=stdout, kwargs...) + if compat + diff && pkgerror("Compat status has no `diff` mode") + outdated && pkgerror("Compat status has no `outdated` mode") + Operations.print_compat(ctx, pkgs; io) + else + Operations.status(ctx.env, ctx.registries, pkgs; mode, git_diff=diff, io, outdated) + end return nothing end @@ -1608,6 +1615,120 @@ function activate(f::Function, new_project::AbstractString) end end +function compat(ctx::Context; io = nothing) + io = something(io, ctx.io) + can_fancyprint(io) || pkgerror("Pkg.compat cannot be run interactively in this terminal") + printpkgstyle(io, :Compat, pathrepr(ctx.env.project_file)) + longest_dep_len = max(5, length.(collect(keys(ctx.env.project.deps)))...) + opt_strs = String[] + opt_pkgs = String[] + compat_str = Operations.get_compat_str(ctx.env.project, "julia") + push!(opt_strs, Operations.compat_line(io, "julia", nothing, compat_str, longest_dep_len, indent = "")) + push!(opt_pkgs, "julia") + for (dep, uuid) in ctx.env.project.deps + compat_str = Operations.get_compat_str(ctx.env.project, dep) + push!(opt_strs, Operations.compat_line(io, dep, uuid, compat_str, longest_dep_len, indent = "")) + push!(opt_pkgs, dep) + end + menu = TerminalMenus.RadioMenu(opt_strs, pagesize=length(opt_strs)) + choice = try + TerminalMenus.request(" Select an entry to edit:", menu) + catch err + if err isa InterruptException # if ^C is entered + println(io) + return false + end + rethrow() + end + choice == -1 && return false + dep = opt_pkgs[choice] + current_compat_str = something(Operations.get_compat_str(ctx.env.project, dep), "") + resp = try + prompt = " Edit compat entry for $(dep):" + print(io, prompt) + buffer = current_compat_str + cursor = length(buffer) + start_pos = length(prompt) + 2 + move_start = "\e[$(start_pos)G" + clear_to_end = "\e[0J" + ccall(:jl_tty_set_mode, Int32, (Ptr{Cvoid},Int32), stdin.handle, true) + while true + print(io, move_start, clear_to_end, buffer, "\e[$(start_pos + cursor)G") + inp = TerminalMenus._readkey(stdin) + if inp == '\r' # Carriage return + println(io) + break + elseif inp == '\x03' # cltr-C + println(io) + return + elseif inp == TerminalMenus.ARROW_RIGHT + cursor = min(length(buffer), cursor + 1) + elseif inp == TerminalMenus.ARROW_LEFT + cursor = max(0, cursor - 1) + elseif inp == TerminalMenus.HOME_KEY + cursor = (0) + elseif inp == TerminalMenus.END_KEY + cursor = length(buffer) + elseif inp == TerminalMenus.DEL_KEY + if cursor == 0 + buffer = buffer[2:end] + elseif cursor < length(buffer) + buffer = buffer[1:cursor] * buffer[(cursor + 2):end] + end + elseif inp isa TerminalMenus.Key + # ignore all other escaped (multi-byte) keys + elseif inp == '\x7f' # backspace + if cursor == 1 + buffer = buffer[2:end] + elseif cursor == length(buffer) + buffer = buffer[1:end - 1] + elseif cursor > 0 + buffer = buffer[1:(cursor-1)] * buffer[(cursor + 1):end] + else + continue + end + cursor -= 1 + else + if cursor == 0 + buffer = inp * buffer + elseif cursor == length(buffer) + buffer = buffer * inp + else + buffer = buffer[1:cursor] * inp * buffer[(cursor + 1):end] + end + cursor += 1 + end + end + buffer + finally + ccall(:jl_tty_set_mode, Int32, (Ptr{Cvoid},Int32), stdin.handle, false) + end + new_entry = strip(resp) + compat(ctx, dep, string(new_entry)) + return +end +function compat(ctx::Context, pkg::String, compat_str::Union{Nothing,String}; io = nothing, kwargs...) + io = something(io, ctx.io) + pkg = pkg == "Julia" ? "julia" : pkg + isnothing(compat_str) || (compat_str = string(strip(compat_str, '"'))) + if haskey(ctx.env.project.deps, pkg) || pkg == "julia" + success = Operations.set_compat(ctx.env.project, pkg, isnothing(compat_str) ? nothing : isempty(compat_str) ? nothing : compat_str) + success === false && pkgerror("invalid compat version specifier \"$(compat_str)\"") + write_env(ctx.env) + if isnothing(compat_str) || isempty(compat_str) + printpkgstyle(io, :Compat, "entry removed for $(pkg)") + else + printpkgstyle(io, :Compat, "entry set:\n $(pkg) = $(repr(compat_str))") + end + return + else + pkgerror("No package named $pkg in current Project") + end +end +compat(pkg::String; kwargs...) = compat(pkg, nothing; kwargs...) +compat(pkg::String, compat_str::Union{Nothing,String}; kwargs...) = compat(Context(), pkg, compat_str; kwargs...) +compat(;kwargs...) = compat(Context(); kwargs...) + ######## # Undo # ######## diff --git a/src/Operations.jl b/src/Operations.jl index b9a8175f09..29b1b9678a 100644 --- a/src/Operations.jl +++ b/src/Operations.jl @@ -174,7 +174,14 @@ end get_compat(proj::Project, name::String) = haskey(proj.compat, name) ? proj.compat[name].val : Types.VersionSpec() get_compat_str(proj::Project, name::String) = haskey(proj.compat, name) ? proj.compat[name].str : nothing function set_compat(proj::Project, name::String, compat::String) - proj.compat[name] = Types.Compat(Types.semver_spec(compat), compat) + semverspec = Types.semver_spec(compat, throw = false) + isnothing(semverspec) && return false + proj.compat[name] = Types.Compat(semverspec, compat) + return true +end +function set_compat(proj::Project, name::String, ::Nothing) + delete!(proj.compat, name) + return true end function reset_all_compat!(proj::Project) @@ -1987,6 +1994,40 @@ function status(env::EnvCache, registries::Vector{Registry.RegistryInstance}, pk end end +function compat_line(io, pkg, uuid, compat_str, longest_dep_len; indent = " ") + iob = IOBuffer() + ioc = IOContext(iob, :color => get(io, :color, false)) + if isnothing(uuid) + print(ioc, "$indent ") + else + printstyled(ioc, "$indent[", string(uuid)[1:8], "] "; color = :light_black) + end + print(ioc, rpad(pkg, longest_dep_len)) + if isnothing(compat_str) + printstyled(ioc, " none"; color = :light_black) + else + print(ioc, " ", compat_str) + end + return String(take!(iob)) +end + +function print_compat(ctx::Context, pkgs_in::Vector{PackageSpec} = PackageSpec[]; io = nothing) + io = something(io, ctx.io) + printpkgstyle(io, :Compat, pathrepr(ctx.env.project_file)) + names = [pkg.name for pkg in pkgs_in] + pkgs = isempty(pkgs_in) ? ctx.env.project.deps : filter(pkg -> in(first(pkg), names), ctx.env.project.deps) + add_julia = isempty(pkgs_in) || any(p->p.name == "julia", pkgs_in) + longest_dep_len = isempty(pkgs) ? length("julia") : max(reduce(max, map(length, collect(keys(pkgs)))), length("julia")) + if add_julia + println(io, compat_line(io, "julia", nothing, get_compat_str(ctx.env.project, "julia"), longest_dep_len)) + end + for (dep, uuid) in pkgs + println(io, compat_line(io, dep, uuid, get_compat_str(ctx.env.project, dep), longest_dep_len)) + end +end +print_compat(pkg::String; kwargs...) = print_compat(Context(), pkg; kwargs...) +print_compat(; kwargs...) = print_compat(Context(); kwargs...) + function apply_force_latest_compatible_version!(ctx::Types.Context; target_name = nothing, allow_earlier_backwards_compatible_versions::Bool = true) diff --git a/src/Pkg.jl b/src/Pkg.jl index 6225a4097c..8876a674a4 100644 --- a/src/Pkg.jl +++ b/src/Pkg.jl @@ -428,6 +428,18 @@ status as a Julia object instead of printing it. """ const status = API.status +""" + Pkg.compat() + +Interactively edit the [compat] entries within the current Project. + + Pkg.compat(pkg::String, compat::String) + +Set the [compat] string for the given package within the current Project. + +See [`Compatibility`](@ref) for more information on the project [compat] section. +""" +const compat = API.compat """ Pkg.activate([s::String]; shared::Bool=false, io::IO=stderr) @@ -440,7 +452,7 @@ The logic for what path is activated is as follows: * If `shared` is `true`, the first existing environment named `s` from the depots in the depot stack will be activated. If no such environment exists, create and activate that environment in the first depot. - * If `temp` is `true` this will create and activate a temporary enviroment which will + * If `temp` is `true` this will create and activate a temporary environment which will be deleted when the julia process is exited. * If `s` is an existing path, then activate the environment at that path. * If `s` is a package in the current project and `s` is tracking a path, then diff --git a/src/REPLMode/command_declarations.jl b/src/REPLMode/command_declarations.jl index 58fbd332cd..575d4d13bd 100644 --- a/src/REPLMode/command_declarations.jl +++ b/src/REPLMode/command_declarations.jl @@ -351,6 +351,7 @@ PSA[:name => "status", PSA[:name => "manifest", :short_name => "m", :api => :mode => PKGMODE_MANIFEST], PSA[:name => "diff", :short_name => "d", :api => :diff => true], PSA[:name => "outdated", :short_name => "o", :api => :outdated => true], + PSA[:name => "compat", :short_name => "c", :api => :compat => true], ], :completions => complete_installed_packages, :description => "summarize contents of and changes to environment", @@ -358,6 +359,7 @@ PSA[:name => "status", [st|status] [-d|--diff] [-o|--outdated] [pkgs...] [st|status] [-d|--diff] [-o|--outdated] [-p|--project] [pkgs...] [st|status] [-d|--diff] [-o|--outdated] [-m|--manifest] [pkgs...] + [st|status] [-c|--compat] [pkgs...] Show the status of the current environment. In `--project` mode (default), the status of the project file is summarized. In `--manifest` mode the output also @@ -367,6 +369,7 @@ The `--diff` option will, if the environment is in a git repository, limit the output to the difference as compared to the last git commit. The `--outdated` option in addition show if some packages are not at their latest version and what packages are holding them back. +The `--compat` option alone shows project compat entries. !!! compat "Julia 1.1" `pkg> status` with package arguments requires at least Julia 1.1. @@ -376,7 +379,20 @@ and what packages are holding them back. is the default for environments in git repositories. !!! compat "Julia 1.8" - The `--outdated` option requires at least Julia 1.8. + The `--outdated` and `--compat` options require at least Julia 1.8. +""", +], +PSA[:name => "compat", + :api => API.compat, + :arg_count => 0 => 2, + :completions => complete_installed_packages_and_compat, + :description => "edit compat entries in the current Project", + :help => md""" + compat [pkg] [compat_string] + +Edit project [compat] entries directly, or via an interactive menu by not specifying any arguments. +When directly editing use tab to complete the package name and any existing compat entry. +Specifying a package with a blank compat entry will remove the entry. """, ], PSA[:name => "gc", diff --git a/src/REPLMode/completions.jl b/src/REPLMode/completions.jl index 11248a6a2d..62e5bdb91b 100644 --- a/src/REPLMode/completions.jl +++ b/src/REPLMode/completions.jl @@ -106,6 +106,18 @@ function complete_installed_packages(options, partial) unique!([entry.name for (uuid, entry) in env.manifest]) end +function complete_installed_packages_and_compat(options, partial) + env = try EnvCache() + catch err + err isa PkgError || rethrow() + return String[] + end + return map(vcat(collect(keys(env.project.deps)), "julia")) do d + compat_str = Operations.get_compat_str(env.project, d) + isnothing(compat_str) ? d : string(d, " ", compat_str) + end +end + function complete_add_dev(options, partial, i1, i2) comps, idx, _ = complete_local_dir(partial, i1, i2) if occursin(Base.Filesystem.path_separator_re, partial) diff --git a/src/Versions.jl b/src/Versions.jl index 72cfeeb142..5bc23a27f5 100644 --- a/src/Versions.jl +++ b/src/Versions.jl @@ -286,7 +286,7 @@ Base.show(io::IO, s::VersionSpec) = print(io, "VersionSpec(\"", s, "\")") # Semver notation # ################### -function semver_spec(s::String) +function semver_spec(s::String; throw = true) ranges = VersionRange[] for ver in strip.(split(strip(s), ',')) range = nothing @@ -298,7 +298,13 @@ function semver_spec(s::String) break end end - found_match || error("invalid version specifier: $s") + if !found_match + if throw + error("invalid version specifier: \"$s\"") + else + return nothing + end + end push!(ranges, range) end return VersionSpec(ranges) diff --git a/test/new.jl b/test/new.jl index 8eb45d77ce..f68ff920aa 100644 --- a/test/new.jl +++ b/test/new.jl @@ -2184,6 +2184,41 @@ end end end +# +# # compat +# +@testset "Pkg.compat" begin + # State changes + isolate(loaded_depot=true) do + Pkg.add("Example") + iob = IOBuffer() + Pkg.status(compat=true, io = iob) + output = String(take!(iob)) + @test occursin(r"Compat `.+Project.toml`", output) + @test occursin(r"\[7876af07\] *Example *none", output) + @test occursin(r"julia *none", output) + + Pkg.compat("Example", "0.2,0.3") + @test Pkg.Operations.get_compat_str(Pkg.Types.Context().env.project, "Example") == "0.2,0.3" + Pkg.status(compat=true, io = iob) + output = String(take!(iob)) + @test occursin(r"Compat `.+Project.toml`", output) + @test occursin(r"\[7876af07\] *Example *0.2,0.3", output) + @test occursin(r"julia *none", output) + + Pkg.compat("Example", nothing) + Pkg.compat("julia", "1.8") + @test Pkg.Operations.get_compat_str(Pkg.Types.Context().env.project, "Example") == nothing + @test Pkg.Operations.get_compat_str(Pkg.Types.Context().env.project, "julia") == "1.8" + Pkg.status(compat=true, io = iob) + output = String(take!(iob)) + @test occursin(r"Compat `.+Project.toml`", output) + @test occursin(r"\[7876af07\] *Example *none", output) + @test occursin(r"julia *1.8", output) + end +end + + # # # Caching # diff --git a/test/repl.jl b/test/repl.jl index 466d3310dc..dfb6e9cce1 100644 --- a/test/repl.jl +++ b/test/repl.jl @@ -635,6 +635,8 @@ end status 7876af07-990d-54b4-ab0e-23690620f79a status Example Random status -m Example + status --outdated + status --compat """ # --diff option @test_logs (:warn, r"diff option only available") pkg"status --diff"