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

[Feature request] Support package snapshot #298 #370

Merged
merged 34 commits into from
Feb 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
910b2db
feat(snapshot)
c3n21 Nov 22, 2021
daefee9
fix(snapshot)
c3n21 Nov 24, 2021
68c7212
fix(rollback)
c3n21 Nov 24, 2021
c1b6a25
fix(rollback): working
c3n21 Nov 24, 2021
6acfee3
modify(packer): packer.snapshot and packer.rollback use f-args
c3n21 Dec 1, 2021
f223a9b
refactor(packer)
c3n21 Dec 1, 2021
dced289
refactor: packer, snapshot, git
c3n21 Dec 2, 2021
71536aa
fix(git): revert_to
c3n21 Dec 3, 2021
3ea6b95
refactor(packer, snapshot)
c3n21 Dec 4, 2021
6832007
test(snapshot): fixed tests
c3n21 Dec 4, 2021
7b8aa42
test(snapshot): fix
c3n21 Dec 24, 2021
438c07a
doc(snapshot): update snapshot feature docs
c3n21 Dec 24, 2021
fd95c13
refactor(snapshot): rename PackerRollback, PackerDelete commands
c3n21 Jan 22, 2022
608cf8f
docs(snapshot): update commands name
c3n21 Jan 22, 2022
3554a8a
refactor(snapshot): add notify on snapshot/rollback complete
c3n21 Jan 22, 2022
2b198ff
refactor(snapshot): using JSON for snapshot files
c3n21 Feb 1, 2022
a32c180
fix(snapshot): automatically create stdpath('cache')/packer.nvim
c3n21 Feb 2, 2022
3183262
test(snapshot): WIP
c3n21 Feb 2, 2022
4e86269
fix(snapshot)
c3n21 Feb 2, 2022
0088f0f
test(snapshot): WIP
c3n21 Feb 5, 2022
26dbf38
test(snapshot): cleanup and fix
c3n21 Feb 5, 2022
2024edd
fix(snapshot): can't rollback if snapshot is older than repo
c3n21 Feb 5, 2022
63db854
Merge branch 'master' into snapshot
wbthomason Feb 14, 2022
abde04d
Update Neovim versions for testing
wbthomason Feb 14, 2022
e36f8ea
Attempt to fix snapshot tests by using proper async function
wbthomason Feb 14, 2022
431a9dc
refactor(snapshot)
c3n21 Feb 14, 2022
85f5afe
refactor(snapshot)
c3n21 Feb 14, 2022
5ead3e4
chore: format with stylua
github-actions[bot] Feb 14, 2022
f840ff9
refactor(snapshot)
c3n21 Feb 15, 2022
c42345b
chore: format with stylua
github-actions[bot] Feb 15, 2022
31c7cf2
refactor(snapshot)
c3n21 Feb 15, 2022
89d9b1a
refactor(snapshot)
c3n21 Feb 15, 2022
b4f45e7
refactor(snapshot): improved async logic of rollback
c3n21 Feb 15, 2022
4864933
build
c3n21 Feb 15, 2022
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: true
matrix:
neovim_branch: ['v0.5.0', 'master']
neovim_branch: ['v0.5.0', 'v0.6.1', 'master']
runs-on: ubuntu-latest
env:
NEOVIM_BRANCH: ${{ matrix.neovim_branch }}
Expand Down
23 changes: 21 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
FROM archlinux
RUN pacman -Syu --noconfirm && pacman -S --noconfirm git neovim python
FROM archlinux:base-devel
WORKDIR /setup
RUN pacman -Sy git neovim python --noconfirm
RUN useradd -m test

USER test
RUN git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim
RUN mkdir -p /home/test/.cache/nvim/packer.nvim
RUN touch /home/test/.cache/nvim/packer.nvim/test_completion{,1,2,3}

USER test
RUN mkdir -p /home/test/.local/share/nvim/site/pack/packer/start/packer.nvim/
WORKDIR /home/test/.local/share/nvim/site/pack/packer/start/packer.nvim/
COPY . ./

USER root
RUN chmod 777 -R /home/test/.local/share/nvim/site/pack/packer/start/packer.nvim
RUN touch /home/test/.cache/nvim/packer.nvim/not_writeable

