[Feature request] Support package snapshot #298 (#370)

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:
c3n21 2022-02-25 17:27:15 +01:00 committed by GitHub
parent 963cb58c3d
commit 40cbd5c88f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 631 additions and 3 deletions

View File

@ -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 }}

View File

@ -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

View File

@ -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

View File

@ -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`

View File

@ -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:

View File

@ -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

View File

@ -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

218
lua/packer/snapshot.lua Normal file
View File

@ -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

165
tests/snapshot_spec.lua Normal file
View File

@ -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)