Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add project compat entry viewing & editing #2702

Merged
merged 1 commit into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TAB>` 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
Expand All @@ -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
Expand Down
125 changes: 123 additions & 2 deletions src/API.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 #
########
Expand Down
43 changes: 42 additions & 1 deletion src/Operations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 13 additions & 1 deletion src/Pkg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
18 changes: 17 additions & 1 deletion src/REPLMode/command_declarations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -351,13 +351,15 @@ 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",
:help => md"""
[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
Expand All @@ -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.
Expand All @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions src/REPLMode/completions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 8 additions & 2 deletions src/Versions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions test/new.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down
2 changes: 2 additions & 0 deletions test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down