diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 83f37b7d..34a874a2 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -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 }} diff --git a/Dockerfile b/Dockerfile index 538a32c0..4e67c024 100644 --- a/Dockerfile +++ b/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 diff --git a/Makefile b/Makefile index 7386e7b0..9a696deb 100644 --- a/Makefile +++ b/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 diff --git a/README.md b/README.md index 7439d1f7..4254fbfd 100644 --- a/README.md +++ b/README.md @@ -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/`. 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/`. 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/`. 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` diff --git a/doc/packer.txt b/doc/packer.txt index 204a0d90..e0b09972 100644 --- a/doc/packer.txt +++ b/doc/packer.txt @@ -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/` 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: diff --git a/lua/packer.lua b/lua/packer.lua index 5898e3fe..140d92cc 100644 --- a/lua/packer.lua +++ b/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()]] + vim.cmd [[command! -nargs=+ -complete=customlist,v:lua.require'packer.snapshot'.completion.rollback PackerSnapshotRollback lua require('packer').rollback()]] + vim.cmd [[command! -nargs=+ -complete=customlist,v:lua.require'packer.snapshot'.completion.snapshot PackerSnapshotDelete lua require('packer.snapshot').delete()]] vim.cmd [[command! -nargs=* -complete=customlist,v:lua.require'packer'.plugin_complete PackerInstall lua require('packer').install()]] vim.cmd [[command! -nargs=* -complete=customlist,v:lua.require'packer'.plugin_complete PackerUpdate lua require('packer').update()]] vim.cmd [[command! -nargs=* -complete=customlist,v:lua.require'packer'.plugin_complete PackerSync lua require('packer').sync()]] @@ -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, ...) + 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 @@ -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 diff --git a/lua/packer/plugin_types/git.lua b/lua/packer/plugin_types/git.lua index c7790575..b45ca445 100644 --- a/lua/packer/plugin_types/git.lua +++ b/lua/packer/plugin_types/git.lua @@ -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 diff --git a/lua/packer/snapshot.lua b/lua/packer/snapshot.lua new file mode 100644 index 00000000..57ee40e5 --- /dev/null +++ b/lua/packer/snapshot.lua @@ -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, completed : table}} +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[] +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, failed: table}} +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 diff --git a/tests/snapshot_spec.lua b/tests/snapshot_spec.lua new file mode 100644 index 00000000..8b397759 --- /dev/null +++ b/tests/snapshot_spec.lua @@ -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 = '', 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)