diff --git a/.busted b/.busted new file mode 100644 index 0000000..fa9d48a --- /dev/null +++ b/.busted @@ -0,0 +1,13 @@ +return { + _all = { + coverage = false, + lpath = 'lua/?.lua;lua/?/init.lua', + lua = 'nlua', + }, + default = { + verbose = true, + }, + tests = { + verbose = true, + }, +} diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..a1d3540 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake . -Lv diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 30d3fd8..241b530 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: theprimeagen +github: polarmutex diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d202a33 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 40fc3d1..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Tests - -on: [push, pull_request] - -jobs: - x64-ubuntu: - name: X64-ubuntu - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - run: date +%F > todays-date - - name: Restore cache for today's nightly. - uses: actions/cache@v2 - with: - path: | - _neovim - key: ${{ runner.os }}-x64-${{ hashFiles('todays-date') }} - - - name: Prepare - run: | - mkdir -p ~/.local/share/nvim/site/pack/vendor/start - git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim - ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start - - name: Run tests - run: | - curl -OL https://raw.githubusercontent.com/norcalli/bot-ci/master/scripts/github-actions-setup.sh - source github-actions-setup.sh nightly-x64 - make test - - appimage-ubuntu: - name: Appimage-ubuntu - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - run: date +%F > todays-date - - name: Restore cache for today's nightly. - uses: actions/cache@v2 - with: - path: | - build - key: ${{ runner.os }}-appimage-${{ hashFiles('todays-date') }} - - - name: Prepare - run: | - test -d build || { - mkdir -p build - wget https://github.com/neovim/neovim/releases/download/nightly/nvim.appimage - chmod +x nvim.appimage - mv nvim.appimage ./build/nvim - } - mkdir -p ~/.local/share/nvim/site/pack/vendor/start - git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim - ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start - - name: Run tests - run: | - export PATH="${PWD}/build/:${PATH}" - make test diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..17b5658 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,12 @@ +# Reference: https://commitlint.js.org/guides/ci-setup +name: Commitlint + +on: [pull_request] + +jobs: + commitlint: + runs-on: ubuntu-latest + name: Commitlint + steps: + - name: Run commitlint + uses: opensource-nepal/commitlint@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2806b77 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: Release +on: + push: + tags: + - "*" + pull_request: + +jobs: + luarocks-release: + runs-on: ubuntu-latest + name: LuaRocks upload + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Fail if changelog entry does not exist + if: startsWith(github.ref, 'refs/tags/') + run: grep -q "${{ github.ref_name }}" CHANGELOG.md + - name: LuaRocks Upload + uses: nvim-neorocks/luarocks-tag-release@v7 + env: + LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} + with: + detailed_description: | + A plugin that helps to use git worktree operations, create, switch, and delete in neovim. + - name: GitHub Release + if: startsWith(github.ref, 'refs/tags/') + uses: ncipollo/release-action@v1 + with: + bodyFile: "CHANGELOG.md" + allowUpdates: true diff --git a/.github/workflows/style-check.yml b/.github/workflows/style-check.yml new file mode 100644 index 0000000..8fe4541 --- /dev/null +++ b/.github/workflows/style-check.yml @@ -0,0 +1,20 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +--- +name: Style checking +on: + pull_request: ~ + push: + branches: + - main + +jobs: + stylua: + name: stylua + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4.1.6 + - uses: JohnnyMorganz/stylua-action@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: latest + args: --color always --check lua/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..32fd026 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,22 @@ +--- +name: tests +on: + pull_request: ~ + push: + branches: + - main + +jobs: + build: + name: Run tests + runs-on: ubuntu-latest + strategy: + matrix: + neovim_version: ["nightly", "stable"] + + steps: + - uses: actions/checkout@v4 + - name: Run tests + uses: nvim-neorocks/nvim-busted-action@v1 + with: + nvim_version: ${{ matrix.neovim_version }} diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml new file mode 100644 index 0000000..7834fdc --- /dev/null +++ b/.github/workflows/type-check.yml @@ -0,0 +1,19 @@ +--- +name: Type Check +on: + pull_request: ~ + push: + branches: + - main + +jobs: + build: + name: Type Check Code Base + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Type Check Code Base + uses: mrcjkb/lua-typecheck-action@v1 diff --git a/.gitignore b/.gitignore index 0fe8f07..836f6ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -.zshrc.bak +.direnv +.pre-commit-config.yaml +minimal.vim +result diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..fe54381 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,9 @@ +globals = { + 'vim', + 'describe', + 'it', + 'before_each', + 'after_each', + 'assert', + 'async', +} diff --git a/.luarc.json b/.luarc.json new file mode 120000 index 0000000..8f4162f --- /dev/null +++ b/.luarc.json @@ -0,0 +1 @@ +/nix/store/k2cp6gm6qlcgvi8alp9kyz81bfgprrn5-luarc.json \ No newline at end of file diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..5f0d0aa --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,6 @@ +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 4 +quote_style = "AutoPreferSingle" +call_parentheses = "NoSingleTable" +collapse_simple_statement = "Never" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..07e7bb3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,106 @@ +## [2.0.1] - 2024-08-07 + +### Fix + +- Telescope display error + +## [2.0.0] - 2024-07-18 + +### Chore + +- Mv test repo to spec dir +- Fix stylua +- Luachecks +- Add name to commit-lint +- Refactor tests to hopefully pass + +### Ci + +- Add commit-lint workflow +- Switch to vusted for testing +- Add plenary install +- Add test for switching normal repo +- Add windows-latest test +- Add busted test to nix check +- Add bused tests +- Add type-check +- Add style check +- Add dependabot.yml +- Add convential commit checker +- Add luarocks release + +### Docs + +- Initial readme update +- Update plugin help docs +- Update readme +- Update changelog + +### Feat + +- V2 refactor +- Add ability to run luarocks non nix + +### Fix + +- Add hook to update current buffer on switch + +### Refactor + +- Stylua fixes and start to build from core +- Config +- Lost code +- Basic switch working +- Create worktree +- Delete worktree +- Delete worktree +- Add back in telescope +- To plenary test and other +- Final rework + +### Test + +- Add git ops tests + +## [1.0.0] - 2023-11-17 + +### Fix + +- Typo in README.md + +### Chore + +- Renamed :w to wip.lua +- Removed file +- Nixify +- Create LICENSE + +### Ci + +- Add luarocks release uploader + +### Feat + +- *(delete)* Allowed for deleting the buffer +- *(worktree-swap)* Better swapping and sane defaults +- *(create)* If rebase fails, we don't stop the creation process +- *(switch)* Clear jumps on switch. Can be configured +- *(status)* Added a status line printer +- *(readme)* Effectively correct +- *(on_tree_change)* Better interfacing with on_tree_change +- *(set_push)* Push so I can push with Git push + +### Feta + +- *(first-commit)* It "sorta" works. + +### Fix + +- *(indenting)* Tree shitter to the rescue +- *(status)* Status was 8 / 7 at some point. +- *(create_worktree)* Allows for worktree to also create the branch. +- Pass opts to git_worktrees +- Always return absolute git dir +- Merge mistake to prevent errors +- :Telescope git_worktree + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..98ba922 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 PolarMutex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index e5f2bc2..716015e 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,13 @@ +.PHONY: lint +lint: + luacheck ./lua + +# GIT_WORKTREE_NVIM_LOG=fatal +.PHONY: test test: - GIT_WORKTREE_NVIM_LOG=fatal nvim --headless --noplugin -u tests/minimal_init.vim -c "PlenaryBustedDirectory tests/ { minimal_init = './tests/minimal_init.vim' }" + # minimal.vim is generated when entering the flake, aka `nix develop` + nvim --headless -u minimal.vim -c "lua require('plenary.test_harness').test_directory('.', {minimal_init='minimal.vim'})" + +.PHONY: wintest +wintest: + vusted --output=gtest -m '.\plenary\lua\?.lua' -m '.\plenary\lua\?\?.lua' -m '.\plenary\lua\?\init.lua' ./lua diff --git a/README.md b/README.md index aaa8970..af71171 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,66 @@ -# git-worktree.nvim + -A simple wrapper around git worktree operations, create, switch, and delete. -There is some assumed workflow within this plugin, but pull requests are welcomed to -fix that). - - - -- [git-worktree.nvim](#git-worktreenvim) - - [Known Issues](#known-issues) - - [Dependencies](#dependencies) - - [Getting Started](#getting-started) - - [Setup](#setup) - - [Repository](#repository) - - [Options](#options) - - [Usage](#usage) - - [Telescope](#telescope) - - [Hooks](#hooks) - - [Made with fury](#made-with-fury) - - - -## Known Issues -There are a few known issues. I'll try to be actively filing them in the issues. If you experience something, and it's not an issue, feel free to make an issue! Even if it's a dupe I am just happy for the contribution. - -## Dependencies - -Requires NeoVim 0.5+ -Requires plenary.nvim -Optional telescope.nvim for telescope extension +![git-worktree.nvim](https://socialify.git.ci/polarmutex/git-worktree.nvim/image?font=Source%20Code%20Pro&name=1&stargazers=1&theme=Dark) -## Getting Started +[![Neovim][neovim-shield]][neovim-url] +[![Lua][lua-shield]][lua-url] +[![Nix][nix-shield]][nix-url] -First, install the plugin the usual way you prefer. + -```console -Plug 'ThePrimeagen/git-worktree.nvim' -``` +A simple wrapper around git worktree operations, create, switch, and delete. +There is some assumed workflow within this plugin, but pull requests are +welcomed to fix that). -Next, re-source your `vimrc`/`init.vim` and execute `PlugInstall` to ensure you have the plugin -installed. +## Quick Links -## Setup +## Prerequisites -## Repository +### Required -This repository does work best with a bare repo. To clone a bare repo, do the following. +- `neovim >= 0.9` +- `plenary.nvim` -```shell -git clone --bare -``` +### Optional -If you do not use a bare repo, using telescope create command will be more helpful in the process of creating a branch. +- [`telescope.nvim`](https://github.com/nvim-telescope/telescope.nvim) -### Debugging -git-worktree writes logs to a `git-worktree-nvim.log` file that resides in Neovim's cache path. (`:echo stdpath("cache")` to find where that is for you.) +## Installation -By default, logging is enabled for warnings and above. This can be changed by setting `vim.g.git_worktree_log_level` variable to one of the following log levels: `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. Note that this would have to be done **before** git-worktree's `setup` call. Alternatively, it can be more convenient to launch Neovim with an environment variable, e.g. `> GIT_WORKTREE_NVIM_LOG=trace nvim`. In case both, `vim.g` and an environment variable are used, the log level set by the environment variable overrules. Supplying an invalid log level defaults back to warnings. +This plugin is [available on LuaRocks][luarocks-url]: -### Troubleshooting -If the upstream is not setup correctly when trying to pull or push, make sure the following command returns what is shown below. This seems to happen with the gitHub cli. -``` -git config --get remote.origin.fetch - -+refs/heads/*:refs/remotes/origin/* -``` -if it does not run the following -``` -git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" +```lua +{ + 'polarmutex/git-worktree.nvim', + version = '^2', + dependencies = { "nvim-lua/plenary.nvim" } +} ``` -## Options - -`change_directory_command`: The vim command used to change to the new worktree directory. -Set this to `tcd` if you want to only change the `pwd` for the current vim Tab. +## Quick Setup -`update_on_change`: Updates the current buffer to point to the new work tree if -the file is found in the new project. Otherwise, the following command will be run. +This plugin does not require to call setup function, but you should setup your default hooks -`update_on_change_command`: The vim command to run during the `update_on_change` event. -Note, that this command will only be run when the current file is not found in the new worktree. -This option defaults to `e .` which opens the root directory of the new worktree. +Example Hook configuration -`clearjumps_on_change`: Every time you switch branches, your jumplist will be -cleared so that you don't accidentally go backward to a different branch and -edit the wrong files. +```lua +local Hooks = require("git-worktree.hooks") +local config = require('git-worktree.config') +local update_on_switch = Hooks.builtins.update_current_buffer_on_switch -`autopush`: When creating a new worktree, it will push the branch to the upstream then perform a `git rebase` +Hooks.register(Hooks.type.SWITCH, function (path, prev_path) + vim.notify("Moved from " .. prev_path .. " to " .. path) + update_on_switch(path, prev_path) +end) -```lua -require("git-worktree").setup({ - change_directory_command = -- default: "cd", - update_on_change = -- default: true, - update_on_change_command = -- default: "e .", - clearjumps_on_change = -- default: true, - autopush = -- default: false, -}) +Hooks.register(Hooks.type.DELETE, function () + vim.cmd(config.update_on_change_command) +end) ``` -## Usage +## Features + +## Usage Three primary functions should cover your day-to-day. @@ -106,79 +69,88 @@ The path can be either relative from the git root dir or absolute path to the wo ```lua -- Creates a worktree. Requires the path, branch name, and the upstream -- Example: -:lua require("git-worktree").create_worktree("feat-69", "master", "origin") +require("git-worktree").create_worktree("feat-69", "master", "origin") -- switches to an existing worktree. Requires the path name -- Example: -:lua require("git-worktree").switch_worktree("feat-69") +require("git-worktree").switch_worktree("feat-69") -- deletes to an existing worktree. Requires the path name -- Example: -:lua require("git-worktree").delete_worktree("feat-69") +require("git-worktree").delete_worktree("feat-69") ``` -## Telescope +## Advanced Configuration + +to modify the default configuration, set `vim.g.git_worktree`. -Add the following to your vimrc to load the telescope extension +- See [`:help git-worktree.config`](./doc/git-worktree.txt) for a detailed + documentation of all available configuration options. ```lua -require("telescope").load_extension("git_worktree") +vim.g.git_worktree = { + ... +} ``` -### Switch and Delete a worktrees -To bring up the telescope window listing your workspaces run the following +### Hooks + +Yes! The best part about `git-worktree` is that it emits information so that you +can act on it. ```lua -:lua require('telescope').extensions.git_worktree.git_worktrees() --- - switches to that worktree --- - deletes that worktree --- - toggles forcing of the next deletion +local Hooks = require("git-worktree.hooks") + +Hooks.register(Hooks.type.SWITCH, Hooks.builtins.update_current_buffer_on_switch) ``` -### Create a worktree -To bring up the telescope window to create a new worktree run the following +> [!IMPORTANT] +> +> - **no** builtins are registered +> by default and will have to be registered + +This means that you can use [harpoon](https://github.com/ThePrimeagen/harpoon) +or other plugins to perform follow up operations that will help in turbo +charging your development experience! + +### Telescope Config + +In order to use [Telescope](https://github.com/nvim-telescope/telescope.nvim) as a UI, +make sure to add `telescope` to your dependencies and paste this following snippet into your configuration. ```lua -:lua require('telescope').extensions.git_worktree.create_git_worktree() +require('telescope').load_extension('git_worktree') ``` -First a telescope git branch window will appear. Pressing enter will choose the selected branch for the branch name. If no branch is selected, then the prompt will be used as the branch name. -After the git branch window, a prompt will be presented to enter the path name to write the worktree to. +### Debugging -As of now you can not specify the upstream in the telescope create workflow, however if it finds a branch of the same name in the origin it will use it +git-worktree writes logs to a `git-worktree-nvim.log` file that resides in Neovim's cache path. (`:echo stdpath("cache")` to find where that is for you.) -## Hooks +By default, logging is enabled for warnings and above. This can be changed by setting `vim.g.git_worktree_log_level` variable to one of the following log levels: `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. Note that this would have to be done **before** git-worktree's `setup` call. Alternatively, it can be more convenient to launch Neovim with an environment variable, e.g. `> GIT_WORKTREE_NVIM_LOG=trace nvim`. In case both, `vim.g` and an environment variable are used, the log level set by the environment variable overrules. Supplying an invalid log level defaults back to warnings. -Yes! The best part about `git-worktree` is that it emits information so that you -can act on it. +### Troubleshooting + +If the upstream is not setup correctly when trying to pull or push, make sure the following command returns what is shown below. This seems to happen with the gitHub cli. ```lua -local Worktree = require("git-worktree") - --- op = Operations.Switch, Operations.Create, Operations.Delete --- metadata = table of useful values (structure dependent on op) --- Switch --- path = path you switched to --- prev_path = previous worktree path --- Create --- path = path where worktree created --- branch = branch name --- upstream = upstream remote name --- Delete --- path = path where worktree deleted - -Worktree.on_tree_change(function(op, metadata) - if op == Worktree.Operations.Switch then - print("Switched from " .. metadata.prev_path .. " to " .. metadata.path) - end -end) +git config --get remote.origin.fetch + ++refs/heads/*:refs/remotes/origin/* ``` -This means that you can use [harpoon](https://github.com/ThePrimeagen/harpoon) -or other plugins to perform follow up operations that will help in turbo -charging your development experience! +if it does not run the following + +```bash +git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" +``` -## Made with fury + -All plugins are made live on [Twitch](https://twitch.tv/ThePrimeagen) with love -and fury. Come and join! +[neovim-shield]: https://img.shields.io/badge/NeoVim-%2357A143.svg?&style=for-the-badge&logo=neovim&logoColor=white +[neovim-url]: https://neovim.io/ +[lua-shield]: https://img.shields.io/badge/lua-%232C2D72.svg?style=for-the-badge&logo=lua&logoColor=white +[lua-url]: https://www.lua.org/ +[nix-shield]: https://img.shields.io/badge/nix-0175C2?style=for-the-badge&logo=NixOS&logoColor=white +[nix-url]: https://nixos.org/ +[luarocks-shield]: https://img.shields.io/luarocks/v/MrcJkb/haskell-tools.nvim?logo=lua&color=purple&style=for-the-badge +[luarocks-url]: https://luarocks.org/modules/polarmutex/git-worktree.nvim diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..659158e --- /dev/null +++ b/cliff.toml @@ -0,0 +1 @@ + tag_pattern = "[0-9].*" diff --git a/doc/git-worktree.txt b/doc/git-worktree.txt new file mode 100644 index 0000000..aa19cd5 --- /dev/null +++ b/doc/git-worktree.txt @@ -0,0 +1,121 @@ +============================================================================== +Table of Contents *git-worktree.contents* + +Introduction ··························································· |intro| + ································································ |git-worktree| +plugin configuration ····································· |git-worktree.config| +hooks ····················································· |git-worktree.hooks| + +============================================================================== +Introduction *intro* + + A plugin that helps to use git worktree operations, create, switch, and delete in neovim. + +============================================================================== + *git-worktree* + + + +M.switch_worktree({path}) *M.switch_worktree* + + Parameters: ~ + {path} (string) + + + *M.create_worktree* +M.create_worktree({path}, {branch}, {upstream?}) + + Parameters: ~ + {path} (string) + {branch} (string) + {upstream?} (string) + + + *M.delete_worktree* +M.delete_worktree({path}, {force}, {opts}) + + Parameters: ~ + {path} (string) + {force} (boolean) + {opts} (any) + + +============================================================================== +plugin configuration *git-worktree.config* + + + git-worktree.nvim does not need a `setup` function to work. + +To configure git-worktree.nvim, set the variable `vim.g.git-worktree`, +which is a `GitWorktreeConfig` table, in your neovim configuration. + + The plugin configuration. + Merges the default config with `vim.g.git_worktree`. + +Example: + +>lua +---@type GitWorktreeConfig +vim.g.git_worktree = { + change_directory_command = 'cd', + update_on_change = true, + update_on_change_command = 'e .', + clearjumps_on_change = true, + confirm_telescope_deletions = true, + autopush = false, + } +< + + +GitWorktreeConfig *GitWorktreeConfig* + + Fields: ~ + {change_directory_command} (string) command to change directory on your OS + {update_on_change_command} (string) vim command to call to switch file buffer to new git-worktree + {clearjumps_on_change} (boolean) clear jump list on change + {confirm_telescope_deletions} (boolean) confirm telescope deletions operations + {autopush} (boolean) automatically push worktree to origin repo + + +============================================================================== +hooks *git-worktree.hooks* + +git-worktree.hooks.cb.create *git-worktree.hooks.cb.create* + + Type: ~ + fun(path:string,branch:string,upstream:string) + + +git-worktree.hooks.cb.delete *git-worktree.hooks.cb.delete* + + Type: ~ + fun(path:string) + + +git-worktree.hooks.cb.switch *git-worktree.hooks.cb.switch* + + Type: ~ + fun(path:string,prev_path:string) + + +M.register({type}, {cb}) *M.register* + Registers a hook + + Each hook type takes a callback a different function + + Parameters: ~ + {type} (git-worktree.hooks.type) + {cb} (function) @overload fun(type: 'CREATE', cb: git-worktree.hooks.cb.create): string + @overload fun(type: 'DELETE', cb: git-worktree.hooks.cb.delete): string + @overload fun(type: 'SWITCH', cb: git-worktree.hooks.cb.switch): string + + +M.emit({type}, {...}) *M.emit* + Emits an event and calls all the hook callbacks registered + + Parameters: ~ + {type} (git-worktree.hooks.type) + {...} (any) + + +vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9c349ed --- /dev/null +++ b/flake.lock @@ -0,0 +1,583 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_3": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_4": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_5": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1719994518, + "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_2" + }, + "locked": { + "lastModified": 1717285511, + "narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_3": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_3" + }, + "locked": { + "lastModified": 1719994518, + "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_4": { + "inputs": { + "nixpkgs-lib": [ + "neorocks", + "neovim-nightly", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1719994518, + "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_5": { + "inputs": { + "nixpkgs-lib": [ + "neorocks", + "neovim-nightly", + "hercules-ci-effects", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1712014858, + "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", + "type": "github" + }, + "original": { + "id": "flake-parts", + "type": "indirect" + } + }, + "gen-luarc": { + "inputs": { + "flake-parts": "flake-parts_2", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1718922730, + "narHash": "sha256-ykhhOPqA9NzdNBr3ii+3h2DkK2+wasNqQLfMF6BXxTE=", + "owner": "mrcjkb", + "repo": "nix-gen-luarc-json", + "rev": "021e8078e43884c6cdc70ca753d9a0b146cd55a4", + "type": "github" + }, + "original": { + "owner": "mrcjkb", + "repo": "nix-gen-luarc-json", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat_2", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs_2", + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1721042469, + "narHash": "sha256-6FPUl7HVtvRHCCBQne7Ylp4p+dpP3P/OYuzjztZ4s70=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "f451c19376071a90d8c58ab1a953c6e9840527fd", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "git-hooks_2": { + "inputs": { + "flake-compat": "flake-compat_4", + "gitignore": "gitignore_2", + "nixpkgs": [ + "neorocks", + "neovim-nightly", + "nixpkgs" + ], + "nixpkgs-stable": [ + "neorocks", + "neovim-nightly", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1721042469, + "narHash": "sha256-6FPUl7HVtvRHCCBQne7Ylp4p+dpP3P/OYuzjztZ4s70=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "f451c19376071a90d8c58ab1a953c6e9840527fd", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "neorocks", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gitignore_2": { + "inputs": { + "nixpkgs": [ + "neorocks", + "neovim-nightly", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gitignore_3": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "hercules-ci-effects": { + "inputs": { + "flake-parts": "flake-parts_5", + "nixpkgs": [ + "neorocks", + "neovim-nightly", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1719226092, + "narHash": "sha256-YNkUMcCUCpnULp40g+svYsaH1RbSEj6s4WdZY/SHe38=", + "owner": "hercules-ci", + "repo": "hercules-ci-effects", + "rev": "11e4b8dc112e2f485d7c97e1cee77f9958f498f5", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "hercules-ci-effects", + "type": "github" + } + }, + "neorocks": { + "inputs": { + "flake-compat": "flake-compat", + "flake-parts": "flake-parts_3", + "git-hooks": "git-hooks", + "neovim-nightly": "neovim-nightly", + "nixpkgs": "nixpkgs_4" + }, + "locked": { + "lastModified": 1721193886, + "narHash": "sha256-pjLc7CavrqEsxWmyNdp7SLSbFoAyPAHBPqw6/8U9Ixs=", + "owner": "nvim-neorocks", + "repo": "neorocks", + "rev": "f3b6b979cd746e73ed4cc2276830da1d2e076ffc", + "type": "github" + }, + "original": { + "owner": "nvim-neorocks", + "repo": "neorocks", + "type": "github" + } + }, + "neovim-nightly": { + "inputs": { + "flake-compat": "flake-compat_3", + "flake-parts": "flake-parts_4", + "git-hooks": "git-hooks_2", + "hercules-ci-effects": "hercules-ci-effects", + "neovim-src": "neovim-src", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1721175028, + "narHash": "sha256-3iNMy+GUVaFVHv2U+g6cMJwouAJinDkT/TLqZpiwsU8=", + "owner": "nix-community", + "repo": "neovim-nightly-overlay", + "rev": "d0040404432ef2f6492d06284c556303262e5054", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "neovim-nightly-overlay", + "type": "github" + } + }, + "neovim-src": { + "flake": false, + "locked": { + "lastModified": 1721152134, + "narHash": "sha256-mKvJmYNz0d+irdQFtUrkFtHY6LgE1SxoT14Zmbn1OXU=", + "owner": "neovim", + "repo": "neovim", + "rev": "1f2f460b4a77a8ff58872e03c071b5d0d882dd44", + "type": "github" + }, + "original": { + "owner": "neovim", + "repo": "neovim", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1718714799, + "narHash": "sha256-FUZpz9rg3gL8NVPKbqU8ei1VkPLsTIfAJ2fdAf5qjak=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "c00d587b1a1afbf200b1d8f0b0e4ba9deb1c7f0e", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1719876945, + "narHash": "sha256-Fm2rDDs86sHy0/1jxTOKB1118Q0O3Uc7EC0iXvXKpbI=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz" + } + }, + "nixpkgs-lib_2": { + "locked": { + "lastModified": 1717284937, + "narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" + } + }, + "nixpkgs-lib_3": { + "locked": { + "lastModified": 1719876945, + "narHash": "sha256-Fm2rDDs86sHy0/1jxTOKB1118Q0O3Uc7EC0iXvXKpbI=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1720386169, + "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "194846768975b7ad2c4988bdb82572c00222c0d7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable_2": { + "locked": { + "lastModified": 1720386169, + "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "194846768975b7ad2c4988bdb82572c00222c0d7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1719082008, + "narHash": "sha256-jHJSUH619zBQ6WdC21fFAlDxHErKVDJ5fpN0Hgx4sjs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9693852a2070b398ee123a329e68f0dab5526681", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1721116560, + "narHash": "sha256-++TYlGMAJM1Q+0nMVaWBSEvEUjRs7ZGiNQOpqbQApCU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9355fa86e6f27422963132c2c9aeedb0fb963d93", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1721116560, + "narHash": "sha256-++TYlGMAJM1Q+0nMVaWBSEvEUjRs7ZGiNQOpqbQApCU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9355fa86e6f27422963132c2c9aeedb0fb963d93", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_5": { + "locked": { + "lastModified": 1721116560, + "narHash": "sha256-++TYlGMAJM1Q+0nMVaWBSEvEUjRs7ZGiNQOpqbQApCU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9355fa86e6f27422963132c2c9aeedb0fb963d93", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat_5", + "gitignore": "gitignore_3", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-stable": "nixpkgs-stable_2" + }, + "locked": { + "lastModified": 1721042469, + "narHash": "sha256-6FPUl7HVtvRHCCBQne7Ylp4p+dpP3P/OYuzjztZ4s70=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "f451c19376071a90d8c58ab1a953c6e9840527fd", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "gen-luarc": "gen-luarc", + "neorocks": "neorocks", + "nixpkgs": "nixpkgs_5", + "pre-commit-hooks": "pre-commit-hooks" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..9d6ca95 --- /dev/null +++ b/flake.nix @@ -0,0 +1,211 @@ +{ + description = "git-worktree.nvim - supercharge your haskell experience in neovim"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + + pre-commit-hooks = { + url = "github:cachix/pre-commit-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + neorocks.url = "github:nvim-neorocks/neorocks"; + gen-luarc.url = "github:mrcjkb/nix-gen-luarc-json"; + + # neovim = { + # url = "github:neovim/neovim?dir=contrib"; + # inputs.nixpkgs.follows = "nixpkgs"; + # }; + # neodev-nvim = { + # url = "github:folke/neodev.nvim"; + # flake = false; + # }; + # plenary-nvim = { + # url = "github:nvim-lua/plenary.nvim"; + # flake = false; + # }; + # telescope-nvim = { + # url = "github:nvim-telescope/telescope.nvim"; + # flake = false; + # }; + }; + + outputs = inputs @ { + self, + flake-parts, + ... + }: + flake-parts.lib.mkFlake {inherit inputs;} { + systems = [ + "aarch64-linux" + "aarch64-darwin" + "x86_64-darwin" + "x86_64-linux" + ]; + perSystem = { + config, + pkgs, + system, + inputs', + ... + }: let + luarc-plugins = with pkgs.lua51Packages; (with pkgs.vimPlugins; [ + telescope-nvim + plenary-nvim + ]); + + luarc-nightly = pkgs.mk-luarc { + nvim = pkgs.neovim-nightly; + plugins = luarc-plugins; + }; + + luarc-stable = pkgs.mk-luarc { + nvim = pkgs.neovim-unwrapped; + plugins = luarc-plugins; + disabled-diagnostics = [ + #"undefined-doc-name" + #"redundant-parameter" + #"invisible" + ]; + }; + + pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run { + src = self; + hooks = { + alejandra.enable = true; + stylua.enable = true; + luacheck.enable = true; + #markdownlint.enable = true; + }; + }; + in { + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ + inputs.neorocks.overlays.default + inputs.gen-luarc.overlays.default + (final: _: { + # neovim-nightly = inputs.neovim.packages.${final.system}.neovim; + }) + ]; + }; + devShells = let + mkDevShell = luaVersion: let + luaEnv = pkgs."lua${luaVersion}".withPackages (lp: + with lp; [ + busted + luacheck + luarocks + ]); + in + pkgs.mkShell { + buildInputs = [ + luaEnv + ]; + shellHook = let + myVimPackage = with pkgs.vimPlugins; { + start = [ + plenary-nvim + ]; + }; + packDirArgs.myNeovimPackages = myVimPackage; + in + pre-commit-check.shellHook + + '' + export DEBUG_PLENARY="debug" + cat <<-EOF > minimal.vim + set rtp+=. + set packpath^=${pkgs.vimUtils.packDir packDirArgs} + EOF + ''; + }; + in { + default = let + in + pkgs.mkShell { + name = "git-worktree-nvim-shell"; + shellHook = '' + ${pre-commit-check.shellHook} + ln -fs ${pkgs.luarc-to-json luarc-nightly} .luarc.json + ''; + buildInputs = + self.checks.${system}.pre-commit-check.enabledPackages + ++ (with pkgs; [ + lua-language-server + busted-nlua + (lua5_1.withPackages (ps: + with ps; [ + luarocks + plenary-nvim + ])) + git-cliff + ]); + }; + }; + + packages = let + docgen = pkgs.callPackage ./nix/docgen.nix {}; + in { + inherit docgen; + }; + # packages.neodev-plugin = pkgs.vimUtils.buildVimPlugin { + # name = "neodev.nvim"; + # src = inputs.neodev-nvim; + # }; + # packages.plenary-plugin = pkgs.vimUtils.buildVimPlugin { + # name = "plenary.nvim"; + # src = inputs.plenary-nvim; + # }; + # packages.telescope-plugin = pkgs.vimUtils.buildVimPlugin { + # name = "telescope.nvim"; + # src = inputs.telescope-nvim; + # }; + + checks = let + type-check-stable = inputs.pre-commit-hooks.lib.${system}.run { + src = self; + hooks = { + lua-ls = { + enable = true; + settings.configuration = luarc-stable; + }; + }; + }; + + type-check-nightly = inputs.pre-commit-hooks.lib.${system}.run { + src = self; + hooks = { + lua-ls = { + enable = true; + settings.configuration = luarc-nightly; + }; + }; + }; + + neorocks-test = pkgs.neorocksTest { + src = self; # Project containing the rockspec and .busted files. + # Plugin name. If running multiple tests, + # you can use pname for the plugin name instead + name = "git-worktree.nvim"; + # version = "scm-1"; # Optional, defaults to "scm-1"; + neovim = pkgs.neovim-nightly; # Optional, defaults to neovim-nightly. + luaPackages = ps: + # Optional + with ps; [ + # LuaRocks dependencies must be added here. + plenary-nvim + ]; + extraPackages = with pkgs; [ + gitMinimal + ]; # Optional. External test runtime dependencies. + }; + in { + inherit pre-commit-check; + inherit type-check-stable; + inherit type-check-nightly; + inherit neorocks-test; + }; + }; + }; +} diff --git a/git-worktree.nvim-scm-1.rockspec b/git-worktree.nvim-scm-1.rockspec new file mode 100644 index 0000000..ccfb0db --- /dev/null +++ b/git-worktree.nvim-scm-1.rockspec @@ -0,0 +1,28 @@ +-- NOTE: This rockspec is used for running busted tests only, +-- not for publishing to LuaRocks.org + +local _MODREV, _SPECREV = 'scm', '-1' +rockspec_format = '3.0' +package = 'git-worktree.nvim' +version = _MODREV .. _SPECREV + +dependencies = { + 'lua >= 5.1', + 'plenary.nvim' +} + +test_dependencies = { + 'lua >= 5.1', + 'plenary.nvim' +} + +source = { + url = 'git://github.com/polarmutex/' .. package, +} + +build = { + type = 'builtin', + copy_directories = { + 'doc', + }, +} diff --git a/lua/git-worktree/config.lua b/lua/git-worktree/config.lua new file mode 100644 index 0000000..d159229 --- /dev/null +++ b/lua/git-worktree/config.lua @@ -0,0 +1,70 @@ +---@mod git-worktree.config plugin configuration +--- +---@brief [[ +--- +--- git-worktree.nvim does not need a `setup` function to work. +--- +---To configure git-worktree.nvim, set the variable `vim.g.git-worktree`, +---which is a `GitWorktreeConfig` table, in your neovim configuration. +--- + +--- The plugin configuration. +--- Merges the default config with `vim.g.git_worktree`. +--- +---Example: +--- +--->lua +------@type GitWorktreeConfig +---vim.g.git_worktree = { +--- change_directory_command = 'cd', +--- update_on_change = true, +--- update_on_change_command = 'e .', +--- clearjumps_on_change = true, +--- confirm_telescope_deletions = true, +--- autopush = false, +--- } +---< +--- + +---@brief ]] + +---@class GitWorktreeConfig +---@field change_directory_command string command to change directory on your OS +---@field update_on_change_command string vim command to call to switch file buffer to new git-worktree +---@field clearjumps_on_change boolean clear jump list on change +---@field confirm_telescope_deletions boolean confirm telescope deletions operations +---@field autopush boolean automatically push worktree to origin repo + +---@type (fun():GitWorktreeConfig) | GitWorktreeConfig | nil +vim.g.git_worktree = vim.g.git_worktree + +local GitWorktreeDefaultConfig = { + + -- command to change directory on your OS. + --- @type string + change_directory_command = 'cd', + + -- vim command to call to switch file buffer to new git-worktree + --- @type string + update_on_change_command = 'e .', + + -- clear jump list on change + --- @type boolean + clearjumps_on_change = true, + + -- confirm telescope deletions operations + --- @type boolean + confirm_telescope_deletions = true, + + -- automatically push worktree to origin repo + --- @type boolean + autopush = false, +} + +local git_worktree = vim.g.git_worktree or {} +---@type GitWorktreeConfig +local opts = type(git_worktree) == 'function' and git_worktree() or git_worktree + +local GitWorktreeConfig = vim.tbl_deep_extend('force', {}, GitWorktreeDefaultConfig, opts) + +return GitWorktreeConfig diff --git a/lua/git-worktree/enum.lua b/lua/git-worktree/enum.lua deleted file mode 100644 index a98b6f7..0000000 --- a/lua/git-worktree/enum.lua +++ /dev/null @@ -1,22 +0,0 @@ -local Enum = function(tbl) - return setmetatable(tbl, { - __index = function(_, key) - error(string.format("%s does not exist for this enum.", key)) - end, - - __newindex = function(t, key, value) - error("Enums are immutable. You are not able to set new values") - end, - }) -end - -return { - Operations = Enum({ - Create = "create", - Switch = "switch", - Delete = "delete", - }) -} - - - diff --git a/lua/git-worktree/git.lua b/lua/git-worktree/git.lua new file mode 100644 index 0000000..c245bfe --- /dev/null +++ b/lua/git-worktree/git.lua @@ -0,0 +1,304 @@ +local Job = require('plenary.job') +local Path = require('plenary.path') +local Log = require('git-worktree.logger') + +---@class GitWorktreeGitOps +local M = {} + +-- A lot of this could be cleaned up if there was better job -> job -> function +-- communication. That should be doable here in the near future +--- +---@param path_str string path to the worktree to check +---@param branch string? branch the worktree is associated with +---@param cb any +function M.has_worktree(path_str, branch, cb) + local found = false + local path + + if path_str == '.' then + path_str = vim.loop.cwd() + end + + path = Path:new(path_str) + if not path:is_absolute() then + path = Path:new(string.format('%s' .. Path.path.sep .. '%s', vim.loop.cwd(), path_str)) + end + path = path:absolute() + + Log.debug('has_worktree: %s %s', path, branch) + + local job = Job:new { + command = 'git', + args = { 'worktree', 'list', '--porcelain' }, + on_stdout = function(_, line) + if line:match('^worktree ') then + local current_worktree = Path:new(line:match('^worktree (.+)$')):absolute() + Log.debug('current_worktree: "%s"', current_worktree) + if path == current_worktree then + found = true + return + end + elseif branch ~= nil and line:match('^branch ') then + local worktree_branch = line:match('^branch (.+)$') + Log.debug('worktree_branch: %s', worktree_branch) + if worktree_branch == 'refs/heads/' .. branch then + found = true + return + end + end + end, + cwd = vim.loop.cwd(), + } + + job:after(function() + Log.debug('calling after') + cb(found) + end) + + Log.debug('Checking for worktree %s', path) + job:start() +end + +--- @return string|nil +function M.gitroot_dir() + local job = Job:new { + command = 'git', + args = { 'rev-parse', '--path-format=absolute', '--git-common-dir' }, + cwd = vim.loop.cwd(), + on_stderr = function(_, data) + Log.error('ERROR: ' .. data) + end, + } + + local stdout, code = job:sync() + if code ~= 0 then + Log.error( + 'Error in determining the git root dir: code:' + .. tostring(code) + .. ' out: ' + .. table.concat(stdout, '') + .. '.' + ) + return nil + end + + return table.concat(stdout, '') +end + +--- @return string|nil +function M.toplevel_dir() + local job = Job:new { + command = 'git', + args = { 'rev-parse', '--path-format=absolute', '--show-toplevel' }, + cwd = vim.loop.cwd(), + on_stderr = function(_, data) + Log.error('ERROR: ' .. data) + end, + } + + local stdout, code = job:sync() + if code ~= 0 then + Log.error( + 'Error in determining the git root dir: code:' + .. tostring(code) + .. ' out: ' + .. table.concat(stdout, '') + .. '.' + ) + return nil + end + + return table.concat(stdout, '') +end + +function M.has_branch(branch, opts, cb) + local found = false + local args = { 'branch', '--format=%(refname:short)' } + opts = opts or {} + for _, opt in ipairs(opts) do + args[#args + 1] = opt + end + + local job = Job:new { + command = 'git', + args = args, + on_stdout = function(_, data) + found = found or data == branch + end, + cwd = vim.loop.cwd(), + } + + -- TODO: I really don't want status's spread everywhere... seems bad + job:after(function() + cb(found) + end):start() +end + +--- @param path string +--- @param branch string? +--- @param found_branch boolean +--- @param upstream string +--- @param found_upstream boolean +--- @return Job +function M.create_worktree_job(path, branch, found_branch, upstream, found_upstream) + local worktree_add_cmd = 'git' + local worktree_add_args = { 'worktree', 'add' } + + if branch == nil then + table.insert(worktree_add_args, '-d') + table.insert(worktree_add_args, path) + else + if not found_branch then + table.insert(worktree_add_args, '-b') + table.insert(worktree_add_args, branch) + table.insert(worktree_add_args, path) + + if found_upstream and branch ~= upstream then + table.insert(worktree_add_args, '--track') + table.insert(worktree_add_args, upstream) + end + else + table.insert(worktree_add_args, path) + table.insert(worktree_add_args, branch) + end + end + + return Job:new { + command = worktree_add_cmd, + args = worktree_add_args, + cwd = vim.loop.cwd(), + on_start = function() + Log.debug(worktree_add_cmd .. ' ' .. table.concat(worktree_add_args, ' ')) + end, + } +end + +--- @param path string +--- @param force boolean +--- @return Job +function M.delete_worktree_job(path, force) + local worktree_del_cmd = 'git' + local worktree_del_args = { 'worktree', 'remove', path } + + if force then + table.insert(worktree_del_args, '--force') + end + + return Job:new { + command = worktree_del_cmd, + args = worktree_del_args, + cwd = vim.loop.cwd(), + on_start = function() + Log.debug(worktree_del_cmd .. ' ' .. table.concat(worktree_del_args, ' ')) + end, + } +end + +--- @param path string +--- @return Job +function M.fetchall_job(path) + return Job:new { + command = 'git', + args = { 'fetch', '--all' }, + cwd = path, + on_start = function() + Log.debug('git fetch --all (This may take a moment)') + end, + } +end + +--- @param path string +--- @param branch string +--- @param upstream string +--- @return Job +function M.setbranch_job(path, branch, upstream) + local set_branch_cmd = 'git' + local set_branch_args = { 'branch', branch, string.format('--set-upstream-to=%s', upstream) } + return Job:new { + command = set_branch_cmd, + args = set_branch_args, + cwd = path, + on_start = function() + Log.debug(set_branch_cmd .. ' ' .. table.concat(set_branch_args, ' ')) + end, + } +end + +--- @param path string +--- @param branch string +--- @param upstream string +--- @return Job +function M.setpush_job(path, branch, upstream) + -- TODO: How to configure origin??? Should upstream ever be the push + -- destination? + local set_push_cmd = 'git' + local set_push_args = { 'push', '--set-upstream', upstream, branch, path } + return Job:new { + command = set_push_cmd, + args = set_push_args, + cwd = path, + on_start = function() + Log.debug(set_push_cmd .. ' ' .. table.concat(set_push_args, ' ')) + end, + } +end + +--- @param path string +--- @return Job +function M.rebase_job(path) + return Job:new { + command = 'git', + args = { 'rebase' }, + cwd = path, + on_start = function() + Log.debug('git rebase') + end, + } +end + +--- @param path string +--- @return string|nil +function M.parse_head(path) + local job = Job:new { + command = 'git', + args = { 'rev-parse', '--abbrev-ref', 'HEAD' }, + cwd = path, + on_start = function() + Log.debug('git rev-parse --abbrev-ref HEAD') + end, + } + + local stdout, code = job:sync() + if code ~= 0 then + Log.error('Error in parsing the HEAD: code:' .. tostring(code) .. ' out: ' .. table.concat(stdout, '') .. '.') + return nil + end + + return table.concat(stdout, '') +end + +--- @param branch string +--- @return Job|nil +function M.delete_branch_job(branch) + local root = M.gitroot_dir() + if root == nil then + return nil + end + + local default = M.parse_head(root) + if default == branch then + print('Refusing to delete default branch') + return nil + end + + return Job:new { + command = 'git', + args = { 'branch', '-D', branch }, + cwd = M.gitroot_dir(), + on_start = function() + Log.debug('git branch -D') + end, + } +end + +return M diff --git a/lua/git-worktree/hooks.lua b/lua/git-worktree/hooks.lua new file mode 100644 index 0000000..aaf934f --- /dev/null +++ b/lua/git-worktree/hooks.lua @@ -0,0 +1,104 @@ +---@mod git-worktree.hooks hooks + +local M = {} + +---@enum git-worktree.hooks.type +M.type = { + CREATE = 'CREATE', + DELETE = 'DELETE', + SWITCH = 'SWITCH', +} + +local hooks = { + [M.type.CREATE] = {}, + [M.type.DELETE] = {}, + [M.type.SWITCH] = {}, +} +local count = 0 + +---@alias git-worktree.hooks.cb.create fun(path: string, branch: string, upstream: string) +---@alias git-worktree.hooks.cb.delete fun(path: string) +---@alias git-worktree.hooks.cb.switch fun(path: string, prev_path: string) + +--- Registers a hook +--- +--- Each hook type takes a callback a different function +---@param type git-worktree.hooks.type +---@param cb function +---@overload fun(type: 'CREATE', cb: git-worktree.hooks.cb.create): string +---@overload fun(type: 'DELETE', cb: git-worktree.hooks.cb.delete): string +---@overload fun(type: 'SWITCH', cb: git-worktree.hooks.cb.switch): string +M.register = function(type, cb) + count = count + 1 + local hook_id = type .. '_' .. tostring(count) + hooks[type][hook_id] = cb + return hook_id +end + +--- Emits an event and calls all the hook callbacks registered +---@param type git-worktree.hooks.type +---@param ... any +function M.emit(type, ...) + for _, hook in pairs(hooks[type]) do + hook(...) + end +end + +local Path = require('plenary.path') + +--- Built in hooks +--- +--- You can register them yourself using `hooks.register` +--- +--- +--- hooks.register( +--- hooks.type.SWTICH, +--- hooks.builtins.update_current_buffer_on_switch, +--- ) +--- +M.builtins = { + ---@type git-worktree.hooks.cb.switch + update_current_buffer_on_switch = function(_, prev_path) + local config = require('git-worktree.config') + local update_cmd = function() + vim.cmd(config.update_on_change_command) + end + if prev_path == nil then + update_cmd() + return + end + + local cwd = vim.loop.cwd() + local current_buf_name = vim.api.nvim_buf_get_name(0) + if not current_buf_name or current_buf_name == '' then + update_cmd() + return + end + + local name = Path:new(current_buf_name):absolute() + local start1, _ = string.find(name, cwd .. Path.path.sep, 1, true) + if start1 ~= nil then + return + end + + local start, fin = string.find(name, prev_path, 1, true) + if start == nil then + update_cmd() + return + end + + local local_name = name:sub(fin + 2) + + local final_path = Path:new({ cwd, local_name }):absolute() + + if not Path:new(final_path):exists() then + update_cmd() + return + end + + local bufnr = vim.fn.bufnr(final_path, true) + vim.api.nvim_set_current_buf(bufnr) + end, +} + +return M diff --git a/lua/git-worktree/init.lua b/lua/git-worktree/init.lua index 0fb5295..4ebd56e 100644 --- a/lua/git-worktree/init.lua +++ b/lua/git-worktree/init.lua @@ -1,556 +1,47 @@ -local Job = require("plenary.job") -local Path = require("plenary.path") -local Enum = require("git-worktree.enum") +---@toc git-worktree.contents + +---@mod intro Introduction +---@brief [[ +--- A plugin that helps to use git worktree operations, create, switch, and delete in neovim. +---@brief ]] +--- +---@mod git-worktree + +---@mod git-worktree.telescope +---@brief [[ +--- To get telescope-file-browser loaded and working with telescope, +--- you need to call load_extension, somewhere after setup function: +--- +--- require("telescope").load_extension("git_worktree") +---@brief ]] + +---@brief [[ +---@brief ]] -local Status = require("git-worktree.status") - -local status = Status:new() local M = {} -local git_worktree_root = nil -local current_worktree_path = nil -local on_change_callbacks = {} - -M.setup_git_info = function() - local cwd = vim.loop.cwd() - - local is_in_worktree = false - - local inside_worktree_job = Job:new({ - 'git', 'rev-parse', '--is-inside-work-tree', - cwd = cwd, - }) - - local process_inside_worktree = function(stdout) - if stdout == "true" then - is_in_worktree = true - end - end - - local find_git_dir_job = Job:new({ - 'git', 'rev-parse', '--absolute-git-dir', - cwd = cwd, - }) - - local process_find_git_dir = function(stdout) - if is_in_worktree then - -- if in worktree git dir returns absolute path - - -- try to find the dot git folder (non-bare repo) - local git_dir = Path:new(stdout) - local has_dot_git = false - for _, dir in ipairs(git_dir:_split()) do - if dir == ".git" then - has_dot_git = true - break - end - end - - if has_dot_git then - if stdout == ".git" then - git_worktree_root = cwd - else - local start = stdout:find("%.git") - git_worktree_root = stdout:sub(1,start - 2) - end - else - local start = stdout:find("/worktrees/") - git_worktree_root = stdout:sub(0, start - 1) - end - elseif stdout == "." then - -- we are in the root git dir - git_worktree_root = cwd - else - -- if not in worktree git dir should be absolute - git_worktree_root = stdout - end - status:log():debug("git directory is: " .. git_worktree_root) - end - - local find_toplevel_job = Job:new({ - 'git', 'rev-parse', '--show-toplevel', - cwd = cwd, - }) - - local find_toplevel_bare_job = Job:new({ - 'git', 'rev-parse', '--is-bare-repository', - cwd = cwd, - }) - - local process_find_toplevel = function(stdout) - current_worktree_path = stdout - status:log().debug("git toplevel is: " .. current_worktree_path) - end - - local process_find_toplevel_bare = function(stdout) - if stdout == "true" then - current_worktree_path = cwd - end - end - - local stdout, code = inside_worktree_job:sync() - if code ~= 0 then - status:log().error("Error in determining if we are in a worktree") - git_worktree_root = nil - current_worktree_path = nil - return - end - stdout = table.concat(stdout, "") - process_inside_worktree(stdout) - - stdout, code = find_git_dir_job:sync() - if code ~= 0 then - status:log().error("Error in determining the git root dir") - git_worktree_root = nil - return - end - stdout = table.concat(stdout, "") - process_find_git_dir(stdout) - - stdout, code = find_toplevel_job:sync() - if code == 0 then - stdout = table.concat(stdout, "") - process_find_toplevel(stdout) - else - stdout, code = find_toplevel_bare_job:sync() - if code == 0 then - stdout = table.concat(stdout, "") - process_find_toplevel_bare(stdout) - else - status:log().error("Error in determining the git toplevel") - current_worktree_path = nil - return - end - end - -end - -local function on_tree_change_handler(op, metadata) - if M._config.update_on_change then - if op == Enum.Operations.Switch then - local changed = M.update_current_buffer(metadata["prev_path"]) - if not changed then - status:log().debug("Could not change to the file in the new worktree, running the `update_on_change_command`") - vim.cmd(M._config.update_on_change_command) - end - end - end -end - -local function emit_on_change(op, metadata) - -- TODO: We don't have a way to async update what is running - status:next_status(string.format("Running post %s callbacks", op)) - on_tree_change_handler(op, metadata) - for idx = 1, #on_change_callbacks do - on_change_callbacks[idx](op, metadata) - end -end - -local function change_dirs(path) - local worktree_path = M.get_worktree_path(path) - - local previous_worktree = current_worktree_path - - -- vim.loop.chdir(worktree_path) - if Path:new(worktree_path):exists() then - local cmd = string.format("%s %s", M._config.change_directory_command, worktree_path) - status:log().debug("Changing to directory " .. worktree_path) - vim.cmd(cmd) - current_worktree_path = worktree_path - else - status:error('Could not chang to directory: ' ..worktree_path) - end - - if M._config.clearjumps_on_change then - status:log().debug("Clearing jumps") - vim.cmd("clearjumps") - end - - return previous_worktree -end - -local function create_worktree_job(path, branch, found_branch) - - local worktree_add_cmd = 'git' - local worktree_add_args = {'worktree', 'add'} - - if not found_branch then - table.insert(worktree_add_args, '-b') - table.insert(worktree_add_args, branch) - table.insert(worktree_add_args, path) - else - table.insert(worktree_add_args, path) - table.insert(worktree_add_args, branch) - end - - return Job:new({ - command = worktree_add_cmd, - args = worktree_add_args, - cwd = git_worktree_root, - on_start = function() - status:next_status(worktree_add_cmd .. " " .. table.concat(worktree_add_args, " ")) - end - }) -end - --- A lot of this could be cleaned up if there was better job -> job -> function --- communication. That should be doable here in the near future -local function has_worktree(path, cb) - local found = false - local plenary_path = Path:new(path) - - local job = Job:new({ - 'git', 'worktree', 'list', on_stdout = function(_, data) - - local list_data = {} - for section in data:gmatch("%S+") do - table.insert(list_data, section) - end - - data = list_data[1] - - local start - if plenary_path:is_absolute() then - start = data == path - else - local worktree_path = Path:new( - string.format("%s" .. Path.path.sep .. "%s", git_worktree_root, path) - ) - worktree_path = worktree_path:absolute() - start = data == worktree_path - end - - -- TODO: This is clearly a hack (do not think we need this anymore?) - local start_with_head = string.find(data, string.format("[heads/%s]", path), 1, true) - found = found or start or start_with_head - end, - cwd = git_worktree_root - }) - - job:after(function() - cb(found) - end) - - -- TODO: I really don't want status's spread everywhere... seems bad - status:next_status("Checking for worktree " .. path) - job:start() -end - -local function failure(from, cmd, path, soft_error) - return function(e) - local error_message = string.format( - "%s Failed: PATH %s CMD %s RES %s, ERR %s", - from, - path, - vim.inspect(cmd), - vim.inspect(e:result()), - vim.inspect(e:stderr_result())) - - if soft_error then - status:status(error_message) - else - status:error(error_message) - end - end -end - -local function has_origin() - local found = false - local job = Job:new({ - 'git', 'remote', 'show', - on_stdout = function(_, data) - data = vim.trim(data) - found = found or data == 'origin' - end, - cwd = git_worktree_root, - }) - - -- TODO: I really don't want status's spread everywhere... seems bad - job:after(function() - status:status("found origin: " .. tostring(found)) - end):sync() - - return found -end - -local function has_branch(branch, cb) - local found = false - local job = Job:new({ - 'git', 'branch', on_stdout = function(_, data) - -- remove markere on current branch - data = data:gsub("*","") - data = vim.trim(data) - found = found or data == branch - end, - cwd = git_worktree_root, - }) - - -- TODO: I really don't want status's spread everywhere... seems bad - status:next_status(string.format("Checking for branch %s", branch)) - job:after(function() - status:status("found branch: " .. tostring(found)) - cb(found) - end):start() -end - -local function create_worktree(path, branch, upstream, found_branch) - local create = create_worktree_job(path, branch, found_branch) - - local worktree_path - if Path:new(path):is_absolute() then - worktree_path = path - else - worktree_path = Path:new(git_worktree_root, path):absolute() - end - local fetch = Job:new({ - 'git', 'fetch', '--all', - cwd = worktree_path, - on_start = function() - status:next_status("git fetch --all (This may take a moment)") - end - }) +local Worktree = require('git-worktree.worktree') - local set_branch_cmd = 'git' - local set_branch_args= {'branch', string.format('--set-upstream-to=%s/%s', upstream, branch)} - local set_branch = Job:new({ - command = set_branch_cmd, - args = set_branch_args, - cwd = worktree_path, - on_start = function() - status:next_status(set_branch_cmd .. " " .. table.concat(set_branch_args, " ")) - end - }) - - -- TODO: How to configure origin??? Should upstream ever be the push - -- destination? - local set_push_cmd = 'git' - local set_push_args = {'push', "--set-upstream", upstream, branch, path} - local set_push = Job:new({ - command = set_push_cmd, - args = set_push_args, - cwd = worktree_path, - on_start = function() - status:next_status(set_push_cmd .. " " .. table.concat(set_push_args, " ")) - end - }) - - local rebase = Job:new({ - 'git', 'rebase', - cwd = worktree_path, - on_start = function() - status:next_status("git rebase") - end - }) - - if upstream ~= nil then - create:and_then_on_success(fetch) - fetch:and_then_on_success(set_branch) - - if M._config.autopush then - -- These are "optional" operations. - -- We have to figure out how we want to handle these... - set_branch:and_then(set_push) - set_push:and_then(rebase) - set_push:after_failure(failure("create_worktree", set_branch.args, worktree_path, true)) - else - set_branch:and_then(rebase) - end - - create:after_failure(failure("create_worktree", create.args, git_worktree_root)) - fetch:after_failure(failure("create_worktree", fetch.args, worktree_path)) - - set_branch:after_failure(failure("create_worktree", set_branch.args, worktree_path, true)) - - rebase:after(function() - - if rebase.code ~= 0 then - status:status("Rebase failed, but that's ok.") - end - - vim.schedule(function() - emit_on_change(Enum.Operations.Create, {path = path, branch = branch, upstream = upstream}) - M.switch_worktree(path) - end) - end) - else - create:after(function() - vim.schedule(function() - emit_on_change(Enum.Operations.Create, {path = path, branch = branch, upstream = upstream}) - M.switch_worktree(path) - end) - end) - end - - create:start() -end - -M.create_worktree = function(path, branch, upstream) - status:reset(8) - - if upstream == nil then - if has_origin() then - upstream = "origin" - end - end - - M.setup_git_info() - - has_worktree(path, function(found) - if found then - status:error("worktree already exists") - end - - has_branch(branch, function(found_branch) - create_worktree(path, branch, upstream, found_branch) - end) - end) - -end - -M.switch_worktree = function(path) - status:reset(2) - M.setup_git_info() - has_worktree(path, function(found) - - if not found then - status:error("worktree does not exists, please create it first " .. path) - end - - vim.schedule(function() - local prev_path = change_dirs(path) - emit_on_change(Enum.Operations.Switch, { path = path, prev_path = prev_path }) - end) - - end) -end - -M.delete_worktree = function(path, force, opts) - if not opts then - opts = {} - end - - status:reset(2) - M.setup_git_info() - has_worktree(path, function(found) - if not found then - status:error(string.format("Worktree %s does not exist", path)) - end - - local cmd = { - "git", "worktree", "remove", path - } - - if force then - table.insert(cmd, "--force") - end - - local delete = Job:new(cmd) - delete:after_success(vim.schedule_wrap(function() - emit_on_change(Enum.Operations.Delete, { path = path }) - if opts.on_success then - opts.on_success() - end - end)) - - delete:after_failure(function(e) - -- callback has to be called before failure() because failure() - -- halts code execution - if opts.on_failure then - opts.on_failure(e) - end - - failure(cmd, vim.loop.cwd())(e) - end) - delete:start() - end) -end - -M.set_worktree_root = function(wd) - git_worktree_root = wd -end - -M.set_current_worktree_path = function(wd) - current_worktree_path = wd +--Switch the current worktree +---@param path string? +function M.switch_worktree(path) + Worktree.switch(path) end -M.update_current_buffer = function(prev_path) - if prev_path == nil then - return false - end - - local cwd = vim.loop.cwd() - local current_buf_name = vim.api.nvim_buf_get_name(0) - if not current_buf_name or current_buf_name == "" then - return false - end - - local name = Path:new(current_buf_name):absolute() - local start, fin = string.find(name, cwd..Path.path.sep, 1, true) - if start ~= nil then - return true - end - - start, fin = string.find(name, prev_path, 1, true) - if start == nil then - return false - end - - local local_name = name:sub(fin + 2) - - local final_path = Path:new({cwd, local_name}):absolute() - - if not Path:new(final_path):exists() then - return false - end - - local bufnr = vim.fn.bufnr(final_path, true) - vim.api.nvim_set_current_buf(bufnr) - return true +--Create a worktree +---@param path string +---@param branch string +---@param upstream? string +function M.create_worktree(path, branch, upstream) + Worktree.create(path, branch, upstream) end -M.on_tree_change = function(cb) - table.insert(on_change_callbacks, cb) +--Delete a worktree +---@param path string +---@param force boolean +---@param opts any +function M.delete_worktree(path, force, opts) + Worktree.delete(path, force, opts) end -M.reset = function() - on_change_callbacks = {} -end - -M.get_root = function() - return git_worktree_root -end - -M.get_current_worktree_path = function() - return current_worktree_path -end - -M.get_worktree_path = function(path) - if Path:new(path):is_absolute() then - return path - else - return Path:new(git_worktree_root, path):absolute() - end -end - -M.setup = function(config) - config = config or {} - M._config = vim.tbl_deep_extend("force", { - change_directory_command = "cd", - update_on_change = true, - update_on_change_command = "e .", - clearjumps_on_change = true, - -- default to false to avoid breaking the previous default behavior - confirm_telescope_deletions = false, - -- should this default to true or false? - autopush = false, - }, config) -end - -M.set_status = function(msg) - -- TODO: make this so #1 -end - -M.setup() -M.Operations = Enum.Operations - return M diff --git a/lua/git-worktree/logger.lua b/lua/git-worktree/logger.lua new file mode 100644 index 0000000..69d4c26 --- /dev/null +++ b/lua/git-worktree/logger.lua @@ -0,0 +1,59 @@ +local LogLevels = { + TRACE = 0, + DEBUG = 1, + INFO = 2, + WARN = 3, + ERROR = 4, + OFF = 5, +} + +local LogHighlights = { + [1] = 'Comment', + [2] = 'None', + [3] = 'WarningMsg', + [4] = 'ErrorMsg', +} + +local M = {} + +--- @param level integer +--- @param msg string +local function log(level, msg) + local msg_lines = vim.split(msg, '\n', { plain = true }) + local msg_chunks = {} + for _, line in ipairs(msg_lines) do + table.insert(msg_chunks, { + string.format('[lsp-progress] %s\n', line), + LogHighlights[level], + }) + end + -- vim.api.nvim_echo(msg_chunks, false, {}) + -- vim.notify(msg, level) + -- print(msg) +end + +--- @param fmt string +--- @param ... any +M.debug = function(fmt, ...) + log(LogLevels.DEBUG, string.format(fmt, ...)) +end + +--- @param fmt string +--- @param ... any +M.info = function(fmt, ...) + log(LogLevels.INFO, string.format(fmt, ...)) +end + +--- @param fmt string +--- @param ... any +M.warn = function(fmt, ...) + log(LogLevels.WARN, string.format(fmt, ...)) +end + +--- @param fmt string +--- @param ... any +M.error = function(fmt, ...) + log(LogLevels.ERROR, string.format(fmt, ...)) +end + +return M diff --git a/lua/git-worktree/status.lua b/lua/git-worktree/status.lua deleted file mode 100644 index e1e5825..0000000 --- a/lua/git-worktree/status.lua +++ /dev/null @@ -1,71 +0,0 @@ -local Status = {} - -local function set_log_level() - local log_levels = { "trace", "debug", "info", "warn", "error", "fatal" } - local log_level = vim.env.GIT_WORKTREE_NVIM_LOG or vim.g.git_worktree_log_level - - for _, level in pairs(log_levels) do - if level == log_level then - return log_level - end - end - - return "warn" -- default, if user hasn't set to one from log_levels -end - - -function Status:new(options) - local obj = vim.tbl_extend('force', { - -- What to do here? - logger = require("plenary.log").new({ - plugin = "git-worktree-nvim", - level = set_log_level(), - }) - }, options or {}) - - setmetatable(obj, self) - self.__index = self - - return obj -end - -function Status:reset(count) - self.count = count - self.idx = 0 -end - -function Status:_get_string(msg) - return string.format("%d / %d: %s", self.idx, self.count, msg) -end - -function Status:next_status(msg) - self.idx = self.idx + 1 - local fmt_msg = self:_get_string(msg) - print(fmt_msg) - self.logger.info(fmt_msg) -end - -function Status:next_error(msg) - self.idx = self.idx + 1 - local fmt_msg = self:_get_string(msg) - error(fmt_msg) - self.logger.error(fmt_msg) -end - -function Status:status(msg) - local fmt_msg = self:_get_string(msg) - print(fmt_msg) - self.logger.info(fmt_msg) -end - -function Status:error(msg) - local fmt_msg = self:_get_string(msg) - error(fmt_msg) - self.logger.error(fmt_msg) -end - -function Status:log() - return self.logger -end - -return Status diff --git a/lua/git-worktree/test.lua b/lua/git-worktree/test.lua deleted file mode 100644 index 3906c9a..0000000 --- a/lua/git-worktree/test.lua +++ /dev/null @@ -1,8 +0,0 @@ - -local Path = require("plenary.path") -local path = Path:new(vim.loop.cwd(), "foo", "..", "..") - - -print(path:absolute()) - - diff --git a/lua/git-worktree/test/git_util.lua b/lua/git-worktree/test/git_util.lua new file mode 100644 index 0000000..82bcf10 --- /dev/null +++ b/lua/git-worktree/test/git_util.lua @@ -0,0 +1,99 @@ +local system = require('git-worktree.test.system_util') + +-- local change_dir = function(dir) +-- vim.api.nvim_set_current_dir(dir) +-- end + +local create_worktree = function(folder_path, commitish) + system.run('git worktree add ' .. folder_path .. ' ' .. commitish) +end + +local M = {} + +local origin_repo_path = nil + +function M.setup_origin_repo() + if origin_repo_path ~= nil then + return origin_repo_path + end + + local workspace_dir = system.create_temp_dir('workspace-dir') + vim.api.nvim_set_current_dir(vim.fn.getcwd()) + system.run('cp -r test/fixtures/.repo ' .. workspace_dir) + vim.api.nvim_set_current_dir(workspace_dir) + system.run([[ + mv .repo/.git-orig ./.git + mv .repo/* . + git config user.email "test@test.test" + git config user.name "Test User" + ]]) + + origin_repo_path = system.create_temp_dir('origin-repo') + system.run(string.format('git clone --bare %s %s', workspace_dir, origin_repo_path)) + + return origin_repo_path +end + +function M.prepare_repo() + M.setup_origin_repo() + + local working_dir = system.create_temp_dir('working-dir') + local master_dir = working_dir .. '/master' + vim.api.nvim_set_current_dir(working_dir) + system.run(string.format('git clone %s %s', origin_repo_path, master_dir)) + vim.api.nvim_set_current_dir(master_dir) + system.run([[ + git config remote.origin.url git@github.com:test/test.git + git config user.email "test@test.test" + git config user.name "Test User" + ]]) + return working_dir, master_dir +end + +function M.prepare_repo_bare() + M.setup_origin_repo() + + local working_dir = system.create_temp_dir('working-bare-dir') + vim.api.nvim_set_current_dir(working_dir) + system.run(string.format('git clone --bare %s %s', origin_repo_path, working_dir)) + return working_dir +end + +--- @param num_worktrees integer +function M.prepare_repo_bare_worktree(num_worktrees) + local working_dir = M.prepare_repo_bare() + local master_dir = working_dir .. '/master' + + if num_worktrees > 0 then + create_worktree('master', 'master') + end + + if num_worktrees > 1 then + create_worktree('featB', 'featB') + end + + if num_worktrees > 2 then + create_worktree('featC', 'featC') + end + + vim.api.nvim_set_current_dir(master_dir) + + return working_dir, master_dir +end + +--- @param num_worktrees integer +function M.prepare_repo_normal_worktree(num_worktrees) + local working_dir, master_dir = M.prepare_repo() + + if num_worktrees > 0 then + create_worktree('../featB', 'featB') + end + + if num_worktrees > 1 then + create_worktree('../featC', 'featC') + end + + return working_dir, master_dir +end + +return M diff --git a/lua/git-worktree/test/system_util.lua b/lua/git-worktree/test/system_util.lua new file mode 100644 index 0000000..2a1e51a --- /dev/null +++ b/lua/git-worktree/test/system_util.lua @@ -0,0 +1,41 @@ +local M = {} + +---Runs a system command and errors if it fails +---@param cmd string | table Command to be ran +---@param ignore_err boolean? Whether the error should be ignored +---@param error_msg string? The error message to be emitted on command failure +---@return string The output of the system command +function M.run(cmd, ignore_err, error_msg) + if ignore_err == nil then + ignore_err = false + end + + local output = vim.fn.system(cmd) + if vim.v.shell_error ~= 0 and not ignore_err then + error(error_msg or ('Command failed: ↓\n' .. cmd .. '\nOutput from command: ↓\n' .. output)) + end + return output +end + +local function is_macos() + return vim.loop.os_uname().sysname == 'Darwin' +end + +---Create a temporary directory for use +---@param suffix string? The suffix to be appended to the temp directory, ideally avoid spaces in your suffix +---@return string The path to the temporary directory +function M.create_temp_dir(suffix) + suffix = 'git-worktree-' .. (suffix or '') + + local cmd + if is_macos() then + cmd = string.format('mktemp -d -t %s', suffix) + else + cmd = string.format('mktemp -d --suffix=%s', suffix) + end + + local prefix = is_macos() and '/private' or '' + return prefix .. vim.trim(M.run(cmd)) +end + +return M diff --git a/lua/git-worktree/worktree.lua b/lua/git-worktree/worktree.lua new file mode 100644 index 0000000..aeb93c4 --- /dev/null +++ b/lua/git-worktree/worktree.lua @@ -0,0 +1,204 @@ +local Path = require('plenary.path') + +local Git = require('git-worktree.git') +local Log = require('git-worktree.logger') +local Hooks = require('git-worktree.hooks') +local Config = require('git-worktree.config') + +local function get_absolute_path(path) + if Path:new(path):is_absolute() then + return path + else + return Path:new(vim.loop.cwd(), path):absolute() + end +end + +local function change_dirs(path) + if path == nil then + local out = vim.fn.systemlist('git rev-parse --git-common-dir') + if vim.v.shell_error ~= 0 then + Log.error('Could not parse common dir') + return + end + path = out[1] + end + + Log.info('changing dirs: %s ', path) + local worktree_path = get_absolute_path(path) + local previous_worktree = vim.loop.cwd() + Config = require('git-worktree.config') + + -- vim.loop.chdir(worktree_path) + if Path:new(worktree_path):exists() then + local cmd = string.format('%s %s', Config.change_directory_command, worktree_path) + Log.debug('Changing to directory %s', worktree_path) + vim.cmd(cmd) + else + Log.error('Could not change to directory: %s', worktree_path) + end + + if Config.clearjumps_on_change then + Log.debug('Clearing jumps') + vim.cmd('clearjumps') + end + + print(string.format('Switched to %s', path)) + return previous_worktree +end + +local function failure(from, cmd, path, soft_error) + return function(e) + local error_message = string.format( + '%s Failed: PATH %s CMD %s RES %s, ERR %s', + from, + path, + vim.inspect(cmd), + vim.inspect(e:result()), + vim.inspect(e:stderr_result()) + ) + + if soft_error then + Log.error(error_message) + else + Log.error(error_message) + end + end +end + +local M = {} + +--- SWITCH --- + +--Switch the current worktree +---@param path string? +function M.switch(path) + if path == nil then + change_dirs(path) + else + if path == vim.loop.cwd() then + return + end + Git.has_worktree(path, nil, function(found) + if not found then + Log.error('Worktree does not exists, please create it first %s ', path) + return + end + + vim.schedule(function() + local prev_path = change_dirs(path) + Hooks.emit(Hooks.type.SWITCH, path, prev_path) + end) + end) + end +end + +--- CREATE --- + +--create a worktree +---@param path string +---@param branch string +---@param upstream? string +function M.create(path, branch, upstream) + -- if upstream == nil then + -- if Git.has_origin() then + -- upstream = 'origin' + -- end + -- end + + -- M.setup_git_info() + + Git.has_worktree(path, branch, function(found) + if found then + Log.error('Path "%s" or branch "%s" already in use.', path, branch) + return + end + + if branch == '' then + -- detached head + local create_wt_job = Git.create_worktree_job(path, nil, false, nil, false) + create_wt_job:after(function() + vim.schedule(function() + Hooks.emit(Hooks.type.CREATE, path, branch, upstream) + M.switch(path) + end) + end) + create_wt_job:start() + return + end + + Git.has_branch(branch, { '--remotes' }, function(found_remote_branch) + Log.debug('Found remote branch %s? %s', branch, found_remote_branch) + if found_remote_branch then + upstream = branch + branch = 'local/' .. branch + end + Git.has_branch(branch, nil, function(found_branch) + Log.debug('Found branch %s? %s', branch, found_branch) + Git.has_branch(upstream, { '--all' }, function(found_upstream) + Log.debug('Found upstream %s? %s', upstream, found_upstream) + + local create_wt_job = Git.create_worktree_job(path, branch, found_branch, upstream, found_upstream) + + if found_branch and found_upstream and branch ~= upstream then + local set_remote = Git.setbranch_job(path, branch, upstream) + create_wt_job:and_then_on_success(set_remote) + end + + create_wt_job:after(function() + vim.schedule(function() + Hooks.emit(Hooks.type.CREATE, path, branch, upstream) + M.switch(path) + end) + end) + + create_wt_job:start() + end) + end) + end) + end) +end + +--- DELETE --- + +--Delete a worktree +---@param path string +---@param force boolean +---@param opts any +function M.delete(path, force, opts) + if not opts then + opts = {} + end + + local branch = Git.parse_head(path) + + Git.has_worktree(path, nil, function(found) + if not found then + Log.error('Worktree %s does not exist', path) + return + end + + local delete = Git.delete_worktree_job(path, force) + delete:after_success(vim.schedule_wrap(function() + Log.info('delete after success') + Hooks.emit(Hooks.type.DELETE, path) + if opts.on_success then + opts.on_success { branch = branch } + end + end)) + + delete:after_failure(function(e) + Log.info('delete after failure') + -- callback has to be called before failure() because failure() + -- halts code execution + if opts.on_failure then + opts.on_failure(e) + end + + failure(delete.cmd, vim.loop.cwd())(e) + end) + Log.info('delete start job') + delete:start() + end) +end + +return M diff --git a/lua/telescope/_extensions/git_worktree.lua b/lua/telescope/_extensions/git_worktree.lua index 706cd2b..608af50 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -1,173 +1,261 @@ -local Path = require("plenary.path") -local Window = require("plenary.window.float") -local strings = require("plenary.strings") -local pickers = require("telescope.pickers") -local finders = require("telescope.finders") -local actions = require("telescope.actions") -local utils = require("telescope.utils") -local action_set = require("telescope.actions.set") -local action_state = require("telescope.actions.state") -local conf = require("telescope.config").values -local git_worktree = require("git-worktree") +local strings = require('plenary.strings') +local pickers = require('telescope.pickers') +local finders = require('telescope.finders') +local actions = require('telescope.actions') +local utils = require('telescope.utils') +local action_set = require('telescope.actions.set') +local action_state = require('telescope.actions.state') +local conf = require('telescope.config').values +local git_worktree = require('git-worktree') +local Config = require('git-worktree.config') +local Git = require('git-worktree.git') +local Log = require('git-worktree.logger') local force_next_deletion = false +-- Get the path of the selected worktree +-- @param prompt_bufnr number: the prompt buffer number +-- @return string: the path of the selected worktree local get_worktree_path = function(prompt_bufnr) local selection = action_state.get_selected_entry(prompt_bufnr) + if selection == nil then + return + end return selection.path end +-- Switch to the selected worktree +-- @param prompt_bufnr number: the prompt buffer number +-- @return nil local switch_worktree = function(prompt_bufnr) local worktree_path = get_worktree_path(prompt_bufnr) - actions.close(prompt_bufnr) - if worktree_path ~= nil then - git_worktree.switch_worktree(worktree_path) + if worktree_path == nil then + vim.print('No worktree selected') + return end + actions.close(prompt_bufnr) + git_worktree.switch_worktree(worktree_path) end +-- Toggle the forced deletion of the next worktree +-- @return nil local toggle_forced_deletion = function() -- redraw otherwise the message is not displayed when in insert mode if force_next_deletion then - print('The next deletion will not be forced') + vim.print('The next deletion will not be forced') vim.fn.execute('redraw') else - print('The next deletion will be forced') + vim.print('The next deletion will be forced') vim.fn.execute('redraw') force_next_deletion = true end end -local delete_success_handler = function() - force_next_deletion = false -end - -local delete_failure_handler = function() - print("Deletion failed, use to force the next deletion") -end +-- Confirm the deletion of a worktree +-- @param forcing boolean: whether the deletion is forced +-- @return boolean: whether the deletion is confirmed +local confirm_worktree_deletion = function(forcing) + if not Config.confirm_telescope_deletions then + return true + end -local ask_to_confirm_deletion = function(forcing) + local confirmed if forcing then - return vim.fn.input("Force deletion of worktree? [y/n]: ") + confirmed = vim.fn.input('Force deletion of worktree? [y/n]: ') + else + confirmed = vim.fn.input('Delete worktree? [y/n]: ') end - return vim.fn.input("Delete worktree? [y/n]: ") -end - -local confirm_deletion = function(forcing) - if not git_worktree._config.confirm_telescope_deletions then + if string.sub(string.lower(confirmed), 0, 1) == 'y' then return true end - local confirmed = ask_to_confirm_deletion(forcing) + print("Didn't delete worktree") + return false +end - if string.sub(string.lower(confirmed), 0, 1) == "y" then +-- Confirm the deletion of a worktree +-- @return boolean: whether the deletion is confirmed +local confirm_branch_deletion = function() + local confirmed = vim.fn.input('Worktree deleted, now force deletion of branch? [y/n]: ') + + if string.sub(string.lower(confirmed), 0, 1) == 'y' then return true end - print("Didn't delete worktree") + print("Didn't delete branch") return false end +-- Handler for successful deletion +-- @return nil +local delete_success_handler = function(opts) + opts = opts or {} + force_next_deletion = false + if opts.branch ~= nil and opts.branch ~= 'HEAD' and confirm_branch_deletion() then + local delete_branch_job = Git.delete_branch_job(opts.branch) + if delete_branch_job ~= nil then + delete_branch_job:after_success(vim.schedule_wrap(function() + print('Branch deleted') + end)) + delete_branch_job:start() + end + end +end + +-- Handler for failed deletion +-- @return nil +local delete_failure_handler = function() + print('Deletion failed, use to force the next deletion') +end + +-- Delete the selected worktree +-- @param prompt_bufnr number: the prompt buffer number +-- @return nil local delete_worktree = function(prompt_bufnr) - if not confirm_deletion() then + -- TODO: confirm_deletion(forcing) + if not confirm_worktree_deletion() then return end + git_worktree.switch_worktree(nil) + local worktree_path = get_worktree_path(prompt_bufnr) actions.close(prompt_bufnr) if worktree_path ~= nil then - git_worktree.delete_worktree(worktree_path, force_next_deletion, { - on_failure = delete_failure_handler, - on_success = delete_success_handler - }) + git_worktree.delete_worktree(worktree_path, force_next_deletion, { + on_failure = delete_failure_handler, + on_success = delete_success_handler, + }) end end -local create_input_prompt = function(cb) - - --[[ - local window = Window.centered({ - width = 30, - height = 1 - }) - vim.api.nvim_buf_set_option(window.bufnr, "buftype", "prompt") - vim.fn.prompt_setprompt(window.bufnr, "Worktree Location: ") - vim.fn.prompt_setcallback(window.bufnr, function(text) - vim.api.nvim_win_close(window.win_id, true) - vim.api.nvim_buf_delete(window.bufnr, {force = true}) - cb(text) - end) - - vim.api.nvim_set_current_win(window.win_id) - vim.fn.schedule(function() - vim.nvim_command("startinsert") - end) - --]] - -- - - local subtree = vim.fn.input("Path to subtree > ") - cb(subtree) -end - -local create_worktree = function(opts) +-- Create a prompt to get the path of the new worktree +-- @param cb function: the callback to call with the path +-- @return nil +local create_input_prompt = function(opts, cb) opts = opts or {} - opts.attach_mappings = function() - actions.select_default:replace( - function(prompt_bufnr, _) - local selected_entry = action_state.get_selected_entry() - local current_line = action_state.get_current_line() + opts.pattern = nil -- show all branches that can be tracked - actions.close(prompt_bufnr) + local prefix = opts.prefix or '' + local path = vim.fn.input('Path to subtree > ', prefix .. opts.branch) + if path == '' then + Log.error('No worktree path provided') + return + end - local branch = selected_entry ~= nil and - selected_entry.value or current_line + if opts.branch == '' then + cb(path, nil) + return + end - if branch == nil then - return - end + local branches = vim.fn.systemlist('git branch --all') + if #branches == 0 then + cb(path, nil) + return + end - create_input_prompt(function(name) - if name == "" then - name = branch - end - git_worktree.create_worktree(name, branch) - end) + local re = string.format('git branch --remotes --list %s', opts.branch) + local remote_branch = vim.fn.systemlist(re) + if #remote_branch == 1 then + cb(path, nil) + return + end + + local confirmed = vim.fn.input('Track an upstream? [y/n]: ') + if string.sub(string.lower(confirmed), 0, 1) == 'y' then + opts.attach_mappings = function() + actions.select_default:replace(function(prompt_bufnr, _) + local selected_entry = action_state.get_selected_entry() + local current_line = action_state.get_current_line() + actions.close(prompt_bufnr) + local upstream = selected_entry ~= nil and selected_entry.value or current_line + cb(path, upstream) end) + return true + end + require('telescope.builtin').git_branches(opts) + else + cb(path, nil) + end +end - -- do we need to replace other default maps? +-- Create a worktree +-- @param opts table: the options for the telescope picker (optional) +-- @return nil +local telescope_create_worktree = function(opts) + git_worktree.switch_worktree(nil) + opts = opts or {} + + local create_branch = function(prompt_bufnr, _) + -- if current_line is still not enough to filter everything but user + -- still wants to use it as the new branch name, without selecting anything + local branch = action_state.get_current_line() + actions.close(prompt_bufnr) + opts.branch = branch + create_input_prompt(opts, function(path, upstream) + git_worktree.create_worktree(path, branch, upstream) + end) + end + local select_or_create_branch = function(prompt_bufnr, _) + local selected_entry = action_state.get_selected_entry() + local current_line = action_state.get_current_line() + actions.close(prompt_bufnr) + -- selected_entry can be null if current_line filters everything + -- and there's no branch shown + local branch = selected_entry ~= nil and selected_entry.value or current_line + if branch == nil or branch == '' then + Log.error('No branch selected') + return + end + opts.branch = branch + create_input_prompt(opts, function(path, upstream) + git_worktree.create_worktree(path, branch, upstream) + end) + end + + opts.attach_mappings = function(_, map) + map({ 'i', 'n' }, '', create_branch) + actions.select_default:replace(select_or_create_branch) return true end - require("telescope.builtin").git_branches(opts) + + -- TODO: A corner case here is that of a new bare repo which has no branch nor tree, + -- but user may want to create one using this picker when creating the first worktree. + -- Perhaps telescope git_branches should only be used for selecting the upstream to track. + require('telescope.builtin').git_branches(opts) end +-- List the git worktrees +-- @param opts table: the options for the telescope picker (optional) +-- @return nil local telescope_git_worktree = function(opts) opts = opts or {} - local output = utils.get_os_command_output({"git", "worktree", "list"}) + local output = utils.get_os_command_output { 'git', 'worktree', 'list' } local results = {} local widths = { path = 0, sha = 0, - branch = 0 + branch = 0, } local parse_line = function(line) - local fields = vim.split(string.gsub(line, "%s+", " "), " ") + local fields = vim.split(string.gsub(line, '%s+', ' '), ' ') local entry = { path = fields[1], sha = fields[2], branch = fields[3], } - if entry.sha ~= "(bare)" then + if entry.sha ~= '(bare)' then local index = #results + 1 for key, val in pairs(widths) do if key == 'path' then - local new_path = utils.transform_path(opts, entry[key]) - local path_len = strings.strdisplaywidth(new_path or "") + local path_len = strings.strdisplaywidth(entry[key] or '') widths[key] = math.max(val, path_len) else - widths[key] = math.max(val, strings.strdisplaywidth(entry[key] or "")) + widths[key] = math.max(val, strings.strdisplaywidth(entry[key] or '')) end end @@ -179,12 +267,12 @@ local telescope_git_worktree = function(opts) parse_line(line) end - if #results == 0 then - return - end + -- if #results == 0 then + -- return + -- end - local displayer = require("telescope.pickers.entry_display").create { - separator = " ", + local displayer = require('telescope.pickers.entry_display').create { + separator = ' ', items = { { width = widths.branch }, { width = widths.path }, @@ -193,43 +281,52 @@ local telescope_git_worktree = function(opts) } local make_display = function(entry) + local path, _ = utils.transform_path(opts, entry.path) return displayer { - { entry.branch, "TelescopeResultsIdentifier" }, - { utils.transform_path(opts, entry.path) }, + { entry.branch, 'TelescopeResultsIdentifier' }, + { path }, { entry.sha }, } end - pickers.new(opts or {}, { - prompt_title = "Git Worktrees", - finder = finders.new_table { - results = results, - entry_maker = function(entry) - entry.value = entry.branch - entry.ordinal = entry.branch - entry.display = make_display - return entry - end, - }, - sorter = conf.generic_sorter(opts), - attach_mappings = function(_, map) - action_set.select:replace(switch_worktree) - - map("i", "", delete_worktree) - map("n", "", delete_worktree) - map("i", "", toggle_forced_deletion) - map("n", "", toggle_forced_deletion) + pickers + .new(opts or {}, { + prompt_title = 'Git Worktrees', + finder = finders.new_table { + results = results, + entry_maker = function(entry) + entry.value = entry.branch + entry.ordinal = entry.branch + entry.display = make_display + return entry + end, + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function(_, map) + action_set.select:replace(switch_worktree) + + map('i', '', function() + telescope_create_worktree {} + end) + map('n', '', function() + telescope_create_worktree {} + end) + map('i', '', delete_worktree) + map('n', '', delete_worktree) + map('i', '', toggle_forced_deletion) + map('n', '', toggle_forced_deletion) - return true - end - }):find() + return true + end, + }) + :find() end -return require("telescope").register_extension( - { - exports = { - git_worktree = telescope_git_worktree, - git_worktrees = telescope_git_worktree, - create_git_worktree = create_worktree - } - }) +-- Register the extension +-- @return table: the extension +return require('telescope').register_extension { + exports = { + git_worktree = telescope_git_worktree, + create_git_worktree = telescope_create_worktree, + }, +} diff --git a/nix/docgen.nix b/nix/docgen.nix new file mode 100644 index 0000000..9aca0a7 --- /dev/null +++ b/nix/docgen.nix @@ -0,0 +1,11 @@ +{pkgs, ...}: +pkgs.writeShellApplication { + name = "docgen"; + runtimeInputs = with pkgs; [ + lemmy-help + ]; + text = '' + mkdir -p doc + lemmy-help lua/git-worktree/{init,config,hooks}.lua > doc/git-worktree.txt + ''; +} diff --git a/nix/neorocks-test.nix b/nix/neorocks-test.nix new file mode 100644 index 0000000..6425eaa --- /dev/null +++ b/nix/neorocks-test.nix @@ -0,0 +1,44 @@ +{ + curl, + extraPkgs ? [], + git, + name, + neorocksTest, + nvim, + plenary-plugin, + self, + wrapNeovim, +}: let + nvim-wrapped = wrapNeovim nvim { + configure = { + packages.myVimPackage = { + start = [ + plenary-plugin + ]; + }; + }; + }; +in + neorocksTest { + inherit name; + pname = "git-worktree.nvim"; + src = self; + neovim = nvim-wrapped; + + extraPackages = + [ + git + curl + ] + ++ extraPkgs; + luaPackages = ps: + with ps; [ + # LuaRocks dependencies must be added here. + plenary-nvim + ]; + + preCheck = '' + # Neovim expects to be able to create log files, etc. + export HOME=$(realpath .) + ''; + } diff --git a/spec/config_spec.lua b/spec/config_spec.lua new file mode 100644 index 0000000..b6171f8 --- /dev/null +++ b/spec/config_spec.lua @@ -0,0 +1,21 @@ +local stub = require('luassert.stub') + +describe('config', function() + local notify_once = stub(vim, 'notify_once') + local notify = stub(vim, 'notify') + local config = require('git-worktree.config') + + it('returns the default config', function() + assert.truthy(config.change_directory_command) + end) + + it('can have configuration applied', function() + config.change_directory_command = 'test' + assert.equals(config.change_directory_command, 'test') + end) + + it('No notifications at startup.', function() + assert.stub(notify_once).was_not_called() + assert.stub(notify).was_not_called() + end) +end) diff --git a/spec/git_spec.lua b/spec/git_spec.lua new file mode 100644 index 0000000..124fd05 --- /dev/null +++ b/spec/git_spec.lua @@ -0,0 +1,188 @@ +local git_harness = require('git-worktree.test.git_util') +local gwt_git = require('git-worktree.git') + +-- local wait_for_result = function(job, result) +-- if type(result) == 'string' then +-- result = { result } +-- end +-- vim.wait(1000, function() +-- return tables_equal(job:result(), result) +-- end) +-- end + +local cwd = vim.fn.getcwd() + +-- luacheck: globals working_dir master_dir +describe('git-worktree git operations', function() + describe('in normal repo', function() + before_each(function() + working_dir, master_dir = git_harness.prepare_repo() + end) + after_each(function() + vim.api.nvim_command('cd ' .. cwd) + end) + it('finds toplevel.', function() + local ret = gwt_git.toplevel_dir() + assert.are.same(ret, master_dir) + end) + it('finds root git dir.', function() + local ret = gwt_git.gitroot_dir() + local root_repo_dir = master_dir .. '/.git' + assert.are.same(ret, root_repo_dir) + end) + it('has_worktree valid absolute.', function() + local completed = false + local ret = false + + gwt_git.has_worktree(master_dir, nil, function(found) + completed = true + ret = found + end) + + vim.fn.wait(10000, function() + return completed + end, 1000) + + assert.are.same(true, ret) + end) + it('has_worktree valid relative.', function() + local completed = false + local ret = false + + gwt_git.has_worktree('.', nil, function(found) + completed = true + ret = found + end) + + vim.fn.wait(10000, function() + return completed + end, 1000) + + assert.are.same(true, ret) + end) + it('has_worktree invalid absolute.', function() + local completed = false + local ret = false + + gwt_git.has_worktree('/tmp', nil, function(found) + completed = true + ret = found + end) + + vim.fn.wait(10000, function() + return completed + end, 1000) + + assert.are.same(false, ret) + end) + it('has_worktree invalid relative.', function() + local completed = false + local ret = false + + gwt_git.has_worktree('../foo', nil, function(found) + completed = true + ret = found + end) + + vim.fn.wait(10000, function() + return completed + end, 1000) + + assert.are.same(false, ret) + end) + end) + + describe('in bare repo', function() + before_each(function() + working_dir = git_harness.prepare_repo_bare() + end) + after_each(function() + vim.api.nvim_command('cd ' .. cwd) + end) + it('finds toplevel', function() + local ret = gwt_git.toplevel_dir() + assert.are.same(ret, nil) + end) + it('finds root git dir.', function() + local ret_git_dir = gwt_git.gitroot_dir() + assert.are.same(ret_git_dir, working_dir) + end) + it('has_worktree valid absolute.', function() + local completed = false + local ret = false + + gwt_git.has_worktree(working_dir, nil, function(found) + completed = true + ret = found + end) + + vim.fn.wait(10000, function() + return completed + end, 1000) + + assert.are.same(true, ret) + end) + it('has_worktree valid relative.', function() + local completed = false + local ret = false + + gwt_git.has_worktree('.', nil, function(found) + completed = true + ret = found + end) + + vim.fn.wait(10000, function() + return completed + end, 1000) + + assert.are.same(true, ret) + end) + it('has_worktree invalid absolute.', function() + local completed = false + local ret = false + + gwt_git.has_worktree('/tmp', nil, function(found) + completed = true + ret = found + end) + + vim.fn.wait(10000, function() + return completed + end, 1000) + + assert.are.same(false, ret) + end) + it('has_worktree invalid relative.', function() + local completed = false + local ret = false + + gwt_git.has_worktree('../foo', nil, function(found) + completed = true + ret = found + end) + + vim.fn.wait(10000, function() + return completed + end, 1000) + + assert.are.same(false, ret) + end) + end) + + describe('in worktree repo', function() + before_each(function() + working_dir, master_dir = git_harness.prepare_repo_bare_worktree(1) + end) + after_each(function() + vim.api.nvim_command('cd ' .. cwd) + end) + it('finds toplevel.', function() + local ret = gwt_git.toplevel_dir() + assert.are.same(ret, master_dir) + end) + it('finds root git dir.', function() + local ret = gwt_git.gitroot_dir() + assert.are.same(ret, working_dir) + end) + end) +end) diff --git a/spec/worktree_spec.lua b/spec/worktree_spec.lua new file mode 100644 index 0000000..504ad5d --- /dev/null +++ b/spec/worktree_spec.lua @@ -0,0 +1,158 @@ +local git_harness = require('git-worktree.test.git_util') +local Hooks = require('git-worktree.hooks') +local Path = require('plenary.path') + +local cwd = vim.fn.getcwd() + +-- luacheck: globals repo_dir config git_worktree +describe('[Worktree]', function() + local completed_create = false + local completed_switch = false + local completed_delete = false + + local reset_variables = function() + completed_create = false + completed_switch = false + completed_delete = false + end + + Hooks.register(Hooks.type.CREATE, function() + completed_create = true + end) + Hooks.register(Hooks.type.DELETE, function() + completed_delete = true + end) + Hooks.register(Hooks.type.SWITCH, function() + completed_switch = true + end) + + before_each(function() + reset_variables() + git_worktree = require('git-worktree') + end) + after_each(function() + vim.api.nvim_command('cd ' .. cwd) + end) + + -- luacheck: globals working_dir master_dir + describe('[Switch]', function() + describe('[bare repo]', function() + before_each(function() + working_dir, master_dir = git_harness.prepare_repo_bare_worktree(2) + end) + it('able to switch to worktree (relative path)', function() + local wt = 'featB' + local input_path = '../' .. wt + local expected_path = working_dir .. Path.path.sep .. wt + -- local prev_path = working_dir .. Path.path.sep .. 'master' + require('git-worktree').switch_worktree(input_path) + + vim.fn.wait(10000, function() + return completed_switch + end, 1000) + + assert.are.same(expected_path, vim.loop.cwd()) + end) + end) + + describe('[normal repo]', function() + before_each(function() + working_dir, master_dir = git_harness.prepare_repo_normal_worktree(1) + end) + it('able to switch to worktree (relative path)', function() + local wt = 'featB' + local input_path = '../' .. wt + local expected_path = working_dir .. Path.path.sep .. wt + -- local prev_path = working_dir .. Path.path.sep .. 'master' + require('git-worktree').switch_worktree(input_path) + + vim.fn.wait(10000, function() + return completed_switch + end, 1000) + + -- Check to make sure directory was switched + assert.are.same(expected_path, vim.loop.cwd()) + end) + end) + end) + + -- luacheck: globals working_dir master_dir + describe('[CREATE]', function() + describe('[bare repo]', function() + before_each(function() + working_dir, master_dir = git_harness.prepare_repo_bare_worktree(1) + end) + it('able to create a worktree (relative path)', function() + local wt = 'featB' + local input_path = '../' .. wt + local expected_path = working_dir .. Path.path.sep .. wt + -- local prev_path = working_dir .. Path.path.sep .. 'master' + require('git-worktree').create_worktree(input_path, wt) + + vim.fn.wait(10000, function() + return completed_create and completed_switch + end, 1000) + + -- Check to make sure directory was switched + assert.are.same(expected_path, vim.loop.cwd()) + end) + end) + describe('[normal repo]', function() + before_each(function() + working_dir, master_dir = git_harness.prepare_repo_normal_worktree(0) + end) + it('able to create a worktree (relative path)', function() + local wt = 'featB' + local input_path = '../' .. wt + local expected_path = working_dir .. Path.path.sep .. wt + -- local prev_path = working_dir .. Path.path.sep .. 'master' + require('git-worktree').create_worktree(input_path, wt) + + vim.fn.wait(10000, function() + return completed_create and completed_switch + end, 1000) + + -- Check to make sure directory was switched + assert.are.same(expected_path, vim.loop.cwd()) + end) + end) + end) + + -- luacheck: globals working_dir master_dir + describe('[DELETE]', function() + describe('[bare repo]', function() + before_each(function() + working_dir, master_dir = git_harness.prepare_repo_bare_worktree(2) + end) + it('able to create a worktree (relative path)', function() + local wt = 'featB' + local input_path = '../' .. wt + require('git-worktree').delete_worktree(input_path, true) + + vim.fn.wait(10000, function() + return completed_delete + end, 1000) + + -- Check to make sure directory was switched + assert.are.same(master_dir, vim.loop.cwd()) + end) + end) + describe('[normal repo]', function() + before_each(function() + working_dir, master_dir = git_harness.prepare_repo_normal_worktree(1) + end) + it('able to create a worktree (relative path)', function() + local wt = 'featB' + local input_path = '../' .. wt + require('git-worktree').delete_worktree(input_path, wt) + + vim.fn.wait(10000, function() + return completed_delete + end, 1000) + + -- Check to make sure directory was switched + assert.are.same(master_dir, vim.loop.cwd()) + end) + end) + end) +end) diff --git a/tests/repo_origin/.git-orig/COMMIT_EDITMSG b/test/fixtures/.repo/.git-orig/COMMIT_EDITMSG similarity index 100% rename from tests/repo_origin/.git-orig/COMMIT_EDITMSG rename to test/fixtures/.repo/.git-orig/COMMIT_EDITMSG diff --git a/tests/repo_origin/.git-orig/FETCH_HEAD b/test/fixtures/.repo/.git-orig/FETCH_HEAD similarity index 100% rename from tests/repo_origin/.git-orig/FETCH_HEAD rename to test/fixtures/.repo/.git-orig/FETCH_HEAD diff --git a/tests/repo_origin/.git-orig/HEAD b/test/fixtures/.repo/.git-orig/HEAD similarity index 100% rename from tests/repo_origin/.git-orig/HEAD rename to test/fixtures/.repo/.git-orig/HEAD diff --git a/tests/repo_origin/.git-orig/config b/test/fixtures/.repo/.git-orig/config similarity index 100% rename from tests/repo_origin/.git-orig/config rename to test/fixtures/.repo/.git-orig/config diff --git a/tests/repo_origin/.git-orig/description b/test/fixtures/.repo/.git-orig/description similarity index 100% rename from tests/repo_origin/.git-orig/description rename to test/fixtures/.repo/.git-orig/description diff --git a/tests/repo_origin/.git-orig/hooks/applypatch-msg.sample b/test/fixtures/.repo/.git-orig/hooks/applypatch-msg.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/applypatch-msg.sample rename to test/fixtures/.repo/.git-orig/hooks/applypatch-msg.sample diff --git a/tests/repo_origin/.git-orig/hooks/commit-msg.sample b/test/fixtures/.repo/.git-orig/hooks/commit-msg.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/commit-msg.sample rename to test/fixtures/.repo/.git-orig/hooks/commit-msg.sample diff --git a/tests/repo_origin/.git-orig/hooks/fsmonitor-watchman.sample b/test/fixtures/.repo/.git-orig/hooks/fsmonitor-watchman.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/fsmonitor-watchman.sample rename to test/fixtures/.repo/.git-orig/hooks/fsmonitor-watchman.sample diff --git a/tests/repo_origin/.git-orig/hooks/post-update.sample b/test/fixtures/.repo/.git-orig/hooks/post-update.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/post-update.sample rename to test/fixtures/.repo/.git-orig/hooks/post-update.sample diff --git a/tests/repo_origin/.git-orig/hooks/pre-applypatch.sample b/test/fixtures/.repo/.git-orig/hooks/pre-applypatch.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/pre-applypatch.sample rename to test/fixtures/.repo/.git-orig/hooks/pre-applypatch.sample diff --git a/tests/repo_origin/.git-orig/hooks/pre-commit.sample b/test/fixtures/.repo/.git-orig/hooks/pre-commit.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/pre-commit.sample rename to test/fixtures/.repo/.git-orig/hooks/pre-commit.sample diff --git a/tests/repo_origin/.git-orig/hooks/pre-merge-commit.sample b/test/fixtures/.repo/.git-orig/hooks/pre-merge-commit.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/pre-merge-commit.sample rename to test/fixtures/.repo/.git-orig/hooks/pre-merge-commit.sample diff --git a/tests/repo_origin/.git-orig/hooks/pre-push.sample b/test/fixtures/.repo/.git-orig/hooks/pre-push.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/pre-push.sample rename to test/fixtures/.repo/.git-orig/hooks/pre-push.sample diff --git a/tests/repo_origin/.git-orig/hooks/pre-rebase.sample b/test/fixtures/.repo/.git-orig/hooks/pre-rebase.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/pre-rebase.sample rename to test/fixtures/.repo/.git-orig/hooks/pre-rebase.sample diff --git a/tests/repo_origin/.git-orig/hooks/pre-receive.sample b/test/fixtures/.repo/.git-orig/hooks/pre-receive.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/pre-receive.sample rename to test/fixtures/.repo/.git-orig/hooks/pre-receive.sample diff --git a/tests/repo_origin/.git-orig/hooks/prepare-commit-msg.sample b/test/fixtures/.repo/.git-orig/hooks/prepare-commit-msg.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/prepare-commit-msg.sample rename to test/fixtures/.repo/.git-orig/hooks/prepare-commit-msg.sample diff --git a/tests/repo_origin/.git-orig/hooks/update.sample b/test/fixtures/.repo/.git-orig/hooks/update.sample similarity index 100% rename from tests/repo_origin/.git-orig/hooks/update.sample rename to test/fixtures/.repo/.git-orig/hooks/update.sample diff --git a/tests/repo_origin/.git-orig/index b/test/fixtures/.repo/.git-orig/index similarity index 100% rename from tests/repo_origin/.git-orig/index rename to test/fixtures/.repo/.git-orig/index diff --git a/tests/repo_origin/.git-orig/info/exclude b/test/fixtures/.repo/.git-orig/info/exclude similarity index 100% rename from tests/repo_origin/.git-orig/info/exclude rename to test/fixtures/.repo/.git-orig/info/exclude diff --git a/tests/repo_origin/.git-orig/logs/HEAD b/test/fixtures/.repo/.git-orig/logs/HEAD similarity index 100% rename from tests/repo_origin/.git-orig/logs/HEAD rename to test/fixtures/.repo/.git-orig/logs/HEAD diff --git a/tests/repo_origin/.git-orig/logs/refs/heads/featB b/test/fixtures/.repo/.git-orig/logs/refs/heads/featB similarity index 100% rename from tests/repo_origin/.git-orig/logs/refs/heads/featB rename to test/fixtures/.repo/.git-orig/logs/refs/heads/featB diff --git a/tests/repo_origin/.git-orig/logs/refs/heads/featC b/test/fixtures/.repo/.git-orig/logs/refs/heads/featC similarity index 100% rename from tests/repo_origin/.git-orig/logs/refs/heads/featC rename to test/fixtures/.repo/.git-orig/logs/refs/heads/featC diff --git a/tests/repo_origin/.git-orig/logs/refs/heads/master b/test/fixtures/.repo/.git-orig/logs/refs/heads/master similarity index 100% rename from tests/repo_origin/.git-orig/logs/refs/heads/master rename to test/fixtures/.repo/.git-orig/logs/refs/heads/master diff --git a/tests/repo_origin/.git-orig/objects/0b/4012bdb8b7f16ec2c7490276b071a85dbf86e9 b/test/fixtures/.repo/.git-orig/objects/0b/4012bdb8b7f16ec2c7490276b071a85dbf86e9 similarity index 100% rename from tests/repo_origin/.git-orig/objects/0b/4012bdb8b7f16ec2c7490276b071a85dbf86e9 rename to test/fixtures/.repo/.git-orig/objects/0b/4012bdb8b7f16ec2c7490276b071a85dbf86e9 diff --git a/tests/repo_origin/.git-orig/objects/1a/d41d73bf6e2f4d929f0ab5e3098e5e5b31f8e2 b/test/fixtures/.repo/.git-orig/objects/1a/d41d73bf6e2f4d929f0ab5e3098e5e5b31f8e2 similarity index 100% rename from tests/repo_origin/.git-orig/objects/1a/d41d73bf6e2f4d929f0ab5e3098e5e5b31f8e2 rename to test/fixtures/.repo/.git-orig/objects/1a/d41d73bf6e2f4d929f0ab5e3098e5e5b31f8e2 diff --git a/tests/repo_origin/.git-orig/objects/2a/fa41f6d9a4e4dc25e9088e402fe7ba35b95a10 b/test/fixtures/.repo/.git-orig/objects/2a/fa41f6d9a4e4dc25e9088e402fe7ba35b95a10 similarity index 100% rename from tests/repo_origin/.git-orig/objects/2a/fa41f6d9a4e4dc25e9088e402fe7ba35b95a10 rename to test/fixtures/.repo/.git-orig/objects/2a/fa41f6d9a4e4dc25e9088e402fe7ba35b95a10 diff --git a/tests/repo_origin/.git-orig/objects/30/a0f033a755629a8ef1561e7b7ece3750ce8a13 b/test/fixtures/.repo/.git-orig/objects/30/a0f033a755629a8ef1561e7b7ece3750ce8a13 similarity index 100% rename from tests/repo_origin/.git-orig/objects/30/a0f033a755629a8ef1561e7b7ece3750ce8a13 rename to test/fixtures/.repo/.git-orig/objects/30/a0f033a755629a8ef1561e7b7ece3750ce8a13 diff --git a/tests/repo_origin/.git-orig/objects/39/ba5d897e269fb97a17058423947603f010e702 b/test/fixtures/.repo/.git-orig/objects/39/ba5d897e269fb97a17058423947603f010e702 similarity index 100% rename from tests/repo_origin/.git-orig/objects/39/ba5d897e269fb97a17058423947603f010e702 rename to test/fixtures/.repo/.git-orig/objects/39/ba5d897e269fb97a17058423947603f010e702 diff --git a/tests/repo_origin/.git-orig/objects/3b/73d17c51c9b22ff9d4552e5bb56927776173b3 b/test/fixtures/.repo/.git-orig/objects/3b/73d17c51c9b22ff9d4552e5bb56927776173b3 similarity index 100% rename from tests/repo_origin/.git-orig/objects/3b/73d17c51c9b22ff9d4552e5bb56927776173b3 rename to test/fixtures/.repo/.git-orig/objects/3b/73d17c51c9b22ff9d4552e5bb56927776173b3 diff --git a/tests/repo_origin/.git-orig/objects/40/c09bafbe2555af7017dd08f027e4ae45db2879 b/test/fixtures/.repo/.git-orig/objects/40/c09bafbe2555af7017dd08f027e4ae45db2879 similarity index 100% rename from tests/repo_origin/.git-orig/objects/40/c09bafbe2555af7017dd08f027e4ae45db2879 rename to test/fixtures/.repo/.git-orig/objects/40/c09bafbe2555af7017dd08f027e4ae45db2879 diff --git a/tests/repo_origin/.git-orig/objects/61/fcdbfa0b2af3418651b55d003c05c8e128d22e b/test/fixtures/.repo/.git-orig/objects/61/fcdbfa0b2af3418651b55d003c05c8e128d22e similarity index 100% rename from tests/repo_origin/.git-orig/objects/61/fcdbfa0b2af3418651b55d003c05c8e128d22e rename to test/fixtures/.repo/.git-orig/objects/61/fcdbfa0b2af3418651b55d003c05c8e128d22e diff --git a/tests/repo_origin/.git-orig/objects/91/b5884361b690f3bf1ba2edc9145a01d651a920 b/test/fixtures/.repo/.git-orig/objects/91/b5884361b690f3bf1ba2edc9145a01d651a920 similarity index 100% rename from tests/repo_origin/.git-orig/objects/91/b5884361b690f3bf1ba2edc9145a01d651a920 rename to test/fixtures/.repo/.git-orig/objects/91/b5884361b690f3bf1ba2edc9145a01d651a920 diff --git a/tests/repo_origin/.git-orig/refs/heads/featB b/test/fixtures/.repo/.git-orig/refs/heads/featB similarity index 100% rename from tests/repo_origin/.git-orig/refs/heads/featB rename to test/fixtures/.repo/.git-orig/refs/heads/featB diff --git a/tests/repo_origin/.git-orig/refs/heads/featC b/test/fixtures/.repo/.git-orig/refs/heads/featC similarity index 100% rename from tests/repo_origin/.git-orig/refs/heads/featC rename to test/fixtures/.repo/.git-orig/refs/heads/featC diff --git a/tests/repo_origin/.git-orig/refs/heads/master b/test/fixtures/.repo/.git-orig/refs/heads/master similarity index 100% rename from tests/repo_origin/.git-orig/refs/heads/master rename to test/fixtures/.repo/.git-orig/refs/heads/master diff --git a/tests/repo_origin/A.txt b/test/fixtures/.repo/A.txt similarity index 100% rename from tests/repo_origin/A.txt rename to test/fixtures/.repo/A.txt diff --git a/tests/git_harness.lua b/tests/git_harness.lua deleted file mode 100644 index afa216b..0000000 --- a/tests/git_harness.lua +++ /dev/null @@ -1,411 +0,0 @@ -local git_worktree = require('git-worktree') -local Job = require('plenary.job') -local Path = require("plenary.path") - -local M = {} - -local get_os_command_output = function(cmd) - local command = table.remove(cmd, 1) - local stderr = {} - local stdout, ret = Job:new({ - command = command, - args = cmd, - cwd = git_worktree.get_root(), - on_stderr = function(_, data) - table.insert(stderr, data) - end - }):sync() - return stdout, ret, stderr -end - -local prepare_origin_repo = function(dir) - vim.api.nvim_exec('!cp -r tests/repo_origin/ /tmp/' .. dir, true) - vim.api.nvim_exec('!mv /tmp/'..dir..'/.git-orig /tmp/'..dir..'/.git', true) -end - -local prepare_bare_repo = function(dir, origin_dir) - vim.api.nvim_exec('!git clone --bare /tmp/'..origin_dir..' /tmp/'..dir, true) -end - -local fix_fetch_all = function() - vim.api.nvim_exec('!git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"', true) -end - -local prepare_repo = function(dir, origin_dir) - vim.api.nvim_exec('!git clone /tmp/'..origin_dir..' /tmp/'..dir, true) -end - -local random_string = function() - math.randomseed(os.clock()^5) - local ret = "" - for _ = 1, 5 do - local random_char = math.random(97,122) - ret = ret .. string.char(random_char) - end - return ret -end - -local change_dir = function(dir) - vim.api.nvim_set_current_dir('/tmp/'..dir) - git_worktree.set_worktree_root('/tmp/'..dir) -end - -local cleanup_repos = function() - vim.api.nvim_exec('silent !rm -rf /tmp/git_worktree_test*', true) -end - -local create_worktree = function(folder_path, commitish) - vim.api.nvim_exec('!git worktree add ' .. folder_path .. ' ' .. commitish, true) -end - -local project_dir = vim.api.nvim_exec('pwd', true) - -local reset_cwd = function() - vim.cmd('cd ' .. project_dir) - vim.api.nvim_set_current_dir(project_dir) -end - -local config_git_worktree = function() - git_worktree.setup({}) -end - -M.in_non_git_repo = function(cb) - return function() - local random_id = random_string() - local dir = "git_worktree_test_repo_" .. random_id - - config_git_worktree() - cleanup_repos() - - Path:new("/tmp/" .. dir):mkdir() - change_dir(dir) - - local _, err = pcall(cb) - - reset_cwd() - - cleanup_repos() - - if err ~= nil then - error(err) - end - - end -end - -M.in_bare_repo_from_origin_no_worktrees = function(cb) - return function() - local random_id = random_string() - local origin_repo_dir = 'git_worktree_test_origin_repo_' .. random_id - local bare_repo_dir = 'git_worktree_test_repo_' .. random_id - - config_git_worktree() - cleanup_repos() - - prepare_origin_repo(origin_repo_dir) - prepare_bare_repo(bare_repo_dir, origin_repo_dir) - - change_dir(bare_repo_dir) - fix_fetch_all() - - local _, err = pcall(cb) - - reset_cwd() - - cleanup_repos() - - if err ~= nil then - error(err) - end - - end -end - -M.in_repo_from_origin_no_worktrees = function(cb) - return function() - local random_id = random_string() - local origin_repo_dir = 'git_worktree_test_origin_repo_' .. random_id - local repo_dir = 'git_worktree_test_repo_' .. random_id - - config_git_worktree() - cleanup_repos() - - prepare_origin_repo(origin_repo_dir) - prepare_repo(repo_dir, origin_repo_dir) - - change_dir(repo_dir) - - local _, err = pcall(cb) - - reset_cwd() - - cleanup_repos() - - if err ~= nil then - error(err) - end - - end -end - -M.in_repo_from_local_no_worktrees = function(cb) - return function() - local random_id = random_string() - local local_repo_dir = 'git_worktree_test_origin_repo_' .. random_id - - config_git_worktree() - cleanup_repos() - - prepare_origin_repo(local_repo_dir) - - change_dir(local_repo_dir) - - local _, err = pcall(cb) - - reset_cwd() - - cleanup_repos() - - if err ~= nil then - error(err) - end - - end -end - -M.in_bare_repo_from_origin_1_worktree = function(cb) - return function() - local random_id = random_string() - local origin_repo_dir = 'git_worktree_test_origin_repo_' .. random_id - local bare_repo_dir = 'git_worktree_test_repo_' .. random_id - - config_git_worktree() - cleanup_repos() - - prepare_origin_repo(origin_repo_dir) - prepare_bare_repo(bare_repo_dir, origin_repo_dir) - change_dir(bare_repo_dir) - create_worktree('master','master') - - local _, err = pcall(cb) - - reset_cwd() - - cleanup_repos() - - if err ~= nil then - error(err) - end - - end -end - -M.in_repo_from_origin_1_worktree = function(cb) - return function() - local random_id = random_string() - local origin_repo_dir = 'git_worktree_test_origin_repo_' .. random_id - local repo_dir = 'git_worktree_test_repo_' .. random_id - local feat_dir = 'git_worktree_test_repo_featB_' .. random_id - - config_git_worktree() - cleanup_repos() - - prepare_origin_repo(origin_repo_dir) - prepare_repo(repo_dir, origin_repo_dir) - change_dir(repo_dir) - - create_worktree('../'..feat_dir,'featB') - - local _, err = pcall(cb) - - reset_cwd() - - cleanup_repos() - - if err ~= nil then - error(err) - end - - end -end - -M.in_bare_repo_from_origin_2_worktrees = function(cb) - return function() - local random_id = random_string() - local origin_repo_dir = 'git_worktree_test_origin_repo_' .. random_id - local bare_repo_dir = 'git_worktree_test_repo_' .. random_id - - config_git_worktree() - cleanup_repos() - - prepare_origin_repo(origin_repo_dir) - prepare_bare_repo(bare_repo_dir, origin_repo_dir) - change_dir(bare_repo_dir) - create_worktree('featB','featB') - create_worktree('featC','featC') - - local _, err = pcall(cb) - - reset_cwd() - - cleanup_repos() - - if err ~= nil then - error(err) - end - - end -end - -M.in_repo_from_origin_2_worktrees = function(cb) - return function() - local random_id = random_string() - local origin_repo_dir = 'git_worktree_test_origin_repo_' .. random_id - local repo_dir = 'git_worktree_test_repo_' .. random_id - local featB_dir = 'git_worktree_test_repo_featB_' .. random_id - local featC_dir = 'git_worktree_test_repo_featC_' .. random_id - - config_git_worktree() - cleanup_repos() - - prepare_origin_repo(origin_repo_dir) - prepare_repo(repo_dir, origin_repo_dir) - change_dir(repo_dir) - - create_worktree('../'..featB_dir,'featB') - create_worktree('../'..featC_dir,'featC') - - local _, err = pcall(cb) - - reset_cwd() - - cleanup_repos() - - if err ~= nil then - error(err) - end - - end -end - -M.in_bare_repo_from_origin_2_similar_named_worktrees = function(cb) - return function() - local random_id = random_string() - local origin_repo_dir = 'git_worktree_test_origin_repo_' .. random_id - local bare_repo_dir = 'git_worktree_test_repo_' .. random_id - - config_git_worktree() - cleanup_repos() - - prepare_origin_repo(origin_repo_dir) - prepare_bare_repo(bare_repo_dir, origin_repo_dir) - change_dir(bare_repo_dir) - create_worktree('featB','featB') - create_worktree('featB-test','featC') - - local _, err = pcall(cb) - - reset_cwd() - - cleanup_repos() - - if err ~= nil then - error(err) - end - - end -end - -M.in_repo_from_origin_2_similar_named_worktrees = function(cb) - return function() - local random_id = random_string() - local origin_repo_dir = 'git_worktree_test_origin_repo_' .. random_id - local repo_dir = 'git_worktree_test_repo_' .. random_id - local featB_dir = 'git_worktree_test_repo_featB_' .. random_id - local featC_dir = 'git_worktree_test_repo_featB-test_' .. random_id - - config_git_worktree() - cleanup_repos() - - prepare_origin_repo(origin_repo_dir) - prepare_repo(repo_dir, origin_repo_dir) - change_dir(repo_dir) - - create_worktree('../'..featB_dir,'featB') - create_worktree('../'..featC_dir,'featC') - - local _, err = pcall(cb) - - reset_cwd() - - cleanup_repos() - - if err ~= nil then - error(err) - end - - end -end - -local get_git_branches_upstreams = function() - local output = get_os_command_output({ - "git", "for-each-ref", "--format", "'%(refname:short),%(upstream:short)'", "refs/heads" - }) - return output -end - -M.check_branch_upstream = function(branch, upstream) - local correct_branch = false - local correct_upstream = false - local upstream_to_check - - if upstream == nil then - upstream_to_check = "" - else - upstream_to_check = upstream .. '/' .. branch - end - - local refs = get_git_branches_upstreams() - for _, ref in ipairs(refs) do - ref = ref:gsub("'","") - local line = vim.split(ref, ",",true) - local b = line[1] - local u = line[2] - - if b == branch then - correct_branch = true - correct_upstream = ( u == upstream_to_check ) - end - - end - - return correct_branch, correct_upstream -end - -local get_git_worktrees = function() - local output = get_os_command_output({ - "git", "worktree", "list" - }) - return output -end - -M.check_git_worktree_exists = function(worktree_path) - local worktree_exists = false - - local refs = get_git_worktrees() - for _, line in ipairs(refs) do - local worktree_line = {} - for section in line:gmatch("%S+") do - table.insert(worktree_line, section) - end - - if worktree_path == worktree_line[1] then - worktree_exists = true - end - - end - - return worktree_exists -end - -return M diff --git a/tests/minimal_init.vim b/tests/minimal_init.vim deleted file mode 100644 index da29e2f..0000000 --- a/tests/minimal_init.vim +++ /dev/null @@ -1,4 +0,0 @@ -set rtp+=. -set rtp+=../plenary.nvim/ - -runtime! plugin/plenary.vim diff --git a/tests/worktree_spec.lua b/tests/worktree_spec.lua deleted file mode 100644 index 4f5e523..0000000 --- a/tests/worktree_spec.lua +++ /dev/null @@ -1,913 +0,0 @@ -local git_worktree = require('git-worktree') -local Path = require('plenary.path') - -local harness = require('tests.git_harness') -local in_non_git_repo = harness.in_non_git_repo -local in_bare_repo_from_origin_no_worktrees = harness.in_bare_repo_from_origin_no_worktrees -local in_repo_from_origin_no_worktrees = harness.in_repo_from_origin_no_worktrees -local in_bare_repo_from_origin_1_worktree = harness.in_bare_repo_from_origin_1_worktree -local in_repo_from_origin_1_worktree = harness.in_repo_from_origin_1_worktree -local in_repo_from_local_no_worktrees = harness.in_repo_from_local_no_worktrees -local in_bare_repo_from_origin_2_worktrees = harness.in_bare_repo_from_origin_2_worktrees -local in_repo_from_origin_2_worktrees = harness.in_repo_from_origin_2_worktrees -local in_bare_repo_from_origin_2_similar_named_worktrees = harness.in_bare_repo_from_origin_2_similar_named_worktrees -local in_repo_from_origin_2_similar_named_worktrees = harness.in_repo_from_origin_2_similar_named_worktrees -local check_git_worktree_exists = harness.check_git_worktree_exists -local check_branch_upstream = harness.check_branch_upstream - -describe('git-worktree', function() - - local completed_create = false - local completed_switch = false - local completed_delete = false - - local reset_variables = function() - completed_create = false - completed_switch = false - completed_delete = false - end - - before_each(function() - reset_variables() - git_worktree.on_tree_change(function(op, _, _) - if op == git_worktree.Operations.Create then - completed_create = true - end - if op == git_worktree.Operations.Switch then - completed_switch = true - end - if op == git_worktree.Operations.Delete then - completed_delete = true - end - end) - end) - - after_each(function() - git_worktree.reset() - end) - - describe('Create', function() - - it('can create a worktree(from origin)(relative path) from a bare repo and switch to it', - in_bare_repo_from_origin_no_worktrees(function() - - local branch = "master" - local upstream = "origin" - local path = "master" - git_worktree.create_worktree(path, branch, upstream) - - vim.fn.wait( - 10000, - function() - return completed_create and completed_switch - end, - 1000 - ) - - local expected_path = git_worktree:get_root() .. Path.path.sep .. path - -- Check to make sure directory was switched - assert.are.same(expected_path, vim.loop.cwd()) - - -- Check to make sure it is added to git worktree list - assert.True(check_git_worktree_exists(expected_path)) - - -- check to make sure branch/upstream is correct - local correct_branch, correct_upstream = check_branch_upstream(branch, upstream) - assert.True(correct_branch) - assert.True(correct_upstream) - - end)) - - it('can create a worktree(from origin)(absolute path) from a bare repo and switch to it', - in_bare_repo_from_origin_no_worktrees(function() - - local branch = "master" - local upstream = "origin" - local path = git_worktree.get_root() .. Path.path.sep .. "master" - git_worktree.create_worktree(path, branch, upstream) - - vim.fn.wait( - 10000, - function() - return completed_create and completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), path) - - -- Check to make sure it is added to git worktree list - assert.True(check_git_worktree_exists(path)) - - -- check to make sure branch/upstream is correct - local correct_branch, correct_upstream = check_branch_upstream(branch, upstream) - assert.True(correct_branch) - assert.True(correct_upstream) - - end)) - - it('can create a worktree(from origin)(relative path) from a repo and switch to it', - in_repo_from_origin_no_worktrees(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - local branch = "featB" - local upstream = "origin" - local path = "../git_worktree_test_repo_" .. branch .. "_" .. random_str - git_worktree.create_worktree(path, branch, upstream) - - vim.fn.wait( - 10000, - function() - return completed_create and completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - local expected_path = Path:new(git_worktree:get_root() .. '/' .. path):normalize() - assert.are.same(expected_path, vim.loop.cwd()) - - -- Check to make sure it is added to git worktree list - assert.True(check_git_worktree_exists(expected_path)) - - -- check to make sure branch/upstream is correct - local correct_branch, correct_upstream = check_branch_upstream(branch, upstream) - assert.True(correct_branch) - assert.True(correct_upstream) - - end)) - - it('can create a worktree(from origin)(absolute path) from a repo and switch to it', - in_repo_from_origin_no_worktrees(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - local branch = "featB" - local upstream = "origin" - local path = "/tmp/git_worktree_test_repo_" .. branch .. "_" .. random_str - - git_worktree.create_worktree(path, branch, upstream) - - vim.fn.wait( - 10000, - function() - return completed_create and completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(path, vim.loop.cwd()) - - -- Check to make sure it is added to git worktree list - assert.True(check_git_worktree_exists(path)) - - -- check to make sure branch/upstream is correct - local correct_branch, correct_upstream = check_branch_upstream(branch, upstream) - assert.True(correct_branch) - assert.True(correct_upstream) - - end)) - - it('can create a worktree(no upstream but detect origin)(relative path) from a bare repo and switch to it', - in_bare_repo_from_origin_no_worktrees(function() - - local branch = "master" - local upstream = "origin" - local path = "master" - git_worktree.create_worktree(path, branch) - - vim.fn.wait( - 10000, - function() - return completed_create and completed_switch - end, - 1000 - ) - - local expected_path = git_worktree:get_root() .. '/' .. path - - -- Check to make sure directory was switched - assert.are.same(expected_path, vim.loop.cwd()) - - -- Check to make sure it is added to git worktree list - assert.True(check_git_worktree_exists(expected_path)) - - -- check to make sure branch/upstream is correct - local correct_branch, correct_upstream = check_branch_upstream(branch, upstream) - assert.True(correct_branch) - assert.True(correct_upstream) - - end)) - - it('can create a worktree(no upstream but detect origin)(absolute path) from a bare repo and switch to it', - in_bare_repo_from_origin_no_worktrees(function() - - local branch = "master" - local upstream = "origin" - local path = git_worktree:get_root() .. Path.path.sep .. "master" - - git_worktree.create_worktree(path, branch) - - vim.fn.wait( - 10000, - function() - return completed_create and completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(path, vim.loop.cwd()) - - -- Check to make sure it is added to git worktree list - assert.True(check_git_worktree_exists(path)) - - -- check to make sure branch/upstream is correct - local correct_branch, correct_upstream = check_branch_upstream(branch, upstream) - assert.True(correct_branch) - assert.True(correct_upstream) - - end)) - - it('can create a worktree(no upstream but detect origin)(relative path) from a repo and switch to it', - in_repo_from_origin_no_worktrees(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - local branch = "featB" - local upstream = "origin" - local path = "../git_worktree_test_repo_" .. branch .. "_" .. random_str - git_worktree.create_worktree(path, branch) - - vim.fn.wait( - 10000, - function() - return completed_create and completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - local expected_path = Path:new(git_worktree:get_root() .. '/' .. path):normalize() - assert.are.same(expected_path, vim.loop.cwd()) - - -- Check to make sure it is added to git worktree list - assert.True(check_git_worktree_exists(expected_path)) - - -- check to make sure branch/upstream is correct - local correct_branch, correct_upstream = check_branch_upstream(branch, upstream) - assert.True(correct_branch) - assert.True(correct_upstream) - - end)) - - it('can create a worktree(no upstream but detect origin)(absolute path) from a repo and switch to it', - in_repo_from_origin_no_worktrees(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - local branch = "featB" - local upstream = "origin" - local path = "/tmp/git_worktree_test_repo_" .. branch .. "_" .. random_str - - git_worktree.create_worktree(path, branch) - - vim.fn.wait( - 10000, - function() - return completed_create and completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(path, vim.loop.cwd()) - - -- Check to make sure it is added to git worktree list - assert.True(check_git_worktree_exists(path)) - - -- check to make sure branch/upstream is correct - local correct_branch, correct_upstream = check_branch_upstream(branch, upstream) - assert.True(correct_branch) - assert.True(correct_upstream) - - end)) - - it('can create a worktree(no upstream no origin)(relative path) from a repo and switch to it', - in_repo_from_local_no_worktrees(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - local branch = "featB" - local upstream = nil - local path = "../git_worktree_test_repo_" .. branch .. "_" .. random_str - git_worktree.create_worktree(path, branch) - - vim.fn.wait( - 10000, - function() - return completed_create and completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - local expected_path = Path:new(git_worktree:get_root() .. '/' .. path):normalize() - assert.are.same(expected_path, vim.loop.cwd()) - - -- Check to make sure it is added to git worktree list - assert.True(check_git_worktree_exists(expected_path)) - - -- check to make sure branch/upstream is correct - local correct_branch, correct_upstream = check_branch_upstream(branch, upstream) - assert.True(correct_branch) - assert.True(correct_upstream) - - end)) - - it('can create a worktree(no upstream no origin)(absolute path) from a repo and switch to it', - in_repo_from_local_no_worktrees(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - local branch = "featB" - local upstream = nil - local path = "/tmp/git_worktree_test_repo_" .. branch .. "_" .. random_str - - git_worktree.create_worktree(path, branch) - - vim.fn.wait( - 10000, - function() - return completed_create and completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(path, vim.loop.cwd()) - - -- Check to make sure it is added to git worktree list - assert.True(check_git_worktree_exists(path)) - - -- check to make sure branch/upstream is correct - local correct_branch, correct_upstream = check_branch_upstream(branch, upstream) - assert.True(correct_branch) - assert.True(correct_upstream) - - end)) - - - end) - - describe('Switch', function() - - it('from a bare repo with one worktree, able to switch to worktree (relative path)', - in_bare_repo_from_origin_1_worktree(function() - - local path = "master" - git_worktree.switch_worktree(path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), git_worktree:get_root() .. Path.path.sep .. path) - - end)) - - it('from a bare repo with one worktree, able to switch to worktree (absolute path)', - in_bare_repo_from_origin_1_worktree(function() - - local path = git_worktree:get_root() .. Path.path.sep .. "master" - git_worktree.switch_worktree(path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), path) - - end)) - - it('from a repo with one worktree, able to switch to worktree (relative path)', - in_repo_from_origin_1_worktree(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - local path = "../git_worktree_test_repo_featB_"..random_str - git_worktree.switch_worktree(path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - local expected_path = Path:new(git_worktree:get_root() .. '/'..path):normalize() - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), expected_path) - - end)) - - it('from a repo with one worktree, able to switch to worktree (absolute path)', - in_repo_from_origin_1_worktree(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - local path = "/tmp/git_worktree_test_repo_featB_"..random_str - git_worktree.switch_worktree(path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), path) - - end)) - - local get_current_file = function() - return vim.api.nvim_buf_get_name(0) - end - - it('in a featB worktree(bare) with file A open, switch to featC and switch to file A in other worktree', - in_bare_repo_from_origin_2_worktrees(function() - - local featB_path = "featB" - local featB_abs_path = git_worktree:get_root() .. Path.path.sep .. featB_path - local featB_abs_A_path = featB_abs_path .. Path.path.sep .. "A.txt" - - local featC_path = "featC" - local featC_abs_path = git_worktree:get_root() .. Path.path.sep .. featC_path - local featC_abs_A_path = featC_abs_path .. Path.path.sep .. "A.txt" - - -- switch to featB worktree - git_worktree.switch_worktree(featB_path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- open A file - vim.cmd("e A.txt") - -- make sure it is opensd - assert.True(featB_abs_A_path == get_current_file()) - - -- switch to featB worktree - reset_variables() - git_worktree.switch_worktree(featC_path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- make sure it switch to file in other tree - assert.True(featC_abs_A_path == get_current_file()) - end)) - - it('in a featB worktree(non bare) with file A open, switch to featC and switch to file A in other worktree', - in_repo_from_origin_2_worktrees(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - - local featB_path = "../git_worktree_test_repo_featB_"..random_str - local featB_abs_path = "/tmp/git_worktree_test_repo_featB_"..random_str - local featB_abs_A_path = featB_abs_path.."/A.txt" - - local featC_path = "../git_worktree_test_repo_featC_"..random_str - local featC_abs_path = "/tmp/git_worktree_test_repo_featC_"..random_str - local featC_abs_A_path = featC_abs_path.."/A.txt" - - -- switch to featB worktree - git_worktree.switch_worktree(featB_path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- open A file - vim.cmd("e A.txt") - -- make sure it is opensd - assert.True(featB_abs_A_path == get_current_file()) - - -- switch to featB worktree - reset_variables() - git_worktree.switch_worktree(featC_path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- make sure it switch to file in other tree - assert.True(featC_abs_A_path == get_current_file()) - end)) - - it("in a featB worktree(bare) with file B open, switch to featC and switch to worktree root in other worktree", - in_bare_repo_from_origin_2_worktrees(function() - - local featB_path = "featB" - local featB_abs_path = git_worktree:get_root() .. Path.path.sep .. featB_path - local featB_abs_B_path = featB_abs_path .. Path.path.sep .. "B.txt" - - local featC_path = "featC" - local featC_abs_path = git_worktree:get_root() .. Path.path.sep .. featC_path - - -- switch to featB worktree - git_worktree.switch_worktree(featB_path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- open B file - vim.cmd("e B.txt") - -- make sure it is opensd - assert.True(featB_abs_B_path == get_current_file()) - - -- switch to featB worktree - reset_variables() - git_worktree.switch_worktree(featC_path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- make sure it switch to file in other tree - assert.True(featC_abs_path == get_current_file()) - end)) - - it("in a featB worktree(non bare) with file B open, switch to featC and switch to worktree root in other worktree", - in_repo_from_origin_2_worktrees(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - - local featB_path = "../git_worktree_test_repo_featB_"..random_str - local featB_abs_path = "/tmp/git_worktree_test_repo_featB_"..random_str - local featB_abs_B_path = featB_abs_path.."/B.txt" - - local featC_path = "../git_worktree_test_repo_featC_"..random_str - local featC_abs_path = "/tmp/git_worktree_test_repo_featC_"..random_str - - -- switch to featB worktree - git_worktree.switch_worktree(featB_path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- open A file - vim.cmd("e B.txt") - -- make sure it is opensd - assert.True(featB_abs_B_path == get_current_file()) - - -- switch to featB worktree - reset_variables() - git_worktree.switch_worktree(featC_path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- make sure it switch to file in other tree - assert.True(featC_abs_path == get_current_file()) - end)) - - it('from a bare repo with two worktrees, able to switch to worktree with similar names (relative path)', - in_bare_repo_from_origin_2_similar_named_worktrees(function() - - local path1 = "featB" - local path2 = "featB-test" - git_worktree.switch_worktree(path1) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - reset_variables() - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), git_worktree:get_root() .. Path.path.sep .. path1) - - -- open A file - vim.cmd("e A.txt") - -- make sure it is opensd - assert.True(vim.loop.cwd().."/A.txt" == get_current_file()) - - git_worktree.switch_worktree(path2) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - reset_variables() - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), git_worktree:get_root() .. Path.path.sep .. path2) - -- Make sure file is switched - assert.True(vim.loop.cwd().."/A.txt" == get_current_file()) - - git_worktree.switch_worktree(path1) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), git_worktree:get_root() .. Path.path.sep .. path1) - -- Make sure file is switched - assert.True(vim.loop.cwd().."/A.txt" == get_current_file()) - - end)) - - it('from a bare repo with two worktrees, able to switch to worktree with similar names (absolute path)', - in_bare_repo_from_origin_2_similar_named_worktrees(function() - - local path1 = git_worktree:get_root() .. Path.path.sep .. "featB" - local path2 = git_worktree:get_root() .. Path.path.sep .. "featB-test" - - git_worktree.switch_worktree(path1) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - reset_variables() - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), path1) - - -- open B file - vim.cmd("e A.txt") - -- make sure it is opensd - assert.True(path1.."/A.txt" == get_current_file()) - - git_worktree.switch_worktree(path2) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - reset_variables() - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), path2) - -- Make sure file is switched - assert.True(path2.."/A.txt" == get_current_file()) - - git_worktree.switch_worktree(path1) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), path1) - -- Make sure file is switched - assert.True(path1.."/A.txt" == get_current_file()) - - end)) - - end) - - describe('Delete', function() - - it('from a bare repo with one worktree, able to delete the worktree (relative path)', - in_bare_repo_from_origin_1_worktree(function() - - local path = "master" - git_worktree.delete_worktree(path) - - vim.fn.wait( - 10000, - function() - return completed_delete - end, - 1000 - ) - - -- Check to make sure it is added to git worktree list - assert.False(check_git_worktree_exists(git_worktree:get_root() .. Path.path.sep .. path)) - - -- Check to make sure directory was not switched - assert.are.same(vim.loop.cwd(), git_worktree:get_root()) - - end)) - - it('from a bare repo with one worktree, able to delete the worktree (absolute path)', - in_bare_repo_from_origin_1_worktree(function() - - local path = git_worktree:get_root() .. Path.path.sep .. "master" - git_worktree.delete_worktree(path) - - vim.fn.wait( - 10000, - function() - return completed_delete - end, - 1000 - ) - - -- Check to make sure it is added to git worktree list - assert.False(check_git_worktree_exists(path)) - - -- Check to make sure directory was not switched - assert.are.same(vim.loop.cwd(), git_worktree:get_root()) - - end)) - - it('from a repo with one worktree, able to delete the worktree (relative path)', - in_repo_from_origin_1_worktree(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - local path = "../git_worktree_test_repo_featB_"..random_str - local absolute_path = "/tmp/git_worktree_test_repo_featB_"..random_str - git_worktree.delete_worktree(path, true) - - vim.fn.wait( - 10000, - function() - return completed_delete - end, - 1000 - ) - - -- Check to make sure it is added to git worktree list - assert.False(check_git_worktree_exists(absolute_path)) - - -- Check to make sure directory was not switched - assert.are.same(vim.loop.cwd(), git_worktree:get_root()) - - end)) - - it('from a repo with one worktree, able to delete the worktree (absolute path)', - in_repo_from_origin_1_worktree(function() - - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - local path = "/tmp/git_worktree_test_repo_featB_"..random_str - git_worktree.delete_worktree(path, true) - - vim.fn.wait( - 10000, - function() - return completed_delete - end, - 1000 - ) - - -- Check to make sure it is added to git worktree list - assert.False(check_git_worktree_exists(path)) - - -- Check to make sure directory was not switched - assert.are.same(vim.loop.cwd(), git_worktree:get_root()) - - end)) - - end) - - describe('Find Git Root Dir / Current Worktree on load', function() - - it('does not find the paths in a non git repo', - in_non_git_repo(function() - - git_worktree:setup_git_info() - assert.are.same(nil, git_worktree:get_root()) - assert.are.same(nil, git_worktree:get_current_worktree_path()) - - end)) - - it('finds the paths in a git repo', - in_repo_from_origin_1_worktree(function() - - git_worktree:setup_git_info() - assert.are.same(vim.loop.cwd(), git_worktree:get_root()) - assert.are.same(vim.loop.cwd(), git_worktree:get_current_worktree_path()) - - end)) - - it('finds the paths in a bare git repo', - in_bare_repo_from_origin_1_worktree(function() - - git_worktree:setup_git_info() - assert.are.same(vim.loop.cwd(), git_worktree:get_root()) - assert.are.same(vim.loop.cwd(), git_worktree:get_current_worktree_path()) - - end)) - - it('finds the paths from a git repo in a worktree', - in_repo_from_origin_1_worktree(function() - - local expected_git_repo = git_worktree:get_root() - -- switch to a worktree - local random_str = git_worktree.get_root():sub(git_worktree.get_root():len()-4) - local path = "/tmp/git_worktree_test_repo_featB_"..random_str - git_worktree.switch_worktree(path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), path) - - git_worktree:setup_git_info() - assert.are.same(expected_git_repo, git_worktree:get_root()) - assert.are.same(vim.loop.cwd(), git_worktree:get_current_worktree_path()) - - end)) - - it('finds the paths from a bare git repo in a worktree', - in_bare_repo_from_origin_1_worktree(function() - - local expected_git_repo = git_worktree:get_root() - -- switch to a worktree - local path = "master" - git_worktree.switch_worktree(path) - - vim.fn.wait( - 10000, - function() - return completed_switch - end, - 1000 - ) - - -- Check to make sure directory was switched - assert.are.same(vim.loop.cwd(), git_worktree:get_root() .. Path.path.sep .. path) - - git_worktree:setup_git_info() - assert.are.same(expected_git_repo, git_worktree:get_root()) - assert.are.same(vim.loop.cwd(), git_worktree:get_current_worktree_path()) - - end)) - end) - -end)