USER test
ENTRYPOINT make test
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ test:
fi; \
nvim --headless --noplugin -u tests/minimal.vim \
-c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal.vim'}"
run:
docker build . -t neovim-stable:latest && docker run --rm -it --entrypoint bash neovim-stable:latest
run-test:
docker build . -t neovim-stable:latest && docker run --rm neovim-stable:latest
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ default configuration values (and structure of the configuration table) are:
```lua
{
ensure_dependencies = true, -- Should packer install plugin dependencies?
snapshot = nil, -- Name of the snapshot you would like to load at startup
snapshot_path = join_paths(stdpath 'cache', 'packer.nvim'), -- Default save directory for snapshots
package_root = util.join_paths(vim.fn.stdpath('data'), 'site', 'pack'),
compile_path = util.join_paths(vim.fn.stdpath('config'), 'plugin', 'packer_compiled.lua'),
plugin_package = 'packer', -- The default package for plugins
Expand Down Expand Up @@ -518,6 +520,9 @@ plugins":
- `packer.clean()`: Remove any disabled or no longer managed plugins
- `packer.sync(plugins)`: Perform a `clean` followed by an `update`
- `packer.compile(path)`: Compile lazy-loader code and save to `path`.
- `packer.snapshot(snapshot_name, ...)`: Creates a snapshot file that will live under `config.snapshot_path/<snapshot_name>`. If `snapshot_name` is an absolute path, then that will be the location where the snapshot will be taken. Optionally, a list of plugins name can be provided to selectively choose the plugins to snapshot.
- `packer.rollback(snapshot_name, ...)`: Rollback plugins status a snapshot file that will live under `config.snapshot_path/<snapshot_name>`. If `snapshot_name` is an absolute path, then that will be the location where the snapshot will be taken. Optionally, a list of plugins name can be provided to selectively choose which plugins to revert.
- `packer.delete(snapshot_name)`: Deletes a snapshot file under `config.snapshot_path/<snapshot_name>`. If `snapshot_name` is an absolute path, then that will be the location where the snapshot will be deleted.

### Extending `packer`
You can add custom key handlers to `packer` by calling `packer.set_handler(name, func)` where `name`
Expand Down
21 changes: 21 additions & 0 deletions doc/packer.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ FEATURES *packer-intro-features*
- Uses jobs for async installation
- Support for `git` tags, branches, revisions, submodules
- Support for local plugins
- Support for saving/restoring snapshots for plugin versions (`git` only)

==============================================================================
QUICKSTART *packer-intro-quickstart*
Expand Down Expand Up @@ -126,6 +127,14 @@ Perform `PackerUpdate` and then `PackerCompile`.
`PackerLoad` *packer-commands-load*
Loads opt plugin immediately

`PackerSnapshot` *packer-commands-snapshot*
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be fixed for the other commands too (not as part of this PR), but we might want to specify the arguments accepted for the snapshot related commands (i.e. the snapshot file).

Snapshots your plugins to a file

`PackerSnapshotDelete` *packer-commands-delete*
Deletes a snapshot

`PackerSnapshotRollback` *packer-commands-rollback*
Rolls back plugins' commit specified by the snapshot
==============================================================================
USAGE *packer-usage*

Expand Down Expand Up @@ -534,6 +543,18 @@ It can be invoked with no arguments or with a list of plugin names to update.
These plugin names must already be managed by `packer` via a call to
|packer.use()|.

snapshot(snapshot_name, ...) *packer.snapshot()*
`snapshot` takes the rev of all the installed plugins and serializes them into a Lua table which will be saved under `config.snapshot_path` (which is the directory that will hold all the snapshots files) as `config.snapshot_path/<snapshot_name>` or an absolute path provided by the users.
Optionally plugins name can be specified so that only those plugins will be
snapshotted.
Snapshot files can be loaded manually via `dofile` which will return a table with the plugins name as keys the commit short hash as value.

delete(snapshot_name) *packer.delete()*
`delete` deletes a snapshot given the name or the absolute path.

rollback(snapshot_name, ...) *packer.rollback()*
`rollback` reverts all plugins or only the specified as extra arguments to the commit specified in the snapshot file

use() *packer.use()*
`use` allows you to add one or more plugins to the managed set. It can be
invoked as follows:
Expand Down
146 changes: 146 additions & 0 deletions lua/packer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ local stdpath = vim.fn.stdpath
local packer = {}
local config_defaults = {
ensure_dependencies = true,
snapshot = nil,
snapshot_path = join_paths(stdpath 'cache', 'packer.nvim'),
package_root = join_paths(stdpath 'data', 'site', 'pack'),
compile_path = join_paths(stdpath 'config', 'plugin', 'packer_compiled.lua'),
plugin_package = 'packer',
Expand Down Expand Up @@ -38,6 +40,7 @@ local config_defaults = {
get_bodies = 'log --color=never --pretty=format:"===COMMIT_START===%h%n%s===BODY_START===%b" --no-show-signature HEAD@{1}...HEAD',
submodules = 'submodule update --init --recursive --progress',
revert = 'reset --hard HEAD@{1}',
revert_to = 'reset --hard %s --',
c3n21 marked this conversation as resolved.
Show resolved Hide resolved
tags_expand_fmt = 'tag -l %s --sort -version:refname',
},
depth = 1,
Expand Down Expand Up @@ -86,6 +89,7 @@ local configurable_modules = {
update = false,
luarocks = false,
log = false,
snapshot = false,
}

local function require_and_configure(module_name)
Expand Down Expand Up @@ -122,9 +126,16 @@ packer.init = function(user_config)
if not config.disable_commands then
packer.make_commands()
end

if vim.fn.mkdir(config.snapshot_path, 'p') ~= 1 then
vim.notify("Couldn't create " .. config.snapshot_path, vim.log.levels.WARN)
end
end

packer.make_commands = function()
vim.cmd [[command! -nargs=+ -complete=customlist,v:lua.require'packer.snapshot'.completion.create PackerSnapshot lua require('packer').snapshot(<f-args>)]]
vim.cmd [[command! -nargs=+ -complete=customlist,v:lua.require'packer.snapshot'.completion.rollback PackerSnapshotRollback lua require('packer').rollback(<f-args>)]]
vim.cmd [[command! -nargs=+ -complete=customlist,v:lua.require'packer.snapshot'.completion.snapshot PackerSnapshotDelete lua require('packer.snapshot').delete(<f-args>)]]
vim.cmd [[command! -nargs=* -complete=customlist,v:lua.require'packer'.plugin_complete PackerInstall lua require('packer').install(<f-args>)]]
vim.cmd [[command! -nargs=* -complete=customlist,v:lua.require'packer'.plugin_complete PackerUpdate lua require('packer').update(<f-args>)]]
vim.cmd [[command! -nargs=* -complete=customlist,v:lua.require'packer'.plugin_complete PackerSync lua require('packer').sync(<f-args>)]]
Expand Down Expand Up @@ -796,6 +807,136 @@ packer.plugin_complete = function(lead, _, _)
return completion_list
end

---Snapshots installed plugins
---@param snapshot_name string absolute path or just a snapshot name
packer.snapshot = function(snapshot_name, ...)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I think we should either print an error or choose a sane default if snapshot_name is nil.

local async = require('packer.async').sync
local await = require('packer.async').wait
local snapshot = require 'packer.snapshot'
local log = require_and_configure 'log'
local args = { ... }
snapshot_name = snapshot_name or require('os').date '%Y-%m-%d'
local snapshot_path = vim.fn.expand(snapshot_name)

local fmt = string.format
log.debug(fmt('Taking snapshots of currently installed plugins to %s...', snapshot_name))
if vim.fn.fnamemodify(snapshot_name, ':p') ~= snapshot_path then -- is not absolute path
if config.snapshot_path == nil then
vim.notify('config.snapshot_path is not set', vim.log.levels.WARN)
return
else
snapshot_path = util.join_paths(config.snapshot_path, snapshot_path) -- set to default path
end
end

manage_all_plugins()

local target_plugins = plugins
if next(args) ~= nil then -- provided extra args
target_plugins = vim.tbl_filter( -- filter plugins
function(plugin)
for k, plugin_shortname in pairs(args) do
if plugin_shortname == plugin.short_name then
args[k] = nil
return true
end
end
return false
end,
plugins
)
end

local write_snapshot = true

if vim.fn.filereadable(snapshot_path) == 1 then
vim.ui.select(
{ 'Replace', 'Cancel' },
{ prompt = fmt("Do you want to replace '%s'?", snapshot_path) },
function(_, idx)
write_snapshot = idx == 1
end
)
end

async(function()
if write_snapshot then
await(snapshot.create(snapshot_path, target_plugins))
:map_ok(function(ok)
vim.notify(ok.message, vim.log.levels.INFO, { title = 'packer.nvim' })

if next(ok.failed) then
vim.notify("Couldn't snapshot " .. vim.inspect(ok.failed), vim.log.levels.WARN, { title = 'packer.nvim' })
end
end)
:map_err(function(err)
vim.notify(err.message, vim.log.levels.WARN, { title = 'packer.nvim' })
end)
end
end)()
end

---Instantly rolls back plugins to a previous state specified by `snapshot_name`
---If `snapshot_name` doesn't exist an error will be displayed
c3n21 marked this conversation as resolved.
Show resolved Hide resolved
---@param snapshot_name string @name of the snapshot or the absolute path to the snapshot
---@vararg string @ if provided, the only plugins to be rolled back,
---otherwise all the plugins will be rolled back
packer.rollback = function(snapshot_name, ...)
local args = { ... }
local a = require 'packer.async'
local async = a.sync
local await = a.wait
local wait_all = a.wait_all
local snapshot = require 'packer.snapshot'
local log = require_and_configure 'log'
local fmt = string.format

async(function()
manage_all_plugins()

local snapshot_path = vim.loop.fs_realpath(util.join_paths(config.snapshot_path, snapshot_name))
or vim.loop.fs_realpath(snapshot_name)

if snapshot_path == nil then
local warn = fmt("Snapshot '%s' is wrong or doesn't exist", snapshot_name)
log.warn(warn)
vim.notify(warn, vim.log.levels.WARN)
return
end

local target_plugins = plugins

if next(args) ~= nil then -- provided extra args
target_plugins = vim.tbl_filter(function(plugin)
for _, plugin_sname in pairs(args) do
if plugin_sname == plugin.short_name then
return true
end
end
return false
end, plugins)
end

await(snapshot.rollback(snapshot_path, target_plugins))
:map_ok(function (ok)
await(a.main)
vim.notify('Rollback to "' .. snapshot_path .. '" completed', vim.log.levels.INFO, { title = 'packer.nvim' })
if next(ok.failed) then
vim.notify(
"Couldn't rollback " .. vim.inspect(ok.failed),
vim.log.levels.INFO, { title = 'packer.nvim' }
)
end
end)
:map_err(function (err)
await(a.main)
vim.notify(err, vim.log.levels.ERROR, { title = 'packer.nvim' })
end)

packer.on_complete()
c3n21 marked this conversation as resolved.
Show resolved Hide resolved
end)()
end

packer.config = config

--- Convenience function for simple setup
Expand Down Expand Up @@ -852,6 +993,11 @@ packer.startup = function(spec)
end
end

require_and_configure 'snapshot' -- initialize snapshot config
if config.snapshot ~= nil then
packer.rollback(config.snapshot)
end

return packer
end

Expand Down
50 changes: 50 additions & 0 deletions lua/packer/plugin_types/git.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ git.cfg = function(_config)
ensure_git_env()
end

---Resets a git repo `dest` to `commit`
---@param dest string @ path to the local git repo
---@param commit string @ commit hash
---@return function @ async function
local function reset(dest, commit)
local reset_cmd = fmt(config.exec_cmd .. config.subcommands.revert_to, commit)
local opts = { capture_output = true, cwd = dest, options = { env = git.job_env } }
return async(function()
return await(jobs.run(reset_cmd, opts))
end)
end

local handle_checkouts = function(plugin, dest, disp)
local plugin_name = util.get_plugin_full_name(plugin)
return async(function()
Expand Down Expand Up @@ -151,6 +163,28 @@ local handle_checkouts = function(plugin, dest, disp)
end)
end

local get_rev = function(plugin)
local plugin_name = util.get_plugin_full_name(plugin)

local rev_cmd = config.exec_cmd .. config.subcommands.get_rev

return async(function()
local rev = await(
jobs.run(rev_cmd, { cwd = plugin.install_path, options = { env = git.job_env }, capture_output = true })
)
:map_ok(function(ok)
local _, r = next(ok.output.data.stdout)
return r
end)
:map_err(function(err)
local _, msg = fmt('%s: %s', plugin_name, next(err.output.data.stderr))
return msg
end)

return rev
end)
end

git.setup = function(plugin)
local plugin_name = util.get_plugin_full_name(plugin)
local install_to = plugin.install_path
Expand Down Expand Up @@ -481,6 +515,22 @@ git.setup = function(plugin)
end)()
return r
end

---Reset the plugin to `commit`
---@param commit string
plugin.revert_to = function(commit)
assert(type(commit) == 'string', fmt("commit: string expected but '%s' provided", type(commit)))
return async(function()
require('packer.log').debug(fmt("Reverting '%s' to commit '%s'", plugin.name, commit))
return await(reset(install_to, commit))
end)
end

---Returns HEAD's short hash
---@return string
plugin.get_rev = function()
return get_rev(plugin)
end
end

return git
Loading