Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature request] Support package snapshot #298 #370

Merged
merged 34 commits into from
Feb 25, 2022
Merged

[Feature request] Support package snapshot #298 #370

merged 34 commits into from
Feb 25, 2022

Conversation

c3n21
Copy link
Contributor

@c3n21 c3n21 commented May 24, 2021

It's just a first implementation and I only tested it with spec(1), namely:

> spec can be a table with a function as its first element and config overrides as another element:
packer.startup({function() use 'tjdevries/colorbuddy.vim' end, config = { ... }})

## Usage
### Make snapshot
To make a snapshot you just have to run :PackerSnapshot <snapshot name>

### Restore snapshot
1. To restore a snapshot you have to add snapshot key in the config table you pass to packer.startup.
So, if I want to restore a snapshot named foo I would have something like this:

packer.startup(~~ ~~{function() use 'tjdevries/colorbuddy.vim' end, ~~ ~~config = { snapshot='foo' })

You can also specify the default directory in which retrieve and save snapshots with the following:
packer.startup(~~ ~~{function() use 'tjdevries/colorbuddy.vim' end, ~~ ~~config = { snapshot='foo', snapshot_path='path/to/snapshots' })

By default the snapshot's location is in ~/.config/nvim/plugin

2. Source the file :luafile %
3. :PackerUpdate
4. Restart NeoVim and enjoy

If you want to update you will need to remove snapshot from config table, and then source the file and update.

Final Implementation

Usage

Make snapshot

To make a snapshot you just have to run :PackerSnapshot <snapshot name>; you can also append single plugins name to selectively snapshot only the plugins you want like :PackerSnapshot <snapshot name> <plugin_1> <plugin_2>

Restore snapshot

To rollback to a snapshot you have two ways:

  1. Add config.snapshot = 'some snapshot'. You can specify either the snapshot name which will be fetched inside config.snapshot_path (the default snapshot directory which is ~/.cache/nvim/packer but you can change it) or an absolute path.
    So, if I want to restore a snapshot named foo I would have something like this:

packer.startup( {function() use 'tjdevries/colorbuddy.vim' end, config = { snapshot='foo' })

In this way, every time you boot up NeoVim it will always load that snapshot.

  1. You can also run :PackerRollback which updates your installed plugins' commit to the snapshot you specified. After that you just need to run :PackerUpdate and it will revert back

Delete snapshot

  1. Just run PackerDelete <snapshot name> , you can either provide only the snapshot name or an absolute path.

Misc:

  • I've also added a packer.snapshot_complete() so that PackerRollback and PackerDelete can prompt which snapshots are available.
  • The snapshot format is a Lua table serialized using vim.inspect
  • packer.rollback hard resets the git repo of the given plugin

Known issues:

After running the update, the window will show error saying that you are in detached HEAD mode

@c3n21
Copy link
Contributor Author

c3n21 commented May 24, 2021

Resolve #298

Copy link
Collaborator

@akinsho akinsho left a comment

Choose a reason for hiding this comment

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

@c3n21 this looks great thanks for taking the time to implement this. I haven't tried it locally or anything just left some comments regarding some comments and a question or two.

Could you also maybe add some tests for this new functionality as well in tests dir under snapshot_spec.lua or something to validate that they are being created and read correctly. There are a few other tests there which should help guide you. Also there are docs on plenary's repo

I should probably say that given this is whole new thing ultimately @wbthomason will have to review when he's free.

lua/packer.lua Outdated
local f_snapshot, err = io.open(filename, 'r+')

if err ~= nil then
print(err)
Copy link
Collaborator

Choose a reason for hiding this comment

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

probs a lingering debug print

@@ -32,6 +32,9 @@ local function install_plugin(plugin, display_win, results)
end

local function do_install(_, plugins, missing_plugins, results)
for index, value in ipairs(plugins) do
print(fmt("index: %s, value: %s", index, value))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this intentional or left over from the initial implementation


local function cfg(_config) config = _config end
--[[
property: 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

What does this apply to? Is this an emmy lua annotation for something?

lua/packer.lua Outdated Show resolved Hide resolved
lua/packer.lua Outdated
filename = util.join_paths(config.snapshot_path, filename)
await(snapshot(filename, plugins))
await(a.main)
--local delta = string.gsub(vim.fn.reltimestr(vim.fn.reltime(start_time)), ' ', '')
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this left over from some profiling you were doing?

CHANGELOG.md Outdated
@@ -0,0 +1,22 @@
# CHANGELOG
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will be one for @wbthomason to decide but packer hasn't previously maintained a changelog so not sure if we need/should be adding this here since it isn't particularly discoverable. I think this should probably just go in the main docs 🤷🏾

Copy link
Owner

Choose a reason for hiding this comment

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

I actually favor adding a changelog and linking to it from the main docs; the README is too long as it is.

Copy link
Owner

Choose a reason for hiding this comment

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

I would prefer to have changes in the changelog be date and commit-stamped, though.

Copy link
Owner

Choose a reason for hiding this comment

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

To clarify: we also ought to document this feature in the main docs, but I like having a changelog too. Also, the docs should describe what a snapshot does/does not save.

@c3n21
Copy link
Contributor Author

c3n21 commented May 25, 2021

@c3n21 this looks great thanks for taking the time to implement this. I haven't tried it locally or anything just left some comments regarding some comments and a question or two.

Could you also maybe add some tests for this new functionality as well in tests dir under snapshot_spec.lua or something to validate that they are being created and read correctly. There are a few other tests there which should help guide you. Also there are docs on plenary's repo

I should probably say that given this is whole new thing ultimately @wbthomason will have to review when he's free.

Thank you for taking your time and review my PR, really appreciated.

As soon as I have some free time I will clean up the mess I left in the code and create the tests.

Copy link
Owner

@wbthomason wbthomason left a comment

Choose a reason for hiding this comment

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

Thanks, as @akinsho said this looks like a great start on the feature. I agree with @akinsho's comments and left some questions of my own on the changes, mostly around documentation and deciding how snapshots will interact with "fresh" plugin specification values.

CHANGELOG.md Outdated
@@ -0,0 +1,22 @@
# CHANGELOG
Copy link
Owner

Choose a reason for hiding this comment

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

I would prefer to have changes in the changelog be date and commit-stamped, though.

lua/packer.lua Outdated Show resolved Hide resolved
CHANGELOG.md Outdated
@@ -0,0 +1,22 @@
# CHANGELOG
Copy link
Owner

Choose a reason for hiding this comment

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

To clarify: we also ought to document this feature in the main docs, but I like having a changelog too. Also, the docs should describe what a snapshot does/does not save.

lua/packer.lua Outdated Show resolved Hide resolved
lua/packer.lua Outdated Show resolved Hide resolved
lua/packer.lua Outdated Show resolved Hide resolved
lua/packer.lua Show resolved Hide resolved
lua/packer/plugin_types/git.lua Outdated Show resolved Hide resolved
lua/packer/plugin_types/git.lua Outdated Show resolved Hide resolved
lua/packer/result.lua Outdated Show resolved Hide resolved
@c3n21 c3n21 marked this pull request as draft May 27, 2021 17:03
@c3n21
Copy link
Contributor Author

c3n21 commented May 27, 2021

Just implemented PackerRollback.
Next I would like to:

  • add the completion for PackerRollback
  • add the tests and it should be good to go

@c3n21
Copy link
Contributor Author

c3n21 commented May 31, 2021

@wbthomason @akinsho I've been working on the tests for the snapshot feature, but I can't get it to work with plugin_utils_spec.lua and packer_snapshot_spec.lua (which is the one I've created)

Could you give me some advice or point out where am I doing wrong?

@akinsho
Copy link
Collaborator

akinsho commented May 31, 2021

@c3n21 are you saying the tests don't run? or that you can't get them to pass? anyway re. running them you need to use PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal.vim'} I advise this over the file only test since that will pick up your init.vim and could be causing unpredictable results.

Regarding the tests themselves from what I can see it appears you might be nesting describe blocks inside the tests themselves which won't work

@akinsho
Copy link
Collaborator

akinsho commented May 31, 2021

Oh and a side note about the tests can you call them just snapshot_spec.lua I don't think the prefix is needed and I mean to try and change any of the existing test files that are prefixed that way since they should just use the filename + _spec.lua as the test name

@c3n21
Copy link
Contributor Author

c3n21 commented Jun 1, 2021

@akinsho

I've made removed the describe block inside the it block as you said and at least it runs now.

However, if I add some async code the test gives me this result

We had an unexpected error: { "...packer/start/plenary.nvim/lua/plenary/async_lib/util.lua:314: Blocking on future failed ...acker/st art/plenary.nvim/lua/plenary/async_lib/async.lua:20: expected a single return value\n\nstack traceback:\n\t...packer/start/plenary.nvim/l ua/plenary/async_lib/util.lua:319: in function <...packer/start/plenary.nvim/lua/plenary/async_lib/util.lua:303>\n" } { errs = {}, fail = {}, fatal = { "...packer/start/plenary.nvim/lua/plenary/async_lib/util.lua:314: Blocking on future failed ...acker/start/plenary.nvim/lua/p lenary/async_lib/async.lua:20: expected a single return value\n\nstack traceback:\n\t...packer/start/plenary.nvim/lua/plenary/async_lib/u til.lua:319: in function <...packer/start/plenary.nvim/lua/plenary/async_lib/util.lua:303>\n" }, pass = {} }

but if I remove all the async code (remove await and a. from describe and it it runs but it makes an empty snapshot.
That's probably because it doesn't install the test plugin, but I can't figure out how to make it work.

@akinsho
Copy link
Collaborator

akinsho commented Jun 1, 2021

@c3n21 the async testing has kind of been borrowed from plenary but packer has it's own async implementation so I think it's a little rough and obscure used together. When I've seen this error in the past it's usually meant that there is an error occurring in your test somewhere and this is how it is bubbling up. It could be the usage/syntax also I'll take a look and see if there are any incorrect usages

@akinsho
Copy link
Collaborator

akinsho commented Jun 1, 2021

@c3n21 I can't see any clear errors. One thing to keep in mind is that it's probably a good idea to test as close to your changes as possible for the time being i.e. just call your functions and assert they do the correct thing rather than trying to setup all of packer. A lot of those code paths are untested so you could easily get blocked by a completely separate issue that is unrelated to your change.

@c3n21
Copy link
Contributor Author

c3n21 commented Jun 1, 2021

@c3n21 I can't see any clear errors. One thing to keep in mind is that it's probably a good idea to test as close to your changes as possible for the time being i.e. just call your functions and assert they do the correct thing rather than trying to setup all of packer. A lot of those code paths are untested so you could easily get blocked by a completely separate issue that is unrelated to your change.

The problem is that I need to be sure that at least one plugin is installed using packer so that I can snapshot it, check the snapshot format etc, so it's quite mandatory to call packer.install()

Without that it's pointless...

Atm I'm only checking if the snapshot have been created or not

Copy link
Collaborator

@akinsho akinsho left a comment

Choose a reason for hiding this comment

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

It looks like snapshot is only really dependent on list_missing_plugins, so rather than trying to run install which I think will have quite a lot of side effects you could try to just mock that function. I think plenary has a few mock utilities but I don't know if those will work with a required module so another thing you could do is in the setup of your test you could replace require with fake_require and check if the value is that module and return a fake module

function test_require(module_name)
  if module_name == "plugin_utils" then
    return {list_missing_plugins = function() return {value_I_want} end}
  end
  return require(module_name)
end

-- Inside describe
_G.require = test_require

tests/packer_snapshot_spec.lua Outdated Show resolved Hide resolved
tests/packer_snapshot_spec.lua Outdated Show resolved Hide resolved
@c3n21
Copy link
Contributor Author

c3n21 commented Jun 10, 2021

Sorry for the absence but I need to take some exams, so I'll keep working on this once I'll be done (by the end of the month)

Just letting you know that I'm not dead and didn't give up about this feature :)

@c3n21 c3n21 marked this pull request as ready for review August 2, 2021 19:38
@c3n21
Copy link
Contributor Author

c3n21 commented Aug 2, 2021

Update:

finally I've managed to pull it off.

I have updated the first message of this thread so check it out for the "basic usage".

I've also updated the documentation about the snapshot feature and added all the tests I could think of.

There is a "bug" though I already documented in the first comment

@c3n21 c3n21 requested a review from wbthomason August 4, 2021 15:34
@c3n21
Copy link
Contributor Author

c3n21 commented Aug 28, 2021

@wbthomason is this feature still needed? Because it still needs some improvement but if it's not needed I'm not going to improve this

@akinsho
Copy link
Collaborator

akinsho commented Aug 29, 2021

@c3n21 @.wbthomason is away atm and I haven't really been following things too closely. I don't think the need for this has changed though since it's driven entirely by all the people who would want to use it which I don't think has changed and no alternative has been implemented

@c3n21
Copy link
Contributor Author

c3n21 commented Aug 29, 2021

@c3n21 @.wbthomason is away atm and I haven't really been following things too closely. I don't think the need for this has changed though since it's driven entirely by all the people who would want to use it which I don't think has changed and no alternative has been implemented

Ok, I'll mention you for the review when I'll be done

@c3n21
Copy link
Contributor Author

c3n21 commented Aug 30, 2021

@akinsho it should be good now. I've added the docs and updated the README.md.

@c3n21 c3n21 requested a review from akinsho August 30, 2021 15:52
@wbthomason
Copy link
Owner

Hi @c3n21! Thanks for your continued work on this, and apologies for my lack of response; as @akinsho mentioned, I was gone.

For reviewing/testing: do you have any suggested operations which would be good to validate that everything is working?

@c3n21
Copy link
Contributor Author

c3n21 commented Aug 30, 2021

Hi @c3n21! Thanks for your continued work on this, and apologies for my lack of response; as @akinsho mentioned, I was gone.

For reviewing/testing: do you have any suggested operations which would be good to validate that everything is working?

Hi and welcome back!
I've already provided some tests for git.get_rev (which fails and I don't know why), git.revert (not revert_last, mine does a hard reset of the git repo to git.commit),snapshot(), packer.delete().

For testing you could:

  1. Make a snapshot with PackerSnapshot <snapshot name>;
  2. PackerUpdate and ensure that git rev-parse --short HEAD of the plugins updated in ~/.local/share/nvim/site/pack/packer/start/ is different with the one reported inside ~/.cache/nvim/packer/<snapshot name>;
  3. PackerRollback <snapshot name> (you can use TAB to autocomplete the snapshot name) and rollback to and check with git rev-parse --short HEAD that the package have been resetted properly.
  4. PackerDelete <snapshot name> (TAB if you want autocompletion) if you don't need that snapshot anymore

You can also specify in the config, e.g. config = {snapshot = <snapshot name>} when you run packer.startup() which calls packer.rollback(config.snapshot) under the hood, so it should be the same as doing it manually.

@c3n21
Copy link
Contributor Author

c3n21 commented Sep 22, 2021

Hi @wbthomason ! Did you manage to review this feature?

@c3n21
Copy link
Contributor Author

c3n21 commented Nov 22, 2021

I've updated my PR and now it seems to pass all the tests.
I've also cleaned up my fork's history since it was a bit cluttered.
Could you take a look when you have some time? @wbthomason

@wbthomason
Copy link
Owner

Taking a look now. Thanks for the work @c3n21, guidance @shadmansaleh, and patience on everyone's parts.

Copy link
Owner

@wbthomason wbthomason left a comment

Choose a reason for hiding this comment

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

This looks mostly good to go. However:

  • We should resolve the currently failing tests
  • packer.snapshot.delete does not seem to work for me - I get an error at snapshot.lua:189.
  • We should probably make a function analogous to packer.snapshot for snapshot deletion - i.e. at the top-level and with the async aspect wrapped appropriately.

lua/packer.lua Outdated Show resolved Hide resolved
@@ -794,6 +805,106 @@ packer.plugin_complete = function(lead, _, _)
return completion_list
end

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

Choose a reason for hiding this comment

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

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

@c3n21
Copy link
Contributor Author

c3n21 commented Feb 14, 2022

Right now I'm having some issues with the async code as mentioned here

If anyone could help me figure out what's going out it would be great.

@wbthomason
Copy link
Owner

@c3n21 I fixed the merge conflict and am re-running the tests; I'll look at the trace and see if I can figure out what's going on.

@wbthomason
Copy link
Owner

@c3n21 Tests seem good to go now. Anything remaining before merge that you know of?

@c3n21
Copy link
Contributor Author

c3n21 commented Feb 14, 2022

@c3n21 Tests seem good to go now. Anything remaining before merge that you know of?

I've made a minor refactor to make the code inside snapshot.create() shorter and a bit more modular.
If you don't like it just reset it and merge it as it is if it's all good to go.

Thanks for your time!

c3n21 and others added 8 commits February 14, 2022 21:08
* 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
* 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
snapshot.delete is not async
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
@c3n21
Copy link
Contributor Author

c3n21 commented Feb 15, 2022

@wbthomason I've polished a bit the code with the advices you gave me above.
I've also added an updated version of the Dockerfile and Makefile I've used during my tests which may be useful in the future.
In the Dockerfile I've created a test user and copied packer.nvim root into the container to simulate a common use case (I don't think we always run nvim as root).

Also I think this might be useful because rollback hard resets the repo, and using it on the host machine could reset all the work not pushed, and running tests is as easy as running make run-tests.

@wbthomason
Copy link
Owner

Thanks again, @c3n21! I think we're good to merge.

@wbthomason wbthomason merged commit 40cbd5c into wbthomason:master Feb 25, 2022
@smhc
Copy link
Contributor

smhc commented Feb 26, 2022

Thanks @c3n21 - good to see this merged. Thanks for all your work on it.

I've been using it since the .lua snapshot generation and have now switched to the more recent incarnation which has .json generation. I do notice that a side effect of async and vim.encode_json is that the .json file generated is not ordered or stable. This is not ideal for checking into source control and easily seeing what changed - I would expect this snapshot file to be source controlled by many people.

I couldn't find a simple solution so I simply use 'jq --sort-keys' on the file afterwards - but if anyone knows a simple fix for packer to do it automatically I can raise a PR.

@shadmansaleh
Copy link
Contributor

shadmansaleh commented Feb 26, 2022

The output isn't table because lua tables aren't stable . We can use list and sort the list before converting it to json.

@c3n21
Copy link
Contributor Author

c3n21 commented Feb 26, 2022

Thanks @c3n21 - good to see this merged. Thanks for all your work on it.

I've been using it since the .lua snapshot generation and have now switched to the more recent incarnation which has .json generation. I do notice that a side effect of async and vim.encode_json is that the .json file generated is not ordered or stable. This is not ideal for checking into source control and easily seeing what changed - I would expect this snapshot file to be source controlled by many people.

I couldn't find a simple solution so I simply use 'jq --sort-keys' on the file afterwards - but if anyone knows a simple fix for packer to do it automatically I can raise a PR.

What about sorting the table before encoding? That should be consistent isn't it?

EDIT:

Just saw the reply

@arathunku
Copy link

arathunku commented Feb 27, 2022

I've solved keys sorting by additionally processing the final file, given that it's a json dump.

echo $(jq --sort-keys . packages) > packages
prettier --parser json --write packages

It's not hooked into vim but into my script for updating plugins with --headless mode.
Easy on eyes diffs :)

EDIT: now I see @smhc had already suggested that 🤦

@smhc
Copy link
Contributor

smhc commented Feb 27, 2022

Yes I use something like:

    packer.snapshot_format = function(fn)
      local fullpath=vim.fn.stdpath('config')..'/'..fn
      local tmp=vim.fn.stdpath('config')..'/temp_'..fn
      os.execute('jq --sort-keys . '..fullpath..'> '..tmp)
      os.rename(tmp, fullpath)
    end

The jq format seems as good as it can get, I don't think there's any need to run 'prettier'. I'd prefer to chain it with 'packer.snapshot' however I couldn't get the 'await', 'and_then' stuff working correctly, and without that there is race conditions causing empty files etc. So I just run it manually afterwards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants