Ver código fonte

feat(plugins): add support for packer snapshots (#2351)

kylo252 3 anos atrás
pai
commit
08d3df2cd3

+ 3 - 3
Makefile

@@ -16,9 +16,9 @@ uninstall:
 	@echo starting LunarVim uninstaller
 	bash ./utils/installer/uninstall.sh
 
-generate_plugins_sha:
-	@echo generating core-plugins latest SHA list
-	lvim --headless -c 'lua require("lvim.utils.git").generate_plugins_sha("latest-sha.lua")' -c 'qall'
+generate_new_lockfile:
+	@echo generating core-plugins latest lockfile
+	bash ./utils/ci/generate_new_lockfile.sh
 
 lint: lint-lua lint-sh
 

+ 59 - 34
lua/lvim/plugin-loader.lua

@@ -1,33 +1,25 @@
 local plugin_loader = {}
 
-local in_headless = #vim.api.nvim_list_uis() == 0
-
 local utils = require "lvim.utils"
 local Log = require "lvim.core.log"
+local join_paths = utils.join_paths
+
 -- we need to reuse this outside of init()
-local compile_path = get_config_dir() .. "/plugin/packer_compiled.lua"
+local compile_path = join_paths(get_config_dir(), "plugin", "packer_compiled.lua")
+local snapshot_path = join_paths(get_cache_dir(), "snapshots")
+local default_snapshot = join_paths(get_lvim_base_dir(), "snapshots", "default.json")
 
 function plugin_loader.init(opts)
   opts = opts or {}
 
-  local install_path = opts.install_path or vim.fn.stdpath "data" .. "/site/pack/packer/start/packer.nvim"
-  local package_root = opts.package_root or vim.fn.stdpath "data" .. "/site/pack"
+  local install_path = opts.install_path
+    or join_paths(vim.fn.stdpath "data", "site", "pack", "packer", "start", "packer.nvim")
 
-  if vim.fn.empty(vim.fn.glob(install_path)) > 0 then
-    vim.fn.system { "git", "clone", "--depth", "1", "https://github.com/wbthomason/packer.nvim", install_path }
-    vim.cmd "packadd packer.nvim"
-  end
-
-  local log_level = in_headless and "debug" or "warn"
-  if lvim.log and lvim.log.level then
-    log_level = lvim.log.level
-  end
-
-  local _, packer = pcall(require, "packer")
-  packer.init {
-    package_root = package_root,
+  local init_opts = {
+    package_root = opts.package_root or join_paths(vim.fn.stdpath "data", "site", "pack"),
     compile_path = compile_path,
-    log = { level = log_level },
+    snapshot_path = snapshot_path,
+    log = { level = "warn" },
     git = {
       clone_timeout = 300,
       subcommands = {
@@ -35,7 +27,6 @@ function plugin_loader.init(opts)
         fetch = "fetch --no-tags --no-recurse-submodules --update-shallow --progress",
       },
     },
-    max_jobs = 50,
     display = {
       open_fn = function()
         return require("packer.util").float { border = "rounded" }
@@ -43,9 +34,28 @@ function plugin_loader.init(opts)
     },
   }
 
-  if not in_headless then
+  local in_headless = #vim.api.nvim_list_uis() == 0
+  if in_headless then
+    init_opts.display = nil
+
+    -- NOTE: `lvim.log.level` may not be loaded from the user's config yet
+    init_opts.log.level = lvim.log and lvim.log.level or "info"
+  else
     vim.cmd [[autocmd User PackerComplete lua require('lvim.utils.hooks').run_on_packer_complete()]]
   end
+
+  if vim.fn.empty(vim.fn.glob(install_path)) > 0 then
+    vim.fn.system { "git", "clone", "--depth", "1", "https://github.com/wbthomason/packer.nvim", install_path }
+    vim.cmd "packadd packer.nvim"
+    -- IMPORTANT: we only set this the very first time to avoid constantly triggering the rollback function
+    -- https://github.com/wbthomason/packer.nvim/blob/c576ab3f1488ee86d60fd340d01ade08dcabd256/lua/packer.lua#L998-L995
+    init_opts.snapshot = default_snapshot
+  end
+
+  local status_ok, packer = pcall(require, "packer")
+  if status_ok then
+    packer.init(init_opts)
+  end
 end
 
 -- packer expects a space separated list
@@ -81,6 +91,7 @@ function plugin_loader.load(configurations)
     return
   end
   local status_ok, _ = xpcall(function()
+    packer.reset()
     packer.startup(function(use)
       for _, plugins in ipairs(configurations) do
         for _, plugin in ipairs(plugins) do
@@ -88,9 +99,6 @@ function plugin_loader.load(configurations)
         end
       end
     end)
-    -- colorscheme must get called after plugins are loaded or it will break new installs.
-    vim.g.colors_name = lvim.colorscheme
-    vim.cmd("colorscheme " .. lvim.colorscheme)
   end, debug.traceback)
   if not status_ok then
     Log:warn "problems detected while loading plugins' configurations"
@@ -102,21 +110,38 @@ function plugin_loader.get_core_plugins()
   local list = {}
   local plugins = require "lvim.plugins"
   for _, item in pairs(plugins) do
-    table.insert(list, item[1]:match "/(%S*)")
+    if not item.disable then
+      table.insert(list, item[1]:match "/(%S*)")
+    end
   end
   return list
 end
 
-function plugin_loader.sync_core_plugins()
+function plugin_loader.sync_core_plugins(opts)
+  opts = opts or {}
+  Log:debug(string.format("Syncing core plugins with snapshot file [%s]", default_snapshot))
+  local packer = require "packer"
+  local a = require "packer.async"
+  local async = a.sync
+  local await = a.wait
+  local main = a.main
   local core_plugins = plugin_loader.get_core_plugins()
-  Log:trace(string.format("Syncing core plugins: [%q]", table.concat(core_plugins, ", ")))
-  pcall_packer_command("sync", core_plugins)
-end
-
-function plugin_loader.ensure_installed()
-  local all_plugins = _G.packer_plugins or plugin_loader.get_core_plugins()
-  Log:trace(string.format("Syncing core plugins: [%q]", table.concat(all_plugins, ", ")))
-  pcall_packer_command("install", all_plugins)
+  async(function()
+    await(packer.rollback(default_snapshot, unpack(core_plugins)))
+      :map_ok(function(ok) --NOTE: these may not be doing anything, use PackerComplete for now
+        await(main)
+        Log:debug(string.format("Rollback snapshot file [%s] completed", default_snapshot))
+        if next(ok.failed) then
+          Log:warn(string.format("Couldn't rollback %s", vim.inspect(ok.failed)))
+        end
+        pcall(opts.on_complete, ok)
+      end)
+      :map_err(function(err)
+        await(main)
+        Log:error(err)
+        pcall(opts.on_error, err)
+      end)
+  end)()
 end
 
 return plugin_loader

+ 19 - 78
lua/lvim/plugins.lua

@@ -1,56 +1,14 @@
-local commit = {
-  alpha_nvim = "14be0ac200f44009672046123c6fcb30724018a5",
-  bufferline = "5e101b1b4e1ea5b868b8865a5f749b0b5b8f3ccd",
-  cmp_buffer = "d66c4c2d376e5be99db68d2362cd94d250987525",
-  cmp_luasnip = "d6f837f4e8fe48eeae288e638691b91b97d1737f",
-  cmp_nvim_lsp = "ebdfc204afb87f15ce3d3d3f5df0b8181443b5ba",
-  cmp_path = "466b6b8270f7ba89abd59f402c73f63c7331ff6e",
-  comment = "a841f73523440c4f32d39f0290cf1e691311db2a",
-  dapinstall = "24923c3819a450a772bb8f675926d530e829665f",
-  fixcursorhold = "1bfb32e7ba1344925ad815cb0d7f901dbc0ff7c1",
-  friendly_snippets = "ad07b2844021b20797adda5b483265802559a693",
-  gitsigns = "2df360de757c39c04076cb04bcbbd361dec3c8c2",
-  lua_dev = "a0ee77789d9948adce64d98700cc90cecaef88d5",
-  lualine = "181b14348f513e6f9eb3bdd2252e13630094fdd3",
-  luasnip = "ee350179f842699a42b3d6277b2ded8ce73bdc33",
-  nlsp_settings = "ea9b88e289359843c3cc5bfbf42e5ed9cc3df5f2",
-  null_ls = "041601cb03daa8982c5af6edc6641f4b97e9d6b5",
-  nvim_autopairs = "6617498bea01c9c628406d7e23030da57f2f8718",
-  nvim_cmp = "71d7f46b930bf08e982925c77bd9b0a9808c1162",
-  nvim_dap = "3d0575a777610b364fea745b85ad497d56b8009a",
-  nvim_lsp_installer = "dc783087bef65cc7c2943d8641ff1b6dfff6e5a9",
-  nvim_lspconfig = "710deb04d9f8b73517e1d995a57a1505cbbaac51",
-  nvim_notify = "f81b48d298c0ff7479b66568d9cc1a4794c196d0",
-  nvim_tree = "20797a8d74e68bce50b98455c76c5de250c6f0e5",
-  nvim_treesitter = "fd92e70c69330dd8f2f6753d3d987c34e7dacd24",
-  nvim_ts_context_commentstring = "097df33c9ef5bbd3828105e4bee99965b758dc3f",
-  nvim_web_devicons = "4415d1aaa56f73b9c05795af84d625c610b05d3b",
-  onedarker = "b00dd2189f264c5aeb4cf04c59439655ecd573ec",
-  packer = "c576ab3f1488ee86d60fd340d01ade08dcabd256",
-  plenary = "14dfb4071022b22e08384ee125a5607464b6d397",
-  popup = "b7404d35d5d3548a82149238289fa71f7f6de4ac",
-  project = "cef52b8da07648b750d7f1e8fb93f12cb9482988",
-  schemastore = "265eabf9f8ab33cc6bf1683c286b04e280a2b2e7",
-  structlog = "6f1403a192791ff1fa7ac845a73de9e860f781f1",
-  telescope = "a36a813d5d031e6f5d52b74986915e68130febd9",
-  telescope_fzf_native = "8ec164b541327202e5e74f99bcc5fe5845720e18",
-  toggleterm = "e97d0c1046512e975a9f3fa95afe98f312752b1c",
-  which_key = "a3c19ec5754debb7bf38a8404e36a9287b282430",
-}
-
-return {
+local core_plugins = {
   -- Packer can manage itself as an optional plugin
-  { "wbthomason/packer.nvim", commit = commit.packer },
-  { "neovim/nvim-lspconfig", commit = commit.nvim_lspconfig },
-  { "tamago324/nlsp-settings.nvim", commit = commit.nlsp_settings },
+  { "wbthomason/packer.nvim" },
+  { "neovim/nvim-lspconfig" },
+  { "tamago324/nlsp-settings.nvim" },
   {
     "jose-elias-alvarez/null-ls.nvim",
-    commit = commit.null_ls,
   },
-  { "antoinemadec/FixCursorHold.nvim", commit = commit.fixcursorhold }, -- Needed while issue https://github.com/neovim/neovim/issues/12587 is still open
+  { "antoinemadec/FixCursorHold.nvim" }, -- Needed while issue https://github.com/neovim/neovim/issues/12587 is still open
   {
     "williamboman/nvim-lsp-installer",
-    commit = commit.nvim_lsp_installer,
   },
   {
     "lunarvim/onedarker.nvim",
@@ -58,26 +16,25 @@ return {
       require("onedarker").setup()
       lvim.builtin.lualine.options.theme = "onedarker"
     end,
-    commit = commit.onedarker,
     disable = lvim.colorscheme ~= "onedarker",
   },
   {
     "rcarriga/nvim-notify",
-    commit = commit.nvim_notify,
+
     config = function()
       require("lvim.core.notify").setup()
     end,
     requires = { "nvim-telescope/telescope.nvim" },
     disable = not lvim.builtin.notify.active or not lvim.builtin.telescope.active,
   },
-  { "Tastyep/structlog.nvim", commit = commit.structlog },
+  { "Tastyep/structlog.nvim" },
 
-  { "nvim-lua/popup.nvim", commit = commit.popup },
-  { "nvim-lua/plenary.nvim", commit = commit.plenary },
+  { "nvim-lua/popup.nvim" },
+  { "nvim-lua/plenary.nvim" },
   -- Telescope
   {
     "nvim-telescope/telescope.nvim",
-    commit = commit.telescope,
+
     config = function()
       require("lvim.core.telescope").setup()
     end,
@@ -86,14 +43,12 @@ return {
   {
     "nvim-telescope/telescope-fzf-native.nvim",
     requires = { "nvim-telescope/telescope.nvim" },
-    commit = commit.telescope_fzf_native,
     run = "make",
     disable = not lvim.builtin.telescope.active,
   },
   -- Install nvim-cmp, and buffer source as a dependency
   {
     "hrsh7th/nvim-cmp",
-    commit = commit.nvim_cmp,
     config = function()
       if lvim.builtin.cmp then
         require("lvim.core.cmp").setup()
@@ -106,41 +61,33 @@ return {
   },
   {
     "rafamadriz/friendly-snippets",
-    commit = commit.friendly_snippets,
   },
   {
     "L3MON4D3/LuaSnip",
     config = function()
       require("luasnip/loaders/from_vscode").lazy_load()
     end,
-    commit = commit.luasnip,
   },
   {
     "hrsh7th/cmp-nvim-lsp",
-    commit = commit.cmp_nvim_lsp,
   },
   {
     "saadparwaiz1/cmp_luasnip",
-    commit = commit.cmp_luasnip,
   },
   {
     "hrsh7th/cmp-buffer",
-    commit = commit.cmp_buffer,
   },
   {
     "hrsh7th/cmp-path",
-    commit = commit.cmp_path,
   },
   {
     "folke/lua-dev.nvim",
     module = "lua-dev",
-    commit = commit.lua_dev,
   },
 
   -- Autopairs
   {
     "windwp/nvim-autopairs",
-    commit = commit.nvim_autopairs,
     -- event = "InsertEnter",
     config = function()
       require("lvim.core.autopairs").setup()
@@ -151,7 +98,6 @@ return {
   -- Treesitter
   {
     "nvim-treesitter/nvim-treesitter",
-    commit = commit.nvim_treesitter,
     branch = vim.fn.has "nvim-0.6" == 1 and "master" or "0.5-compat",
     -- run = ":TSUpdate",
     config = function()
@@ -160,7 +106,6 @@ return {
   },
   {
     "JoosepAlviste/nvim-ts-context-commentstring",
-    commit = commit.nvim_ts_context_commentstring,
     event = "BufReadPost",
   },
 
@@ -169,7 +114,6 @@ return {
     "kyazdani42/nvim-tree.lua",
     -- event = "BufWinOpen",
     -- cmd = "NvimTreeToggle",
-    commit = commit.nvim_tree,
     config = function()
       require("lvim.core.nvimtree").setup()
     end,
@@ -178,7 +122,6 @@ return {
 
   {
     "lewis6991/gitsigns.nvim",
-    commit = commit.gitsigns,
 
     config = function()
       require("lvim.core.gitsigns").setup()
@@ -190,7 +133,6 @@ return {
   -- Whichkey
   {
     "folke/which-key.nvim",
-    commit = commit.which_key,
     config = function()
       require("lvim.core.which-key").setup()
     end,
@@ -201,7 +143,6 @@ return {
   -- Comments
   {
     "numToStr/Comment.nvim",
-    commit = commit.comment,
     event = "BufRead",
     config = function()
       require("lvim.core.comment").setup()
@@ -212,7 +153,6 @@ return {
   -- project.nvim
   {
     "ahmedkhalf/project.nvim",
-    commit = commit.project,
     config = function()
       require("lvim.core.project").setup()
     end,
@@ -220,13 +160,12 @@ return {
   },
 
   -- Icons
-  { "kyazdani42/nvim-web-devicons", commit = commit.nvim_web_devicons },
+  { "kyazdani42/nvim-web-devicons" },
 
   -- Status Line and Bufferline
   {
     -- "hoob3rt/lualine.nvim",
     "nvim-lualine/lualine.nvim",
-    commit = commit.lualine,
     -- "Lunarvim/lualine.nvim",
     config = function()
       require("lvim.core.lualine").setup()
@@ -236,7 +175,6 @@ return {
 
   {
     "akinsho/bufferline.nvim",
-    commit = commit.bufferline,
     config = function()
       require("lvim.core.bufferline").setup()
     end,
@@ -247,7 +185,6 @@ return {
   -- Debugging
   {
     "mfussenegger/nvim-dap",
-    commit = commit.nvim_dap,
     -- event = "BufWinEnter",
     config = function()
       require("lvim.core.dap").setup()
@@ -258,7 +195,6 @@ return {
   -- Debugger management
   {
     "Pocco81/DAPInstall.nvim",
-    commit = commit.dapinstall,
     -- event = "BufWinEnter",
     -- event = "BufRead",
     disable = not lvim.builtin.dap.active,
@@ -270,14 +206,12 @@ return {
     config = function()
       require("lvim.core.alpha").setup()
     end,
-    commit = commit.alpha_nvim,
     disable = not lvim.builtin.alpha.active,
   },
 
   -- Terminal
   {
     "akinsho/toggleterm.nvim",
-    commit = commit.toggleterm,
     event = "BufWinEnter",
     config = function()
       require("lvim.core.terminal").setup()
@@ -288,6 +222,13 @@ return {
   -- SchemaStore
   {
     "b0o/schemastore.nvim",
-    commit = commit.schemastore,
   },
 }
+
+for _, entry in ipairs(core_plugins) do
+  if not os.getenv "LVIM_DEV_MODE" then
+    entry["lock"] = true
+  end
+end
+
+return core_plugins

+ 0 - 18
lua/lvim/utils/git.lua

@@ -113,22 +113,4 @@ function M.get_lvim_current_sha()
   return abbrev_version
 end
 
-function M.generate_plugins_sha(output)
-  local list = {}
-  output = output or "commits.lua"
-
-  local core_plugins = require "lvim.plugins"
-  for _, plugin in pairs(core_plugins) do
-    local name = plugin[1]:match "/(%S*)"
-    local url = "https://github.com/" .. plugin[1]
-    print("checking: " .. name .. ", at: " .. url)
-    local retval, latest_sha = git_cmd { args = { "ls-remote", url, "origin", "HEAD" } }
-    if retval == 0 then
-      -- replace dashes, remove postfixes and use lowercase
-      local normalize_name = (name:gsub("-", "_"):gsub("%.%S+", "")):lower()
-      list[normalize_name] = latest_sha[1]:gsub("\tHEAD", "")
-    end
-  end
-  require("lvim.utils").write_file(output, "local commit = " .. vim.inspect(list), "w")
-end
 return M

+ 16 - 8
lua/lvim/utils/hooks.lua

@@ -12,17 +12,25 @@ function M.run_pre_reload()
 end
 
 function M.run_on_packer_complete()
-  if not in_headless then
-    -- manually trigger event to fix colors
-    vim.cmd [[ doautocmd ColorScheme ]]
-  end
-  Log:info "Reloaded configuration"
+  vim.schedule(function()
+    if not in_headless then
+      -- colorscheme must get called after plugins are loaded or it will break new installs.
+      vim.g.colors_name = lvim.colorscheme
+      vim.cmd("colorscheme " .. lvim.colorscheme)
+    else
+      Log:debug "Packer operation complete"
+    end
+  end)
 end
 
 function M.run_post_reload()
   Log:debug "Starting post-reload hook"
-  require("lvim.plugin-loader").ensure_installed()
   M.reset_cache()
+  vim.schedule(function()
+    if not in_headless then
+      Log:info "Reloaded configuration"
+    end
+  end)
 end
 
 ---Reset any startup cache files used by Packer and Impatient
@@ -48,8 +56,8 @@ function M.run_post_update()
   Log:debug "Starting post-update hook"
   M.reset_cache()
 
-  Log:debug "Updating core plugins"
-  require("lvim.plugin-loader").ensure_installed()
+  Log:debug "Syncing core plugins"
+  require("lvim.plugin-loader").sync_core_plugins()
 
   if not in_headless then
     vim.schedule(function()

+ 113 - 0
snapshots/default.json

@@ -0,0 +1,113 @@
+{
+  "Comment.nvim": {
+    "commit": "8a2b2f3"
+  },
+  "DAPInstall.nvim": {
+    "commit": "24923c3"
+  },
+  "FixCursorHold.nvim": {
+    "commit": "1bfb32e"
+  },
+  "LuaSnip": {
+    "commit": "680d42a"
+  },
+  "alpha-nvim": {
+    "commit": "14be0ac"
+  },
+  "bufferline.nvim": {
+    "commit": "e1202c6"
+  },
+  "cmp-buffer": {
+    "commit": "d66c4c2"
+  },
+  "cmp-nvim-lsp": {
+    "commit": "ebdfc20"
+  },
+  "cmp-path": {
+    "commit": "466b6b8"
+  },
+  "cmp_luasnip": {
+    "commit": "d6f837f"
+  },
+  "friendly-snippets": {
+    "commit": "ad07b28"
+  },
+  "gitsigns.nvim": {
+    "commit": "3791dfa"
+  },
+  "lua-dev.nvim": {
+    "commit": "a0ee777"
+  },
+  "lualine.nvim": {
+    "commit": "181b143"
+  },
+  "nlsp-settings.nvim": {
+    "commit": "66234e2"
+  },
+  "null-ls.nvim": {
+    "commit": "71bb21d"
+  },
+  "nvim-autopairs": {
+    "commit": "6617498"
+  },
+  "nvim-cmp": {
+    "commit": "272cbdc"
+  },
+  "nvim-dap": {
+    "commit": "3d0575a"
+  },
+  "nvim-lsp-installer": {
+    "commit": "56b7cf4"
+  },
+  "nvim-lspconfig": {
+    "commit": "48e59a4"
+  },
+  "nvim-notify": {
+    "commit": "da10302"
+  },
+  "nvim-tree.lua": {
+    "commit": "e87ee0e"
+  },
+  "nvim-treesitter": {
+    "commit": "9e8749f"
+  },
+  "nvim-ts-context-commentstring": {
+    "commit": "7810f1f"
+  },
+  "nvim-web-devicons": {
+    "commit": "4415d1a"
+  },
+  "onedarker.nvim": {
+    "commit": "b00dd21"
+  },
+  "packer.nvim": {
+    "commit": "c576ab3"
+  },
+  "plenary.nvim": {
+    "commit": "0d66015"
+  },
+  "popup.nvim": {
+    "commit": "b7404d3"
+  },
+  "project.nvim": {
+    "commit": "cef52b8"
+  },
+  "schemastore.nvim": {
+    "commit": "0a3f765"
+  },
+  "structlog.nvim": {
+    "commit": "6f1403a"
+  },
+  "telescope-fzf-native.nvim": {
+    "commit": "8ec164b"
+  },
+  "telescope.nvim": {
+    "commit": "1a72a92"
+  },
+  "toggleterm.nvim": {
+    "commit": "e97d0c1"
+  },
+  "which-key.nvim": {
+    "commit": "a3c19ec"
+  }
+}

+ 3 - 6
tests/helpers.lua → tests/lvim/helpers.lua

@@ -13,11 +13,11 @@ function M.search_file(file, args)
       end,
     })
     :sync()
-  return stdout, ret, stderr
+  return ret, stdout, stderr
 end
 
 function M.file_contains(file, query)
-  local stdout, ret, stderr = M.search_file(file, query)
+  local ret, stdout, stderr = M.search_file(file, query)
   if ret == 0 then
     return true
   end
@@ -32,7 +32,7 @@ end
 
 function M.log_contains(query)
   local logfile = require("lvim.core.log"):get_path()
-  local stdout, ret, stderr = M.search_file(logfile, query)
+  local ret, stdout, stderr = M.search_file(logfile, query)
   if ret == 0 then
     return true
   end
@@ -42,9 +42,6 @@ function M.log_contains(query)
   if not vim.tbl_isempty(stdout) then
     error(vim.inspect(stdout))
   end
-  if not vim.tbl_isempty(stderr) then
-    error(vim.inspect(stderr))
-  end
   return false
 end
 

+ 6 - 6
tests/minimal_init.lua

@@ -2,11 +2,11 @@ local path_sep = vim.loop.os_uname().version:match "Windows" and "\\" or "/"
 local base_dir = os.getenv "LUNARVIM_RUNTIME_DIR" .. path_sep .. "lvim"
 local tests_dir = base_dir .. path_sep .. "tests"
 
-vim.opt.rtp = { base_dir, tests_dir, os.getenv "VIMRUNTIME" }
+vim.opt.rtp:append(tests_dir)
+vim.opt.rtp:append(base_dir)
 
-vim.opt.swapfile = false
+require("lvim.bootstrap"):init(base_dir)
 
--- load helper functions before any other plugin to avoid name-collisions
-pcall(require, "tests.helpers")
-
-require("lvim.bootstrap"):init()
+-- NOTE: careful about name collisions
+-- see https://github.com/nvim-lualine/lualine.nvim/pull/621
+require "tests.lvim.helpers"

+ 1 - 1
tests/specs/lsp_spec.lua

@@ -1,6 +1,6 @@
 local a = require "plenary.async_lib.tests"
 local utils = require "lvim.utils"
-local helpers = require "tests.helpers"
+local helpers = require "tests.lvim.helpers"
 local temp_dir = vim.loop.os_getenv "TEMP" or "/tmp"
 lvim.lsp.templates_dir = join_paths(temp_dir, "lvim", "tests", "artifacts")
 

+ 34 - 2
tests/specs/plugins_load_spec.lua

@@ -5,7 +5,7 @@ a.describe("plugin-loader", function()
   local loader = require "lvim.plugin-loader"
 
   a.it("should be able to load default packages without errors", function()
-    loader:load { plugins, lvim.plugins }
+    loader.load { plugins, lvim.plugins }
 
     -- TODO: maybe there's a way to avoid hard-coding the names of the modules?
     local startup_plugins = {
@@ -18,7 +18,7 @@ a.describe("plugin-loader", function()
   end)
 
   a.it("should be able to load lsp packages without errors", function()
-    loader:load { plugins, lvim.plugins }
+    loader.load { plugins, lvim.plugins }
 
     require("lvim.lsp").setup()
 
@@ -32,4 +32,36 @@ a.describe("plugin-loader", function()
       assert.truthy(package.loaded[plugin])
     end
   end)
+  a.it("should be able to rollback plugins without errors", function()
+    local plugin = { name = "onedarker.nvim" }
+    plugin.path = vim.tbl_filter(function(package)
+      return package:match(plugin.name)
+    end, vim.api.nvim_list_runtime_paths())[1]
+
+    local get_current_sha = function(repo)
+      local res = vim.fn.system(string.format("git -C %s log -1 --pretty=%%h", repo)):gsub("\n", "")
+      return res
+    end
+    plugin.test_sha = "316b1c9"
+    _G.locked_sha = get_current_sha(plugin.path)
+    loader.load { plugins, lvim.plugins }
+
+    os.execute(string.format("git -C %s fetch --deepen 999 --quiet", plugin.path))
+    os.execute(string.format("git -C %s checkout %s --quiet", plugin.path, plugin.test_sha))
+    assert.equal(plugin.test_sha, get_current_sha(plugin.path))
+    _G.completed = false
+    _G.verify_sha = function()
+      if _G.locked_sha ~= get_current_sha(plugin.path) then
+        error "unmached results!"
+      else
+        _G.completed = true
+      end
+    end
+    vim.cmd [[autocmd User PackerComplete lua _G.verify_sha()]]
+    loader.sync_core_plugins()
+    local ret = vim.wait(30 * 10 * 1000, function()
+      return _G.completed == true
+    end, 200)
+    assert.True(ret)
+  end)
 end)

+ 114 - 0
utils/ci/generate_new_lockfile.lua

@@ -0,0 +1,114 @@
+local sp = os.getenv "SNAPSHOT_PATH"
+
+local function call_proc(process, opts, cb)
+  local std_output = ""
+  local error_output = ""
+
+  local function onread(_, is_stderr)
+    return function(err, data)
+      if data then
+        if is_stderr then
+          error_output = (error_output or "") .. err
+        else
+          std_output = (std_output or "") .. data
+        end
+      end
+    end
+  end
+
+  local uv = vim.loop
+  local handle
+
+  local stdout = uv.new_pipe(false)
+  local stderr = uv.new_pipe(false)
+
+  local stdio = { nil, stdout, stderr }
+
+  handle = uv.spawn(
+    process,
+    { args = opts.args, cwd = uv.cwd(), stdio = stdio },
+    vim.schedule_wrap(function(code)
+      if code ~= 0 then
+        stdout:read_stop()
+        stderr:read_stop()
+      end
+
+      local check = uv.new_check()
+      check:start(function()
+        for _, pipe in ipairs(stdio) do
+          if pipe and not pipe:is_closing() then
+            return
+          end
+        end
+        check:stop()
+        handle:close()
+        cb(code, std_output, error_output)
+      end)
+    end)
+  )
+
+  uv.read_start(stdout, onread(handle, false))
+  uv.read_start(stderr, onread(handle, true))
+
+  return handle
+end
+
+local plugins_list = {}
+
+local completed = 0
+
+local function write_lockfile(verbose)
+  local default_plugins = {}
+  local active_jobs = {}
+
+  local core_plugins = require "lvim.plugins"
+  for _, plugin in pairs(core_plugins) do
+    local name = plugin[1]:match "/(%S*)"
+    local url = "https://github.com/" .. plugin[1]
+    local commit = ""
+    table.insert(default_plugins, {
+      name = name,
+      url = url,
+      commit = commit,
+    })
+  end
+
+  table.sort(default_plugins, function(a, b)
+    return a.name < b.name
+  end)
+
+  for _, entry in pairs(default_plugins) do
+    local on_done = function(success, result, errors)
+      completed = completed + 1
+      if not success then
+        print("error: " .. errors)
+        return
+      end
+      local latest_sha = result:gsub("\tHEAD\n", ""):sub(1, 7)
+      plugins_list[entry.name] = {
+        commit = latest_sha,
+      }
+    end
+
+    local handle = call_proc("git", { args = { "ls-remote", entry.url, "HEAD" } }, on_done)
+    table.insert(active_jobs, handle)
+  end
+
+  print("active: " .. #active_jobs)
+  print("parsers: " .. #default_plugins)
+
+  vim.wait(#active_jobs * 60 * 1000, function()
+    return completed == #active_jobs
+  end)
+
+  if verbose then
+    print(vim.inspect(plugins_list))
+  end
+
+  local fd = assert(io.open(sp, "w"))
+  fd:write(vim.json.encode(plugins_list), "\n")
+  fd:flush()
+end
+
+write_lockfile()
+vim.cmd "q"

+ 15 - 0
utils/ci/generate_new_lockfile.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -e
+
+REPO_DIR=$(git rev-parse --show-toplevel)
+
+export SNAPSHOT_NAME="default.json"
+export SNAPSHOT_PATH="${REPO_DIR}/snapshots/${SNAPSHOT_NAME}"
+
+time lvim --headless \
+  -c "luafile ./utils/ci/generate_new_lockfile.lua"
+
+temp=$(mktemp)
+
+jq --sort-keys . "${SNAPSHOT_PATH}" >"${temp}"
+mv "${temp}" "${SNAPSHOT_PATH}"