Thanks @c3n21! * feat(snapshot) Add snapshotting feature * fix(snapshot) Set (stdpath 'cache')/packer as default snapshot path location * fix(rollback) Fixed revert = nil * fix(rollback): working * modify(packer): packer.snapshot and packer.rollback use f-args * refactor(packer) * snapshot_path changed to stdpath("cache")/packer.nvim * using vim.loop.fs for snapshot_complete * refactor: packer, snapshot, git * using vim.loop functions to write files * refactor of snapshot as a module * refactor of async function of git module * fix(git): revert_to * now it works * refactor(packer, snapshot) * renamed snapshot() to create() * moved snapshot completion functions into `snapshot` module needs `config.snapshot_path` * set default snapshot to nil * initialize snapshot module in packer.startup to avoid errors with `config` table not being available * test(snapshot): fixed tests * test(snapshot): fix * doc(snapshot): update snapshot feature docs * refactor(snapshot): rename PackerRollback, PackerDelete commands Renamed commands: * PackerRollback -> PackerSnapshotRollback * PackerDelete -> PackerSnapshotDelete * docs(snapshot): update commands name * refactor(snapshot): add notify on snapshot/rollback complete * refactor(snapshot): using JSON for snapshot files * fix(snapshot): automatically create stdpath('cache')/packer.nvim * test(snapshot): WIP * fix(snapshot) Fixed bug in PackerSnapshotRollback that when provided a snapshot name it will first look in the root of `packer.nvim`, and then in `config.snapshot_path` which can cause some issues if there are other files named the same as the snapshots inside the root of `packer.nvim` * test(snapshot): WIP * test(snapshot): cleanup and fix * fix(snapshot): can't rollback if snapshot is older than repo * snapshot.rollback() will first run `git fetch --depth 999999 --progress` to fetch the history, and then rollback * Update Neovim versions for testing * Attempt to fix snapshot tests by using proper async function * refactor(snapshot) * refactor(snapshot) * chore: format with stylua * refactor(snapshot) * when taking a snapshot, if `snapshot_name` exists the user will be asked if they want to overwrite the existing snapshot * better error handling * packer.snapshot() will default to '%Y-%m-%d' if no snapshot name is provided * chore: format with stylua * refactor(snapshot) * when taking a snapshot, if `snapshot_name` exists the user will be asked if they want to overwrite the existing snapshot * better error handling * packer.snapshot() will default to '%Y-%m-%d' if no snapshot name is provided * refactor(snapshot) snapshot.delete is not async * refactor(snapshot): improved async logic of rollback * build Dockerfile: * added non-root user `test` * added entrypoint to automatically run `make test` inside archlinux container * copy packer.nvim directly into the container Makefile: * added run and run-tests to quickly run tests inside the container Co-authored-by: Wil Thomason <wbthomason@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
963cb58c3d
commit
40cbd5c88f
|
@ -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 }}
|
||||
|
|
23
Dockerfile
23
Dockerfile
|
@ -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
Makefile
4
Makefile
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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`
|
||||
|
|
|
@ -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*
|
||||
|
@ -126,6 +127,14 @@ Perform `PackerUpdate` and then `PackerCompile`.
|
|||
`PackerLoad` *packer-commands-load*
|
||||
Loads opt plugin immediately
|
||||
|
||||
`PackerSnapshot` *packer-commands-snapshot*
|
||||
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*
|
||||
|
||||
|
@ -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:
|
||||
|
|
146
lua/packer.lua
146
lua/packer.lua
|
@ -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',
|
||||
|
@ -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 --',
|
||||
tags_expand_fmt = 'tag -l %s --sort -version:refname',
|
||||
},
|
||||
depth = 1,
|
||||
|
@ -86,6 +89,7 @@ local configurable_modules = {
|
|||
update = false,
|
||||
luarocks = false,
|
||||
log = false,
|
||||
snapshot = false,
|
||||
}
|
||||
|
||||
local function require_and_configure(module_name)
|
||||
|
@ -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>)]]
|
||||
|
@ -798,6 +809,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, ...)
|
||||
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
|
||||
---@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()
|
||||
end)()
|
||||
end
|
||||
|
||||
packer.config = config
|
||||
|
||||
--- Convenience function for simple setup
|
||||
|
@ -854,6 +995,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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
local a = require 'packer.async'
|
||||
local util = require 'packer.util'
|
||||
local log = require 'packer.log'
|
||||
local plugin_utils = require 'packer.plugin_utils'
|
||||
local plugin_complete = require('packer').plugin_complete
|
||||
local result = require 'packer.result'
|
||||
local async = a.sync
|
||||
local await = a.wait
|
||||
local fmt = string.format
|
||||
|
||||
local config = {}
|
||||
|
||||
local snapshot = {
|
||||
completion = {},
|
||||
}
|
||||
|
||||
snapshot.cfg = function(_config)
|
||||
config = _config
|
||||
end
|
||||
|
||||
--- Completion for listing snapshots in `config.snapshot_path`
|
||||
--- Intended to provide completion for PackerSnapshotDelete command
|
||||
snapshot.completion.snapshot = function(lead, cmdline, pos)
|
||||
local completion_list = {}
|
||||
if config.snapshot_path == nil then
|
||||
return completion_list
|
||||
end
|
||||
|
||||
local dir = vim.loop.fs_opendir(config.snapshot_path)
|
||||
|
||||
if dir ~= nil then
|
||||
local res = vim.loop.fs_readdir(dir)
|
||||
while res ~= nil do
|
||||
for _, entry in ipairs(res) do
|
||||
if entry.type == 'file' and vim.startswith(entry.name, lead) then
|
||||
completion_list[#completion_list + 1] = entry.name
|
||||
end
|
||||
end
|
||||
|
||||
res = vim.loop.fs_readdir(dir)
|
||||
end
|
||||
end
|
||||
|
||||
vim.loop.fs_closedir(dir)
|
||||
return completion_list
|
||||
end
|
||||
|
||||
--- Completion for listing single plugins before taking snapshot
|
||||
--- Intended to provide completion for PackerSnapshot command
|
||||
snapshot.completion.create = function(lead, cmdline, pos)
|
||||
local cmd_args = (vim.fn.split(cmdline, ' '))
|
||||
|
||||
if #cmd_args > 1 then
|
||||
return plugin_complete(lead, cmdline, pos)
|
||||
end
|
||||
|
||||
return {}
|
||||
end
|
||||
|
||||
--- Completion for listing snapshots in `config.snapshot_path` and single plugins after
|
||||
--- the first argument is provided
|
||||
--- Intended to provide completion for PackerSnapshotRollback command
|
||||
snapshot.completion.rollback = function(lead, cmdline, pos)
|
||||
local cmd_args = vim.split(cmdline, ' ')
|
||||
|
||||
if #cmd_args > 2 then
|
||||
return plugin_complete(lead)
|
||||
else
|
||||
return snapshot.completion.snapshot(lead, cmdline, pos)
|
||||
end
|
||||
end
|
||||
|
||||
--- Creates a with with `completed` and `failed` keys, each containing a map with plugin name as key and commit hash/error as value
|
||||
--- @param plugins list
|
||||
--- @return { ok: { failed : table<string, string>, completed : table<string, string>}}
|
||||
local function generate_snapshot(plugins)
|
||||
local completed = {}
|
||||
local failed = {}
|
||||
local opt, start = plugin_utils.list_installed_plugins()
|
||||
local installed = vim.tbl_extend('error', start, opt)
|
||||
|
||||
plugins = vim.tbl_filter(function(plugin)
|
||||
if installed[plugin.install_path] and plugin.type == plugin_utils.git_plugin_type then -- this plugin is installed
|
||||
return plugin
|
||||
end
|
||||
end, plugins)
|
||||
return async(function()
|
||||
for _, plugin in pairs(plugins) do
|
||||
local rev = await(plugin.get_rev())
|
||||
|
||||
if rev.err then
|
||||
failed[plugin.short_name] = fmt(
|
||||
"Snapshotting %s failed because of error '%s'",
|
||||
plugin.short_name,
|
||||
vim.inspect(rev.err)
|
||||
)
|
||||
else
|
||||
completed[plugin.short_name] = { commit = rev.ok }
|
||||
end
|
||||
end
|
||||
|
||||
return result.ok { failed = failed, completed = completed }
|
||||
end)
|
||||
end
|
||||
|
||||
---Serializes a table of git-plugins with `short_name` as table key and another
|
||||
---table with `commit`; the serialized tables will be written in the path `snapshot_path`
|
||||
---provided, if there is already a snapshot it will be overwritten
|
||||
---Snapshotting work only with `plugin_utils.git_plugin_type` type of plugins,
|
||||
---other will be ignored.
|
||||
---@param snapshot_path string realpath for snapshot file
|
||||
---@param plugins table<string, any>[]
|
||||
snapshot.create = function(snapshot_path, plugins)
|
||||
assert(type(snapshot_path) == 'string', fmt("filename needs to be a string but '%s' provided", type(snapshot_path)))
|
||||
assert(type(plugins) == 'table', fmt("plugins needs to be an array but '%s' provided", type(plugins)))
|
||||
return async(function()
|
||||
local commits = await(generate_snapshot(plugins))
|
||||
|
||||
await(a.main)
|
||||
local snapshot_content = vim.fn.json_encode(commits.ok.completed)
|
||||
|
||||
local status, res = pcall(function()
|
||||
return vim.fn.writefile({ snapshot_content }, snapshot_path) == 0
|
||||
end)
|
||||
|
||||
if status and res then
|
||||
return result.ok {
|
||||
message = fmt("Snapshot '%s' complete", snapshot_path),
|
||||
completed = commits.ok.completed,
|
||||
failed = commits.ok.failed,
|
||||
}
|
||||
else
|
||||
return result.err { message = fmt("Error on creation of snapshot '%s': '%s'", snapshot_path, res) }
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function fetch(plugin)
|
||||
local git = require 'packer.plugin_types.git'
|
||||
local opts = { capture_output = true, cwd = plugin.install_path, options = { env = git.job_env } }
|
||||
|
||||
return async(function ()
|
||||
return await(require('packer.jobs').run('git ' .. config.git.subcommands.fetch, opts))
|
||||
end)
|
||||
end
|
||||
|
||||
---Rollbacks `plugins` to the hash specified in `snapshot_path` if exists.
|
||||
---It automatically runs `git fetch --depth 999999 --progress` to retrieve the history
|
||||
---@param snapshot_path string @ realpath to the snapshot file
|
||||
---@param plugins list @ of `plugin_utils.git_plugin_type` type of plugins
|
||||
---@return {ok: {completed: table<string, string>, failed: table<string, string[]>}}
|
||||
snapshot.rollback = function(snapshot_path, plugins)
|
||||
assert(type(snapshot_path) == "string", "snapshot_path: expected string but got " .. type(snapshot_path))
|
||||
assert(type(plugins) == "table", "plugins: expected table but got " .. type(snapshot_path))
|
||||
log.debug('Rolling back to ' .. snapshot_path)
|
||||
local content = vim.fn.readfile(snapshot_path)
|
||||
---@type string
|
||||
local plugins_snapshot = vim.fn.json_decode(content)
|
||||
if plugins_snapshot == nil then -- not valid snapshot file
|
||||
return result.err(fmt("Couldn't load '%s' file", snapshot_path))
|
||||
end
|
||||
|
||||
local completed = {}
|
||||
local failed = {}
|
||||
|
||||
return async(function ()
|
||||
for _, plugin in pairs(plugins) do
|
||||
local function err_handler(err)
|
||||
failed[plugin.short_name] = failed[plugin.short_name] or {}
|
||||
failed[plugin.short_name][#failed[plugin.short_name]+1] = err
|
||||
end
|
||||
|
||||
if plugins_snapshot[plugin.short_name] then
|
||||
local commit = plugins_snapshot[plugin.short_name].commit
|
||||
if commit ~= nil then
|
||||
await(fetch(plugin))
|
||||
:map_err(err_handler)
|
||||
:and_then(await, plugin.revert_to(commit))
|
||||
:map_ok(function (ok)
|
||||
completed[plugin.short_name] = ok
|
||||
end)
|
||||
:map_err(err_handler)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result.ok {completed = completed, failed = failed}
|
||||
end)
|
||||
end
|
||||
|
||||
---Deletes the snapshot provided
|
||||
---@param snapshot_name string absolute path or just a snapshot name
|
||||
snapshot.delete = function(snapshot_name)
|
||||
assert(type(snapshot_name) == 'string', fmt('Expected string, got %s', type(snapshot_name)))
|
||||
---@type string
|
||||
local snapshot_path = vim.loop.fs_realpath(snapshot_name)
|
||||
or vim.loop.fs_realpath(util.join_paths(config.snapshot_path, 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, { title = 'packer.nvim' })
|
||||
return
|
||||
end
|
||||
|
||||
log.debug('Deleting ' .. snapshot_path)
|
||||
if vim.loop.fs_unlink(snapshot_path) then
|
||||
local info = 'Deleted ' .. snapshot_path
|
||||
log.info(info)
|
||||
vim.notify(info, vim.log.levels.INFO, { title = 'packer.nvim' })
|
||||
else
|
||||
local warn = "Couldn't delete " .. snapshot_path
|
||||
log.warn(warn)
|
||||
vim.notify(warn, vim.log.levels.WARN, { title = 'packer.nvim' })
|
||||
end
|
||||
end
|
||||
|
||||
return snapshot
|
|
@ -0,0 +1,165 @@
|
|||
local before_each = require('plenary.busted').before_each
|
||||
local a = require 'plenary.async_lib.tests'
|
||||
local util = require 'packer.util'
|
||||
local mocked_plugin_utils = require 'packer.plugin_utils'
|
||||
local log = require 'packer.log'
|
||||
local async = require('packer.async').sync
|
||||
local await = require('packer.async').wait
|
||||
local wait_all = require('packer.async').wait_all
|
||||
local main = require('packer.async').main
|
||||
local packer = require 'packer'
|
||||
local jobs = require 'packer.jobs'
|
||||
local git = require 'packer.plugin_types.git'
|
||||
local join_paths = util.join_paths
|
||||
local stdpath = vim.fn.stdpath
|
||||
local fmt = string.format
|
||||
|
||||
local config = {
|
||||
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',
|
||||
max_jobs = nil,
|
||||
auto_clean = true,
|
||||
compile_on_sync = true,
|
||||
disable_commands = false,
|
||||
opt_default = false,
|
||||
transitive_opt = true,
|
||||
transitive_disable = true,
|
||||
auto_reload_compiled = true,
|
||||
git = {
|
||||
mark_breaking_changes = true,
|
||||
cmd = 'git',
|
||||
subcommands = {
|
||||
update = 'pull --ff-only --progress --rebase=false',
|
||||
install = 'clone --depth %i --no-single-branch --progress',
|
||||
fetch = 'fetch --depth 999999 --progress',
|
||||
checkout = 'checkout %s --',
|
||||
update_branch = 'merge --ff-only @{u}',
|
||||
current_branch = 'rev-parse --abbrev-ref HEAD',
|
||||
diff = 'log --color=never --pretty=format:FMT --no-show-signature HEAD@{1}...HEAD',
|
||||
diff_fmt = '%%h %%s (%%cr)',
|
||||
git_diff_fmt = 'show --no-color --pretty=medium %s',
|
||||
get_rev = 'rev-parse --short HEAD',
|
||||
get_header = 'log --color=never --pretty=format:FMT --no-show-signature HEAD -n 1',
|
||||
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 --',
|
||||
},
|
||||
depth = 1,
|
||||
clone_timeout = 60,
|
||||
default_url_format = 'https://github.com/%s.git',
|
||||
},
|
||||
display = {
|
||||
non_interactive = false,
|
||||
open_fn = nil,
|
||||
open_cmd = '65vnew',
|
||||
working_sym = '⟳',
|
||||
error_sym = '✗',
|
||||
done_sym = '✓',
|
||||
removed_sym = '-',
|
||||
moved_sym = '→',
|
||||
header_sym = '━',
|
||||
header_lines = 2,
|
||||
title = 'packer.nvim',
|
||||
show_all_info = true,
|
||||
prompt_border = 'double',
|
||||
keybindings = { quit = 'q', toggle_info = '<CR>', diff = 'd', prompt_revert = 'r' },
|
||||
},
|
||||
luarocks = { python_cmd = 'python' },
|
||||
log = { level = 'trace' },
|
||||
profile = { enable = false },
|
||||
}
|
||||
|
||||
git.cfg(config)
|
||||
|
||||
--[[ For testing purposes the spec file is made up so that when running `packer`
|
||||
it could manage itself as if it was in `~/.local/share/nvim/site/pack/packer/start/` --]]
|
||||
local install_path = vim.fn.getcwd()
|
||||
|
||||
mocked_plugin_utils.list_installed_plugins = function()
|
||||
return { [install_path] = true }, {}
|
||||
end
|
||||
|
||||
local old_require = _G.require
|
||||
|
||||
_G.require = function(modname)
|
||||
if modname == 'plugin_utils' then
|
||||
return mocked_plugin_utils
|
||||
end
|
||||
|
||||
return old_require(modname)
|
||||
end
|
||||
|
||||
local spec = { 'wbthomason/packer.nvim' }
|
||||
|
||||
local snapshotted_plugins = {}
|
||||
a.describe('Packer testing ', function()
|
||||
local snapshot_name = 'test'
|
||||
local test_path = join_paths(config.snapshot_path, snapshot_name)
|
||||
local snapshot = require 'packer.snapshot'
|
||||
snapshot.cfg(config)
|
||||
|
||||
before_each(function()
|
||||
packer.reset()
|
||||
packer.init(config)
|
||||
packer.use(spec)
|
||||
packer.__manage_all()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
spec = { 'wbthomason/packer.nvim' }
|
||||
spec.install_path = install_path
|
||||
end)
|
||||
|
||||
a.describe('snapshot.create()', function()
|
||||
a.it(fmt("create snapshot in '%s'", test_path), function()
|
||||
local result = await(snapshot.create(test_path, { spec }))
|
||||
local stat = vim.loop.fs_stat(test_path)
|
||||
assert.truthy(stat)
|
||||
end)
|
||||
|
||||
a.it("checking if snapshot content corresponds to plugins'", function()
|
||||
async(function()
|
||||
local file_content = vim.fn.readfile(test_path)
|
||||
snapshotted_plugins = vim.fn.json_decode(file_content)
|
||||
local expected_rev = await(spec.get_rev())
|
||||
assert.are.equals(expected_rev.ok, snapshotted_plugins['packer.nvim'].commit)
|
||||
end)()
|
||||
end)
|
||||
end)
|
||||
|
||||
a.describe('packer.delete()', function()
|
||||
a.it(fmt("delete '%s' snapshot", snapshot_name), function()
|
||||
snapshot.delete(snapshot_name)
|
||||
local stat = vim.loop.fs_stat(test_path)
|
||||
assert.falsy(stat)
|
||||
end)
|
||||
end)
|
||||
|
||||
a.describe('packer.rollback()', function()
|
||||
local rollback_snapshot_name = 'rollback_test'
|
||||
local rollback_test_path = join_paths(config.snapshot_path, rollback_snapshot_name)
|
||||
local prev_commit_cmd = 'git rev-parse --short HEAD~5'
|
||||
|
||||
local opts = { capture_output = true, cwd = spec.install_path, options = { env = git.job_env } }
|
||||
|
||||
a.it("restore 'packer' to the commit hash HEAD~5", function()
|
||||
async(function()
|
||||
local r = await(jobs.run(prev_commit_cmd, opts))
|
||||
_, snapshotted_plugins['packer.nvim'].commit = next(r.ok.output.data.stdout)
|
||||
await(main)
|
||||
local encoded_json = vim.fn.json_encode(snapshotted_plugins)
|
||||
vim.fn.writefile({ encoded_json }, rollback_test_path)
|
||||
-- wait_all(snapshot.rollback(rollback_test_path, {spec}))
|
||||
local job = snapshot.rollback(rollback_test_path, { spec })
|
||||
await(job[1])
|
||||
local rev = await(spec.get_rev())
|
||||
assert.are.equals(snapshotted_plugins['packer.nvim'].commit, rev.ok)
|
||||
end)()
|
||||
end)
|
||||
end)
|
||||
end)
|
Loading…
Reference in New Issue