pax_global_header00006660000000000000000000000064151305314270014513gustar00rootroot0000000000000052 comment=e0d24c64fcc3ac09702c65cf5f929cc307363c3c neovim-gitsigns-2.0.0/000077500000000000000000000000001513053142700146345ustar00rootroot00000000000000neovim-gitsigns-2.0.0/.editorconfig000066400000000000000000000004261513053142700173130ustar00rootroot00000000000000root = true [*] end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 [*.lua] indent_style = space indent_size = 2 [Makefile] indent_style = tab neovim-gitsigns-2.0.0/.emmyrc.json000066400000000000000000000012401513053142700170760ustar00rootroot00000000000000{ "$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json", "runtime": { "version": "LuaJIT" }, "diagnostics" : { "disable" : [ "unnecessary-if" ], "enables": [ "iter-variable-reassign", "non-literal-expressions-in-assert", "incomplete-signature-doc", "missing-global-doc" ] }, "strict": { "typeCall": true, "arrayIndex": true }, "codeAction": { "insertSpace": true }, "workspace": { "library": [ "$VIMRUNTIME", "deps/nvim-test" ], "ignoreDir": [ ".luarocks" ] } } neovim-gitsigns-2.0.0/.gitattributes000066400000000000000000000000351513053142700175250ustar00rootroot00000000000000doc/* linguist-documentation neovim-gitsigns-2.0.0/.github/000077500000000000000000000000001513053142700161745ustar00rootroot00000000000000neovim-gitsigns-2.0.0/.github/FUNDING.yml000066400000000000000000000000241513053142700200050ustar00rootroot00000000000000github: [lewis6991] neovim-gitsigns-2.0.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001513053142700203575ustar00rootroot00000000000000neovim-gitsigns-2.0.0/.github/ISSUE_TEMPLATE/bug_report.yaml000066400000000000000000000061151513053142700234160ustar00rootroot00000000000000name: Bug report description: Report a problem with Gitsigns labels: [bug] body: - type: markdown attributes: value: > Before reporting make sure that Gitsigns is updated to the latest version. - type: textarea attributes: label: "Description" description: "A comprehensive description of the problem you are reporting." validations: required: true - type: input attributes: label: "Neovim version" description: | Output of `nvim --version` validations: required: true - type: input attributes: label: "Operating system and version" validations: required: true - type: textarea attributes: label: "Expected behavior" description: "A description of the behavior you expected:" - type: textarea attributes: label: "Actual behavior" description: "Observed behavior (may optionally include logs, images, or videos)." validations: required: true - type: textarea attributes: label: "Minimal config" description: > Minimal(!) configuration necessary to reproduce the issue. Save this as `init.lua`. Please **DO NOT** include a package manager (such as `lazy.nvim`) in this. If this is not provided, the issue may be closed without notice. render: Lua value: | for name, url in pairs{ gitsigns = 'https://github.com/lewis6991/gitsigns.nvim', -- ADD OTHER PLUGINS _NECESSARY_ TO REPRODUCE THE ISSUE } do local install_path = vim.fn.fnamemodify('gitsigns_issue/'..name, ':p') if vim.fn.isdirectory(install_path) == 0 then vim.fn.system { 'git', 'clone', '--depth=1', url, install_path } end vim.opt.runtimepath:append(install_path) end require('gitsigns').setup{ debug_mode = true, -- You must add this to enable debug messages -- ADD GITSIGNS CONFIG THAT IS _NECESSARY_ FOR REPRODUCING THE ISSUE } -- ADD INIT.LUA SETTINGS THAT IS _NECESSARY_ FOR REPRODUCING THE ISSUE validations: required: true - type: textarea attributes: label: "Steps to reproduce" description: > Steps to reproduce using the minimal config provided. If this is not provided, the issue may be closed without notice. Example: 1. `mkdir gitsigns_issue` 2. `cd gitsigns_issue` 3. `git init` 4. `touch file` 5. `git add file` 6. `git commit -m 'initial commit'` 7. `nvim --clean -u init.lua file` 8. ... validations: required: true - type: textarea attributes: label: "Gitsigns debug messages" render: text description: > Please include the output of `:Gitsigns debug_messages`. Note: You must have `debug_mode = true` in `setup()`. - type: textarea attributes: label: "Gitsigns cache" render: lua description: > If you think it's relevant maybe also provide the output of `:Gitsigns dump_cache`. neovim-gitsigns-2.0.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002441513053142700223470ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Question url: https://github.com/lewis6991/gitsigns.nvim/discussions about: Ask about Gitsigns.nvim neovim-gitsigns-2.0.0/.github/workflows/000077500000000000000000000000001513053142700202315ustar00rootroot00000000000000neovim-gitsigns-2.0.0/.github/workflows/ci.yml000066400000000000000000000044701513053142700213540ustar00rootroot00000000000000name: CI on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: commit_lint: runs-on: ubuntu-latest steps: # Check commit messages - uses: webiny/action-conventional-commits@v1.3.0 test: runs-on: ubuntu-latest timeout-minutes: 10 strategy: # Nightly can often fail as it is a moving target so disable fail-fast so # we can always see if the over versions pass or not. fail-fast: false matrix: neovim_branch: - 'v0.10.4' - 'v0.11.4' - 'nightly' env: NVIM_TEST_VERSION: ${{ matrix.neovim_branch }} steps: - name: Checkout uses: actions/checkout@v4 - uses: lewis6991/gh-actions-lua@master with: luaVersion: "5.1.5" - uses: leafo/gh-actions-luarocks@v4 - name: Download nvim-test run: make nvim-test - name: Run Test run: make test format: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Download stylua run: make stylua - name: Format run: make format-check emmylua: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - uses: lewis6991/gh-actions-lua@master with: luaVersion: "5.1.5" - uses: leafo/gh-actions-luarocks@v4 - name: Install nvim-test run: make nvim-test - name: Cache EmmyLua id: cache-emmylua uses: actions/cache@v4 with: path: deps/emmylua_analyzer-rust-* key: emmylua-${{ hashFiles('Makefile') }} - name: Install EmmyLua if: steps.cache-emmylua.outputs.cache-hit != 'true' run: make emmylua - name: Lint run: make emmylua-check NVIM_TEST_VERSION=nightly doc: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - uses: lewis6991/gh-actions-lua@master with: luaVersion: "5.1.5" - uses: leafo/gh-actions-luarocks@v4 - name: Download nvim-test run: make nvim-test - name: Doc Check run: make doc-check neovim-gitsigns-2.0.0/.github/workflows/release-please.yml000066400000000000000000000047061513053142700236520ustar00rootroot00000000000000on: push: branches: - main name: release-please permissions: contents: write pull-requests: write jobs: release-please: runs-on: ubuntu-latest outputs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} steps: - uses: googleapis/release-please-action@v4 id: release - uses: actions/checkout@v4 if: ${{ steps.release.outputs.release_created }} - name: tag stable versions if: ${{ steps.release.outputs.release_created }} run: | git config user.name github-actions[bot] git config user.email github-actions[bot]@users.noreply.github.com git remote add gh-token "https://${{ secrets.GITHUB_TOKEN }}@github.com/google-github-actions/release-please-action.git" git tag -f -a release -m "Last Stable Release" git push origin release -f luarocks-upload: needs: release-please if: ${{ needs.release-please.outputs.release_created }} runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Required to count the commits - name: Get Version run: echo "LUAROCKS_VERSION=${{ needs.release-please.outputs.tag_name }}" >> $GITHUB_ENV - name: LuaRocks Upload uses: nvim-neorocks/luarocks-tag-release@v7 env: LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} with: version: ${{ env.LUAROCKS_VERSION }} update-doc: needs: release-please if: ${{ ! needs.release-please.outputs.release_created }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: ref: release-please--branches--main # Fetch the last 2 commits instead of just 1. (Fetching just 1 commit would overwrite the whole history) fetch-depth: 2 - uses: leafo/gh-actions-lua@v10 with: luaVersion: "5.1.5" - uses: leafo/gh-actions-luarocks@v4 - name: Update doc run: make doc - name: Update PR run: | git config user.name github-actions[bot] git config user.email github-actions[bot]@users.noreply.github.com git remote add gh-token "https://${{ secrets.GITHUB_TOKEN }}@github.com/google-github-actions/release-please-action.git" git diff git add doc git commit --amend --no-edit git push --force neovim-gitsigns-2.0.0/.github/workflows/response.yml000066400000000000000000000040131513053142700226100ustar00rootroot00000000000000name: stale on: schedule: - cron: '30 1 * * *' # Run every day at 01:30 issue_comment: jobs: stale: if: github.event_name == 'schedule' runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/stale@v9 with: stale-issue-message: > This has been labeled stale since a request for information has not been answered for 30 days. close-issue-message: > This has been closed since it has been marked stale for 5 days. It can be reopened when the requested information is provided. days-before-stale: 30 days-before-close: 5 any-of-labels: needs response remove_label: if: github.event_name == 'issue_comment' runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/checkout@v4 - uses: actions/github-script@v7 with: script: | const issue = await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); const author = issue.data.user.login; const commenter = context.actor; if (author === commenter) { const labels = issue.data.labels.map((e) => e.name); if (labels.includes("needs response")) { github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: "needs response", }); } if (labels.includes("Stale")) { github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: "Stale", }); } } neovim-gitsigns-2.0.0/.gitignore000066400000000000000000000000401513053142700166160ustar00rootroot00000000000000doc/tags deps emydoc/ .nvimlog neovim-gitsigns-2.0.0/.release-please-manifest.json000066400000000000000000000000251513053142700222750ustar00rootroot00000000000000{ ".": "2.0.0" } neovim-gitsigns-2.0.0/.stylua.toml000066400000000000000000000002161513053142700171270ustar00rootroot00000000000000column_width = 100 line_endings = "Unix" indent_type = "Spaces" indent_width = 2 quote_style = "AutoPreferSingle" call_parentheses = "Always" neovim-gitsigns-2.0.0/AGENTS.md000066400000000000000000000036561513053142700161510ustar00rootroot00000000000000# Repository Guidelines ## Project Structure & Module Organization - Core Lua modules live in `lua/gitsigns/` (config, handlers, utilities). - User-facing commands are wired in `plugin/gitsigns.vim` - Help files under `doc/` get regenerated by `gen_help.lua`. - Specs plus fixtures sit in `test/`, relying on helpers in `test/gs_helpers.lua`. - Tooling binaries (Stylua, nvim-test, EmmyLua) are cached in `deps/`. ## Build, Test, and Development Commands - `make build`: run Stylua over `lua/` + `test/`, then regenerate help files before committing. - `make test [FILTER=pattern]`: execute the functional suite via nvim-test with the default Neovim runner. - `make test-010`, `make test-011`, `make test-nightly`: confirm compatibility with multiple Neovim versions. - `make doc` / `make doc-check`: regenerate help from `lua/gitsigns/config.lua` and fail if docs drift. - `make format-check` or `make format`: lint or autoformat Lua sources. - `make emmylua-check`: run optional static analysis after fetching the analyzer. ## Coding Style & Naming Conventions - Lua code must have emmylua/LuaCATS type annotations - 2-space indentation, 100-character columns, single quotes for strings. - Run `make format` and `make emmylua-check` after changing any code ## Testing Guidelines - Every bug fix must include a spec that reproduces the regression and asserts the desired buffer state, co-located with related modules (for example, `hunk_spec.lua` for hunk logic). - Keep tests deterministic by guarding optional Git features and running the version matrix (`make test-010 && make test-nightly`) when Neovim internals are touched. ## Commit & Pull Request Guidelines - History follows a Conventional Commit style: `(): ` (for instance, `fix(blame): close blame window on bufhidden`), so match that pattern and keep subjects under 72 characters. - Ensure `make build`, the relevant `make test-*`, and `make doc-check` all pass locally. neovim-gitsigns-2.0.0/CHANGELOG.md000066400000000000000000001277261513053142700164640ustar00rootroot00000000000000# Changelog ## [2.0.0](https://github.com/lewis6991/gitsigns.nvim/compare/v1.0.2...v2.0.0) (2026-01-09) ### ⚠ BREAKING CHANGES * **config:** remove support for custom highlight names * **setup:** make optional * target Nvim 0.11, drop testing for 0.9.5 ### Features * **actions.blame:** add `BlameOpts` parameter ([30ec2bb](https://github.com/lewis6991/gitsigns.nvim/commit/30ec2bbb121fb7b582deba5e5ea3e7486605c25f)) * **actions:** add show_commit ([6e3ee68](https://github.com/lewis6991/gitsigns.nvim/commit/6e3ee68bc9f65b21a21582a3d80e270c7e4f2992)) * add basic gh integration ([aa49c96](https://github.com/lewis6991/gitsigns.nvim/commit/aa49c9675433d3751b7afd198c9f5d2e03252af1)), closes [#839](https://github.com/lewis6991/gitsigns.nvim/issues/839) * add diffthis options ([93f882f](https://github.com/lewis6991/gitsigns.nvim/commit/93f882f7041a2e779addbd34943812ca66edef5a)), closes [#1314](https://github.com/lewis6991/gitsigns.nvim/issues/1314) * **blame:** do not show hunk if it was added in commit ([0ddad02](https://github.com/lewis6991/gitsigns.nvim/commit/0ddad02a2ed5249d7d21e90bd550eb8a2f6e7b8c)) * **blame:** general improvements ([7bf01f0](https://github.com/lewis6991/gitsigns.nvim/commit/7bf01f0c27040ffe4ab2d9ed52ab1f926b0670a8)) * **cache:** add support for fetching line ranges ([#1414](https://github.com/lewis6991/gitsigns.nvim/issues/1414)) ([5813e48](https://github.com/lewis6991/gitsigns.nvim/commit/5813e4878748805f1518cee7abb50fd7205a3a48)) * **config:** remove support for custom highlight names ([74fce28](https://github.com/lewis6991/gitsigns.nvim/commit/74fce28b8954c26f79b83736f34093d341bf1a0e)) * **diffthis:** add some rename detection ([8dec8da](https://github.com/lewis6991/gitsigns.nvim/commit/8dec8da8ed8a4463cc6abcd3cc3801373600767d)) * enable new sign calc ([40e235f](https://github.com/lewis6991/gitsigns.nvim/commit/40e235fa320e6f70293ed0274986dfd706bd9142)) * enhance status formatting with color codes ([e9cfaa0](https://github.com/lewis6991/gitsigns.nvim/commit/e9cfaa08c67f80fbca1e6230bf32455beb249523)) * improve context for logging ([4b2c9ab](https://github.com/lewis6991/gitsigns.nvim/commit/4b2c9ab533ea0d07f132b38d2f16e8c018aba529)) * minor improvements to logging ([d62e3ee](https://github.com/lewis6991/gitsigns.nvim/commit/d62e3ee6c35585a3e2aa74581bb2f5adc81db750)) * move watchers to repo objects ([2bf0f73](https://github.com/lewis6991/gitsigns.nvim/commit/2bf0f734f1eeae0ce0839dd93104641ea90082fd)) * overhaul repo watcher ([a772850](https://github.com/lewis6991/gitsigns.nvim/commit/a7728509c034367f99a78e72231155d0f2600ddd)) * remove border from preview_config default ([9a75d9f](https://github.com/lewis6991/gitsigns.nvim/commit/9a75d9f46cfa2128fabf64a625c7901564236f22)), closes [#1241](https://github.com/lewis6991/gitsigns.nvim/issues/1241) * set buffer name for blame window ([588264b](https://github.com/lewis6991/gitsigns.nvim/commit/588264bee92993df92535b6742576f5655c91b1c)) * **setqflist:** improve text in list ([b014331](https://github.com/lewis6991/gitsigns.nvim/commit/b01433169be710d6c69f7b4ee264d9670698b831)) * **setup:** make optional ([6933bee](https://github.com/lewis6991/gitsigns.nvim/commit/6933beee338960b980b71372e948b6af501445c0)), closes [#1222](https://github.com/lewis6991/gitsigns.nvim/issues/1222) * **show:** add navigation mappings ([1ee5c1f](https://github.com/lewis6991/gitsigns.nvim/commit/1ee5c1fd068c81f9dd06483e639c2aa4587dc197)) * **show:** adjust output to include tree and parent ([9dfa82c](https://github.com/lewis6991/gitsigns.nvim/commit/9dfa82c1c6e1fe0bc59a719d4bc7ef3a28d9cdc7)) * support for statuscolumn ([b2094c6](https://github.com/lewis6991/gitsigns.nvim/commit/b2094c6b8d72568eca08f18e7e494aa3e22d9963)) * **win_width:** accept winid param ([ace6c6c](https://github.com/lewis6991/gitsigns.nvim/commit/ace6c6c2d045fe951c397a367124638a97f2b60f)) ### Bug Fixes * [#1246](https://github.com/lewis6991/gitsigns.nvim/issues/1246) ([17ab794](https://github.com/lewis6991/gitsigns.nvim/commit/17ab794b6fce6fce768430ebc925347e349e1d60)) * [#1274](https://github.com/lewis6991/gitsigns.nvim/issues/1274) ([550757c](https://github.com/lewis6991/gitsigns.nvim/commit/550757c41a25b80447b821ca3b9ac1cfda894267)) * [#1277](https://github.com/lewis6991/gitsigns.nvim/issues/1277) ([c5a39b2](https://github.com/lewis6991/gitsigns.nvim/commit/c5a39b2cf7fa41a364fa82a6bb08f6c6091cc6b2)) * [#1280](https://github.com/lewis6991/gitsigns.nvim/issues/1280) ([4e1337a](https://github.com/lewis6991/gitsigns.nvim/commit/4e1337abe78000c14317a2707f0fd713572a967d)) * [#1300](https://github.com/lewis6991/gitsigns.nvim/issues/1300) ([7ce11ab](https://github.com/lewis6991/gitsigns.nvim/commit/7ce11abbb8b038a9de4fb6f75d8289c58d81aed7)) * [#1307](https://github.com/lewis6991/gitsigns.nvim/issues/1307) ([ab9e05d](https://github.com/lewis6991/gitsigns.nvim/commit/ab9e05d1cd5b372d4d443fa5c8e0e334232f2c77)) * [#1312](https://github.com/lewis6991/gitsigns.nvim/issues/1312) ([5624b5e](https://github.com/lewis6991/gitsigns.nvim/commit/5624b5ebe6988c75d3f4eb588b9f31f3847a721c)) * [#1372](https://github.com/lewis6991/gitsigns.nvim/issues/1372) ([83e29aa](https://github.com/lewis6991/gitsigns.nvim/commit/83e29aad7d9bc55fcc68ee6c74f8c92cae16869f)) * [#1384](https://github.com/lewis6991/gitsigns.nvim/issues/1384) ([cc2e664](https://github.com/lewis6991/gitsigns.nvim/commit/cc2e664c7e3cd8a31af34df040d16a75cfcadced)) * [#1388](https://github.com/lewis6991/gitsigns.nvim/issues/1388) ([c7d37ca](https://github.com/lewis6991/gitsigns.nvim/commit/c7d37ca22b461f64e26f8f6701b2586128ed0bef)) * [#1440](https://github.com/lewis6991/gitsigns.nvim/issues/1440) ([5cdd276](https://github.com/lewis6991/gitsigns.nvim/commit/5cdd276fbda182f7bb480a7052c8e421d3706819)) * [#1457](https://github.com/lewis6991/gitsigns.nvim/issues/1457) ([6a8dbf0](https://github.com/lewis6991/gitsigns.nvim/commit/6a8dbf0e3d179f6f876744169b5ceebd9bec91b1)), closes [#1458](https://github.com/lewis6991/gitsigns.nvim/issues/1458) * **actions:** allow bufnr to be 0 ([0d46562](https://github.com/lewis6991/gitsigns.nvim/commit/0d4656286ff348557a19fa76059947938f9abd0f)), closes [#1422](https://github.com/lewis6991/gitsigns.nvim/issues/1422) * add a 2s timeout to the git lock ([1bfeabd](https://github.com/lewis6991/gitsigns.nvim/commit/1bfeabdf1c21cb039cc2049d2519c3d1d48787c2)) * add explicit tostring() for thread in string.format() for Lua 5.1 compatibility ([#1461](https://github.com/lewis6991/gitsigns.nvim/issues/1461)) ([d6482eb](https://github.com/lewis6991/gitsigns.nvim/commit/d6482eb0b1e0ef22df660e7ecf65f38c8be352d9)) * add WinResized handler to update blame window extmarks ([ecd3717](https://github.com/lewis6991/gitsigns.nvim/commit/ecd3717d33e23f5f4dd0acaa6235533557597803)) * all toggle actions didn't refresh highlight in non-active buffers ([50a635b](https://github.com/lewis6991/gitsigns.nvim/commit/50a635b9bbd65a9b6d95e8ed7b7206348d11fde8)) * always schedule after git commands ([11b67da](https://github.com/lewis6991/gitsigns.nvim/commit/11b67da4935caafe1f75dd38a0aff2c78ea6df46)), closes [#1425](https://github.com/lewis6991/gitsigns.nvim/issues/1425) [#1460](https://github.com/lewis6991/gitsigns.nvim/issues/1460) * **async:** raise errors when they happen ([ee7e50d](https://github.com/lewis6991/gitsigns.nvim/commit/ee7e50dfbdf49e3acfa416fd3ad3abbdb658582c)) * attach through symlinks ([2ac55db](https://github.com/lewis6991/gitsigns.nvim/commit/2ac55dbde63eec1a41c65e6574a8ddef6d816262)) * **attach:** do not attach to files in resolved gitdir ([c80e0b4](https://github.com/lewis6991/gitsigns.nvim/commit/c80e0b4bfc411d5740a47adc8775fd1070f2028b)), closes [#1218](https://github.com/lewis6991/gitsigns.nvim/issues/1218) * **attach:** don't skip all `.git*` files at the root of the repo ([362fe61](https://github.com/lewis6991/gitsigns.nvim/commit/362fe61f9f19e9bceff178792780df5cce118a7d)) * blame incompatible neovim function ([#1406](https://github.com/lewis6991/gitsigns.nvim/issues/1406)) ([400cfab](https://github.com/lewis6991/gitsigns.nvim/commit/400cfabf87fb3f7b48aa4eae1c11758e39a57071)) * blame_line{full=true} stop work ([27c3f37](https://github.com/lewis6991/gitsigns.nvim/commit/27c3f37a8ea6480ba336dab74f73a8032a0de63c)) * **blame:** always update current_line_blame on WinResized ([ea7c05f](https://github.com/lewis6991/gitsigns.nvim/commit/ea7c05f70214aed320ad6afa0718a9c15cf8cb12)) * **blame:** check valid buf ([58e3e52](https://github.com/lewis6991/gitsigns.nvim/commit/58e3e52e46c6abefd3dc8b6e246716e30ce771ef)) * **blame:** close blame window on bufhidden ([91f39eb](https://github.com/lewis6991/gitsigns.nvim/commit/91f39eb148fd984449203c29e169b614eea273b4)) * **blame:** do no expand hunk text ([425cb39](https://github.com/lewis6991/gitsigns.nvim/commit/425cb3942716554035ee56b0e36528355c238e3d)) * **blame:** do not show stale blames popup ([0c68263](https://github.com/lewis6991/gitsigns.nvim/commit/0c6826374f47fcbb2b53053986ccc59c115044ff)) * **blame:** do not unpack hunk linespec ([731b581](https://github.com/lewis6991/gitsigns.nvim/commit/731b581428ec6c1ccb451b95190ebbc6d7006db7)) * **blame:** get gh blame info asynchronously ([a434c8c](https://github.com/lewis6991/gitsigns.nvim/commit/a434c8cc97d8b96cb272e7a44112891d5a05bb06)) * **blame:** handle bad git-blame output ([07d4263](https://github.com/lewis6991/gitsigns.nvim/commit/07d426364c476e8a091ff7ee40b862f97e2cfb3c)), closes [#1332](https://github.com/lewis6991/gitsigns.nvim/issues/1332) * **blame:** handle partial lines in blame output ([3d01bad](https://github.com/lewis6991/gitsigns.nvim/commit/3d01bad517a9cd8d6b1ac6871e16188375c2853b)), closes [#1236](https://github.com/lewis6991/gitsigns.nvim/issues/1236) * **blame:** not stale if enter popup before result popup.update ([bf77caa](https://github.com/lewis6991/gitsigns.nvim/commit/bf77caa5da671f5bab16e4792711d5aa288e8db0)) * **blame:** remove link highlight on whitespace ([89f7507](https://github.com/lewis6991/gitsigns.nvim/commit/89f75073da1c8fab1d8b6285da72366ee54633ba)) * **blame:** set nolist in the blame window ([6067670](https://github.com/lewis6991/gitsigns.nvim/commit/60676707b6a5fa42369e8ff40a481ca45987e0d0)) * **cache:** correct condition for range blame ([3f5ffea](https://github.com/lewis6991/gitsigns.nvim/commit/3f5ffea8abbb3d58d536abfe65cb7e48caee38f5)) * calculate staged color dynamically adjust based on background ([d16d4ed](https://github.com/lewis6991/gitsigns.nvim/commit/d16d4ed864478c13d9bdd74230af0a4cc12e644b)) * check cwd before running rev-parse ([a3f64d4](https://github.com/lewis6991/gitsigns.nvim/commit/a3f64d4289f818bc5de66295a9696e2819bfb270)), closes [#1331](https://github.com/lewis6991/gitsigns.nvim/issues/1331) * check preview popup before navigating ([e399f97](https://github.com/lewis6991/gitsigns.nvim/commit/e399f9748d7cfd8859747c8d6c4e9c8b4d50a1bd)) * Close blame buffer on closure of source buffer ([130beac](https://github.com/lewis6991/gitsigns.nvim/commit/130beacf8a51f00aede9c31064c749136679a321)) * correct hl group ([b79047e](https://github.com/lewis6991/gitsigns.nvim/commit/b79047e81f645875e500b4f433d8133bc421446c)) * cygpath output handling for MSYS2 environments ([#1463](https://github.com/lewis6991/gitsigns.nvim/issues/1463)) ([8690d7a](https://github.com/lewis6991/gitsigns.nvim/commit/8690d7a4e244a639b50de720a2d8b57ebeaff485)) * do not attach if buffer is a directory ([392b9da](https://github.com/lewis6991/gitsigns.nvim/commit/392b9da4abebe9bee11b66dfdad82e0234bac4c2)) * do not attach to fugitive tree buffers ([472f752](https://github.com/lewis6991/gitsigns.nvim/commit/472f752943d44d732cece09d442d45681ce38f48)) * do not error if no gh remotes ([736f51d](https://github.com/lewis6991/gitsigns.nvim/commit/736f51d2bb684c06f39a2032f064d7244f549981)) * emmylua fixes ([7bbc674](https://github.com/lewis6991/gitsigns.nvim/commit/7bbc674278f22376850576dfdddf43bbc17e62b5)) * emmylua fixes ([c9165bb](https://github.com/lewis6991/gitsigns.nvim/commit/c9165bbc3266d14d557397baf42f4a2389acbe3d)) * error when `Gitsigns next_hunk target=all` ([4666d04](https://github.com/lewis6991/gitsigns.nvim/commit/4666d040b60d1dc0e474ccd9a3fd3c4d67b4767c)) * **error:** [#1277](https://github.com/lewis6991/gitsigns.nvim/issues/1277) ([9cd665f](https://github.com/lewis6991/gitsigns.nvim/commit/9cd665f46ab7af2e49d140d328b8e72ea1cf511b)) * errors nil ref ([43b0c85](https://github.com/lewis6991/gitsigns.nvim/commit/43b0c856ae5f32a195d83f4a27fe21d63e6c966c)) * force release lock if we waiting for more than 4 seconds ([24d4c92](https://github.com/lewis6991/gitsigns.nvim/commit/24d4c92dc635a445f309b7a5b99499d06714e2e8)) * handle files outside of repo ([1796c7c](https://github.com/lewis6991/gitsigns.nvim/commit/1796c7cedfe7e5dd20096c5d7b8b753d8f8d22eb)), closes [#1117](https://github.com/lewis6991/gitsigns.nvim/issues/1117) [#1296](https://github.com/lewis6991/gitsigns.nvim/issues/1296) [#1297](https://github.com/lewis6991/gitsigns.nvim/issues/1297) * handle when files are removed from index ([fd50977](https://github.com/lewis6991/gitsigns.nvim/commit/fd50977fce4d5240b910d2b816e71fb726cbbaf7)) * **handle_blame_info:** do not consider `wrap/nowrap` for `right_align/eol` ([75879cd](https://github.com/lewis6991/gitsigns.nvim/commit/75879cd946b5d4aa922b9d96423bce092838be1a)) * make update lock disabled by default ([8270378](https://github.com/lewis6991/gitsigns.nvim/commit/8270378ab83540b03d09c0194ba3e208f9d0cb72)) * more robust timer closing ([72acb69](https://github.com/lewis6991/gitsigns.nvim/commit/72acb69020c92d99cf388bfeb390481ccec50c04)) * nvim<0.11 has no `&winborder` ([2f0f65e](https://github.com/lewis6991/gitsigns.nvim/commit/2f0f65ed8002f2e3123035913c27b87c2d14e9d2)) * **popup:** don't move window when resizing ([20ad441](https://github.com/lewis6991/gitsigns.nvim/commit/20ad4419564d6e22b189f6738116b38871082332)) * prevent inline hunk preview from folding ([02eafb1](https://github.com/lewis6991/gitsigns.nvim/commit/02eafb1273afec94447f66d1a43fc5e477c2ab8a)) * preview_hunk format ([8bdaccd](https://github.com/lewis6991/gitsigns.nvim/commit/8bdaccdb897945a3c99c1ad8df94db0ddf5c8790)) * **preview:** set border to none for inline preview ([7cfd88d](https://github.com/lewis6991/gitsigns.nvim/commit/7cfd88d9c017283df14125640c9ce9c07f284519)) * react to config changes more robustly ([c4dbc36](https://github.com/lewis6991/gitsigns.nvim/commit/c4dbc3624999e9ddd9d1f5a6749f0a9346bfc2ed)) * remove border from docs ([be7640c](https://github.com/lewis6991/gitsigns.nvim/commit/be7640c55bf1306769f5cf3215d8cf52e80eba2c)) * remove clear_env=true for system calls ([03fb621](https://github.com/lewis6991/gitsigns.nvim/commit/03fb6212779fa62bde4176719383bcd658fd7963)), closes [#1350](https://github.com/lewis6991/gitsigns.nvim/issues/1350) * remove duplicated phrase in comments of util.lua ([18ec9a8](https://github.com/lewis6991/gitsigns.nvim/commit/18ec9a862741453e0f47f28155728b11c992b5f4)) * repo memory leak ([1fcaddc](https://github.com/lewis6991/gitsigns.nvim/commit/1fcaddcc427ff5802b6602f46de37a5352d0f9e0)) * respect winborder when creating popups ([ce5e1b5](https://github.com/lewis6991/gitsigns.nvim/commit/ce5e1b5ae3455316364ac1c96c2787d7925a2914)) * **scm-rockspec:** add 'plugin' to copy_directories ([cdafc32](https://github.com/lewis6991/gitsigns.nvim/commit/cdafc320f03f2572c40ab93a4eecb733d4016d07)) * set more buf options in commit buffers ([c8ddbdb](https://github.com/lewis6991/gitsigns.nvim/commit/c8ddbdbce20d31561f6e19e7a3e9e8874714edfc)) * **show:** handle numeric branch name ([0b3ac7a](https://github.com/lewis6991/gitsigns.nvim/commit/0b3ac7a7cbb9999957bc5d8a1973214bfa37c3cf)) * tests for nightly ([00f1418](https://github.com/lewis6991/gitsigns.nvim/commit/00f14183abbcc38766d9d0b63f3f03174e3a3bd8)) * tracking multiple branch changes ([#1266](https://github.com/lewis6991/gitsigns.nvim/issues/1266)) ([2149fc2](https://github.com/lewis6991/gitsigns.nvim/commit/2149fc2009d1117d58e86e56836f70c969f60a82)) * type errors from emmylua ([5f1b1e2](https://github.com/lewis6991/gitsigns.nvim/commit/5f1b1e25373cd589ecf418ced8c2ece28229dd83)) * type errors from emmylua ([d1c3d5a](https://github.com/lewis6991/gitsigns.nvim/commit/d1c3d5af2cbe235def22006888df41fa22c1fd9c)) * type fixes ([24ecb13](https://github.com/lewis6991/gitsigns.nvim/commit/24ecb1375789bd3dec196f13d03163c0f0a68c47)) * **types:** add on_attach return type ([8b729e4](https://github.com/lewis6991/gitsigns.nvim/commit/8b729e489f1475615dc6c9737da917b3bc163605)) * **util.flatten:** iterator table in order ([45086b3](https://github.com/lewis6991/gitsigns.nvim/commit/45086b350f6769af7dc36770ea68dc73e81ed6f1)) * vim.ui.select with Snacks ([f780609](https://github.com/lewis6991/gitsigns.nvim/commit/f780609807eca1f783a36a8a31c30a48fbe150c5)) * **watcher:** invalidate the cache earlier ([d600d39](https://github.com/lewis6991/gitsigns.nvim/commit/d600d3922c1d001422689319a8f915136bb64e1e)) * when diff dos format with unix format ([8820595](https://github.com/lewis6991/gitsigns.nvim/commit/88205953bd748322b49b26e1dfb0389932520dc9)) * **windows:** [#1250](https://github.com/lewis6991/gitsigns.nvim/issues/1250) ([140ac64](https://github.com/lewis6991/gitsigns.nvim/commit/140ac646db125904e456e42ab8b538d28f9607d7)) * **word_diff:** align inline preview highlights ([684270f](https://github.com/lewis6991/gitsigns.nvim/commit/684270f22364bd248fcedd51598b6433266fdc47)) * **word_diff:** no "No newline at end of file" shown in popup ([6bd2949](https://github.com/lewis6991/gitsigns.nvim/commit/6bd29494e3f79ff08be1d35bc1926ed23c22ed9a)) ### Performance Improvements * defer updates to hidden buffers ([3c69cac](https://github.com/lewis6991/gitsigns.nvim/commit/3c69cac2793cffa95cb62e8a457fe98f944133dc)) * ignore gitdir changes by watchmen ([e44821b](https://github.com/lewis6991/gitsigns.nvim/commit/e44821b9b50168a847b159f66c5c413ea2804f64)) ### Continuous Integration * target Nvim 0.11, drop testing for 0.9.5 ([3c76f7f](https://github.com/lewis6991/gitsigns.nvim/commit/3c76f7fabac723aa682365ef782f88a83ccdb4ac)) ## [1.0.2](https://github.com/lewis6991/gitsigns.nvim/compare/v1.0.1...v1.0.2) (2025-03-16) ### Bug Fixes * `stage_hunk` on staged hunk should behave like `undo_stage_hunk` ([5fefc7b](https://github.com/lewis6991/gitsigns.nvim/commit/5fefc7bf6966f9a1ca961ac2fca0f9d93118df18)) * change_base with empty base ([751bfae](https://github.com/lewis6991/gitsigns.nvim/commit/751bfae26a3561394afcafdf92b0dc52988ce436)) ## [1.0.1](https://github.com/lewis6991/gitsigns.nvim/compare/v1.0.0...v1.0.1) (2025-02-15) ### Bug Fixes * **blame:** fix popup menu shortcut mappings ([420b199](https://github.com/lewis6991/gitsigns.nvim/commit/420b19971c22ba7558dfd39ec1c1c2735c7db93f)), closes [#1215](https://github.com/lewis6991/gitsigns.nvim/issues/1215) * **current_line_blame:** last line show not committed ([8b00147](https://github.com/lewis6991/gitsigns.nvim/commit/8b00147519d6f8353867d5d0b55f587306b0cfb6)), closes [#1213](https://github.com/lewis6991/gitsigns.nvim/issues/1213) * stylua ([2bc3b47](https://github.com/lewis6991/gitsigns.nvim/commit/2bc3b472bbc2484214549af4d9f38c127b886a55)) ## [1.0.0](https://github.com/lewis6991/gitsigns.nvim/compare/v0.9.0...v1.0.0) (2025-02-07) ### ⚠ BREAKING CHANGES * deprecate some functions * **blame:** replace dot with dash in blame file type name * remove current_line_blame_formatter_opts * remove support for yadm * **config:** deprecate highlight groups in config.signs ### Features * add highlights for the current line ([b29cb58](https://github.com/lewis6991/gitsigns.nvim/commit/b29cb58126663569f6f34401fab513c2375e95d3)) * add staging and update locks ([e6e3c3a](https://github.com/lewis6991/gitsigns.nvim/commit/e6e3c3a1394d9e0a1c75d8620f8631e4a6ecde0e)) * add submodule support for gitsigns urls ([f074844](https://github.com/lewis6991/gitsigns.nvim/commit/f074844b60f9e151970fbcdbeb8a2cd52b6ef25a)), closes [#1095](https://github.com/lewis6991/gitsigns.nvim/issues/1095) * add type annotations for modules ([ac5aba6](https://github.com/lewis6991/gitsigns.nvim/commit/ac5aba6dce8c06ea22bea2c9016f51a2dbf90dc7)) * **async:** add async.pcall ([562dc47](https://github.com/lewis6991/gitsigns.nvim/commit/562dc47189ad3c8696dbf460d38603a74d544849)) * **blame_line:** add option to show when not focused ([2667904](https://github.com/lewis6991/gitsigns.nvim/commit/2667904fb0ee62832c55b56acb9ade3e02a0c202)) * **blame:** add `Gitsigns blame` ([25b6ee4](https://github.com/lewis6991/gitsigns.nvim/commit/25b6ee4be514b38d5bfe950d790a67042e05ef35)) * **blame:** add reblame at commit parent ([f4928ba](https://github.com/lewis6991/gitsigns.nvim/commit/f4928ba14eb6c667786ac7d69927f6aee6719f1e)) * **blame:** run formatter with pcall ([9ca00df](https://github.com/lewis6991/gitsigns.nvim/commit/9ca00df1c84fc0a1ed18c79156c06b081dc1da1f)) * **blame:** set filetype to gitsigns.blame ([0dc8866](https://github.com/lewis6991/gitsigns.nvim/commit/0dc886637f9686b7cfd245a4726f93abeab19d4a)), closes [#1049](https://github.com/lewis6991/gitsigns.nvim/issues/1049) * **config:** deprecate highlight groups in config.signs ([3d7e49c](https://github.com/lewis6991/gitsigns.nvim/commit/3d7e49c201537ee0293a1a3abe67b67f8e7648a5)) * **config:** improve deprecation message ([fa42613](https://github.com/lewis6991/gitsigns.nvim/commit/fa42613096ebfa5fee1ea87d70f8625ab9685d01)) * deprecate some functions ([8b74e56](https://github.com/lewis6991/gitsigns.nvim/commit/8b74e560f7cba19b45b7d72a3cf8fb769316d259)) * detect repo errors ([899e993](https://github.com/lewis6991/gitsigns.nvim/commit/899e993850084ea33d001ec229d237bc020c19ae)) * **nav:** add target option ([9291836](https://github.com/lewis6991/gitsigns.nvim/commit/929183666540e164fa74028954ade62fa703fa1a)) * nicer errors for failed stages ([9521fe8](https://github.com/lewis6991/gitsigns.nvim/commit/9521fe8be39255b9abc6ec54e352bf04c410f5cf)) * remove current_line_blame_formatter_opts ([92a8fbb](https://github.com/lewis6991/gitsigns.nvim/commit/92a8fbb8453571978468e4ad2d4f8cd302d79eab)) * remove support for yadm ([61f5b64](https://github.com/lewis6991/gitsigns.nvim/commit/61f5b6407611a25e2d407ac0bc60e5c87c25ad72)) * set bufname for commit buffers ([e4efe9b](https://github.com/lewis6991/gitsigns.nvim/commit/e4efe9b99b7c473e9f917edf441cec48c05fd99e)) * share Repo objects across buffers ([2593efa](https://github.com/lewis6991/gitsigns.nvim/commit/2593efa3c53f41987d99bf8727f67154e88c0c91)) * **signs:** able staged signs by default ([b8cf5e8](https://github.com/lewis6991/gitsigns.nvim/commit/b8cf5e8efaa0036d493a2e2dfed768c3a03fac73)) * **signs:** improve sign generation from hunks ([2d2156a](https://github.com/lewis6991/gitsigns.nvim/commit/2d2156a2f8c6babbf5f10aea6df23993416f0f28)) * tweak how commit buffers are processed ([47c8e3e](https://github.com/lewis6991/gitsigns.nvim/commit/47c8e3e571376b24de62408fd0c9d12f0a9fc0a3)) * use luajit buffers to serialize thread data ([233bcbf](https://github.com/lewis6991/gitsigns.nvim/commit/233bcbfda3a04e19ae4fb365a8cbd32d9aa8c0d1)) ### Bug Fixes * [#1182](https://github.com/lewis6991/gitsigns.nvim/issues/1182) ([632fda7](https://github.com/lewis6991/gitsigns.nvim/commit/632fda72df903255dc1683cd739dceaa7338128a)) * [#1182](https://github.com/lewis6991/gitsigns.nvim/issues/1182) (take 2) ([3868c17](https://github.com/lewis6991/gitsigns.nvim/commit/3868c176d406b217ec8961e47ad033105ddc486c)) * [#1185](https://github.com/lewis6991/gitsigns.nvim/issues/1185) ([8fb9e75](https://github.com/lewis6991/gitsigns.nvim/commit/8fb9e7515d38c042f26bfa894a0b7cb36e27c895)) * [#1185](https://github.com/lewis6991/gitsigns.nvim/issues/1185) (take 2) ([fd7457f](https://github.com/lewis6991/gitsigns.nvim/commit/fd7457fa13b7b5c63b5dc164c6cbf9192fbe72d1)) * [#1185](https://github.com/lewis6991/gitsigns.nvim/issues/1185) (take 3) ([f301005](https://github.com/lewis6991/gitsigns.nvim/commit/f301005d8eaa15ef61ed6e7dbaa8c5193541ac37)) * [#1187](https://github.com/lewis6991/gitsigns.nvim/issues/1187) ([d8918f0](https://github.com/lewis6991/gitsigns.nvim/commit/d8918f06624dd53b9a82bd0e29c31bcfd541b40d)) * add cli binding for show ([d9f997d](https://github.com/lewis6991/gitsigns.nvim/commit/d9f997dba757be01434ed3538d202f88286df476)) * add nil check ([9d80331](https://github.com/lewis6991/gitsigns.nvim/commit/9d803313b7384bd52e0a9ad19307e9ae774fc926)) * add nil check ([7178d1a](https://github.com/lewis6991/gitsigns.nvim/commit/7178d1a430dcfff8a4c92d78b9e39e0297a779c0)) * **attach:** do not attach to fugitive directory buffers ([cf1ffe6](https://github.com/lewis6991/gitsigns.nvim/commit/cf1ffe682d3ac3a3cb89a7bdf50cc15ff1fadb8e)), closes [#1198](https://github.com/lewis6991/gitsigns.nvim/issues/1198) * **attach:** resolve error viewing fugitive trees ([#1058](https://github.com/lewis6991/gitsigns.nvim/issues/1058)) ([89a4dce](https://github.com/lewis6991/gitsigns.nvim/commit/89a4dce7c94c40c89774d3cb3a7788a9ecf412c0)) * **blame:** ensure blame object is valid when all lines are requested ([817bd84](https://github.com/lewis6991/gitsigns.nvim/commit/817bd848fffe82e697b4da656e3f2834cd0665c5)), closes [#1156](https://github.com/lewis6991/gitsigns.nvim/issues/1156) * **blame:** handle incremental output with a buffered reader ([e9c4187](https://github.com/lewis6991/gitsigns.nvim/commit/e9c4187c3774a46df2d086a66cf3a7e6bea4c432)), closes [#1084](https://github.com/lewis6991/gitsigns.nvim/issues/1084) * **blame:** include error message in error ([d03a1c9](https://github.com/lewis6991/gitsigns.nvim/commit/d03a1c9a1045122823af97e351719227ed3718eb)) * **blame:** parse blame info correctly ([0595724](https://github.com/lewis6991/gitsigns.nvim/commit/0595724fa9516a35696ff6b1e3cb95b6462b38b1)), closes [#1065](https://github.com/lewis6991/gitsigns.nvim/issues/1065) * **blame:** popupmenu error ([93c38d9](https://github.com/lewis6991/gitsigns.nvim/commit/93c38d97260330e8501ccda1e6000c858af0d603)), closes [#1061](https://github.com/lewis6991/gitsigns.nvim/issues/1061) * **blame:** render blame end_col out of range ([aa12bb9](https://github.com/lewis6991/gitsigns.nvim/commit/aa12bb9cd22f1a612dd9cda6c6fc26475e94fc4f)) * **blame:** replace dot with dash in blame file type name ([0ed4669](https://github.com/lewis6991/gitsigns.nvim/commit/0ed466953fe5885166e0d60799172a8b1f752d16)) * **blame:** respect original blame winbar ([d0db8ef](https://github.com/lewis6991/gitsigns.nvim/commit/d0db8ef6a0489ed6af0baacb101a7b733c5d5de1)) * **blame:** restore original options when blame window is closed ([564849a](https://github.com/lewis6991/gitsigns.nvim/commit/564849a17bf5c5569e0bae98c8328de9c7a1ed29)) * **blame:** show current buffer line blame immediately ([6b1a14e](https://github.com/lewis6991/gitsigns.nvim/commit/6b1a14eabcebbcca1b9e9163a26b2f8371364cb7)) * **blame:** show same commit twice or more ([#1136](https://github.com/lewis6991/gitsigns.nvim/issues/1136)) ([ee7634a](https://github.com/lewis6991/gitsigns.nvim/commit/ee7634ab4f0a6606438fe13e16cbf2065589a5ed)) * **blame:** show the winbar if the main window has it enabled ([17e8fd6](https://github.com/lewis6991/gitsigns.nvim/commit/17e8fd66182c9ad79dc129451ad015af3d27529c)) * **blame:** track buffers changes correctly in the cache ([0349546](https://github.com/lewis6991/gitsigns.nvim/commit/0349546134d8a3a3c3a33e2e781b8d7bd07ea156)) * **blame:** update current_line_blame when attaching ([8df63f2](https://github.com/lewis6991/gitsigns.nvim/commit/8df63f2ddc615feb71fd4aee45a4cee022876df1)) * change assert to eprint ([7c27a30](https://github.com/lewis6991/gitsigns.nvim/commit/7c27a30450130cd59c4994a6755e3c5d74d83e76)) * derive Staged*Cul highlight correctly ([1d2cb56](https://github.com/lewis6991/gitsigns.nvim/commit/1d2cb568a7105a860941ef45a01b13709d7aa9d2)) * diffthis vertical option ([dcdcfcb](https://github.com/lewis6991/gitsigns.nvim/commit/dcdcfcb15eb7c6fc6023dbf03e9644e9d5b2f484)) * do not mix staged signs with normal signs ([9541f5e](https://github.com/lewis6991/gitsigns.nvim/commit/9541f5e8e24571723cb02a5c2bf078aeacc5a711)), closes [#1152](https://github.com/lewis6991/gitsigns.nvim/issues/1152) * do not show staged signs for different bases ([0edca9d](https://github.com/lewis6991/gitsigns.nvim/commit/0edca9d1a06db1ae95d79c210825711172fb2802)), closes [#1118](https://github.com/lewis6991/gitsigns.nvim/issues/1118) * **docs:** Add signs_staged to default config in README ([d44a794](https://github.com/lewis6991/gitsigns.nvim/commit/d44a7948ffc717af578c424add818b7684c7ed68)) * fileformat autocmd bug ([f41b934](https://github.com/lewis6991/gitsigns.nvim/commit/f41b934e70e2ae9b0a7a3cb1a5a7d172a4d8f1fd)), closes [#1123](https://github.com/lewis6991/gitsigns.nvim/issues/1123) * get the repo version of the username ([2e5719c](https://github.com/lewis6991/gitsigns.nvim/commit/2e5719c79aead05c4269d6bd250acbc9c4d26d37)) * GitSignsChanged autocmd for staged hunks ([ac38d78](https://github.com/lewis6991/gitsigns.nvim/commit/ac38d7860b258ec07085d8d1931e1a487bcee21d)), closes [#1168](https://github.com/lewis6991/gitsigns.nvim/issues/1168) * handle repos with no commits ([0cd4f0a](https://github.com/lewis6991/gitsigns.nvim/commit/0cd4f0aa1067b7261f0649b3124e1159dac3df8b)) * handle terminal-only highlights ([356df59](https://github.com/lewis6991/gitsigns.nvim/commit/356df59308d8b87486644d2324d7558ac0f3db36)) * help triggering text autocmds ([2a7b39f](https://github.com/lewis6991/gitsigns.nvim/commit/2a7b39f4d282935f8b44cbe82879af69c7472f5c)) * improve support for worktrees in bare repos ([6811483](https://github.com/lewis6991/gitsigns.nvim/commit/68114837e81ca16d06514c3a997c9102d1b25c15)), closes [#1160](https://github.com/lewis6991/gitsigns.nvim/issues/1160) * lint ([39b5b6f](https://github.com/lewis6991/gitsigns.nvim/commit/39b5b6f48bde0595ce68007ffce408c5d7ac1f79)) * more EOL fixes ([f10fdda](https://github.com/lewis6991/gitsigns.nvim/commit/f10fddafe06f7ab7931031b394a26b2f3f434f3e)), closes [#1145](https://github.com/lewis6991/gitsigns.nvim/issues/1145) * **nav:** misc bugs ([7516bac](https://github.com/lewis6991/gitsigns.nvim/commit/7516bac5639a9ce8e7b199066199a02cb3057230)) * nil check for repo cache ([375c44b](https://github.com/lewis6991/gitsigns.nvim/commit/375c44bdfdde25585466a966f00c2e291db74f2d)) * nil check for repo info ([e784e5a](https://github.com/lewis6991/gitsigns.nvim/commit/e784e5a078f993f7218b8a857cb581d5b9ca42dc)) * partial staging of staged signs ([31d2dcd](https://github.com/lewis6991/gitsigns.nvim/commit/31d2dcd144c7404dacbd2ca36b5abd37cc9fa506)) * random errors from blame autocommands ([#1139](https://github.com/lewis6991/gitsigns.nvim/issues/1139)) ([2d725fd](https://github.com/lewis6991/gitsigns.nvim/commit/2d725fdd7fe4a612fa3171ca0a965f455d8dc325)) * **repo:** make sure --git-dir is always provided --work-tree ([310018d](https://github.com/lewis6991/gitsigns.nvim/commit/310018d54357b8a3cbbcd2b7f589d12e61d2db35)) * reset diff when quiting diff buffer ([b544bd6](https://github.com/lewis6991/gitsigns.nvim/commit/b544bd62623ca1b483d8b9bfb6d65805f112a320)), closes [#1155](https://github.com/lewis6991/gitsigns.nvim/issues/1155) * revision buffer name parsing for index buffers ([76d88f3](https://github.com/lewis6991/gitsigns.nvim/commit/76d88f3b584e1f83b2aa51663a32cc6ee8d97eff)) * select hunk gets all adjacent linematch hunks ([abc6dec](https://github.com/lewis6991/gitsigns.nvim/commit/abc6dec92232944108250e321858014bf79de245)), closes [#1133](https://github.com/lewis6991/gitsigns.nvim/issues/1133) * **select_hunk:** compatible with <cmd> mapping ([8974fd3](https://github.com/lewis6991/gitsigns.nvim/commit/8974fd397e854bfa13a5130dc32ee357dbade276)) * setqflist("all") should respect change_base ([58bd9e9](https://github.com/lewis6991/gitsigns.nvim/commit/58bd9e98d8e3c5a1c98af312e85247ee1afd3ed2)) * **signs:** avoid placing signs on lnum 0 ([2f9f20e](https://github.com/lewis6991/gitsigns.nvim/commit/2f9f20ea3baacc077e940b7878a46a8295129418)) * sort get_nav_hunks to handle mixed hunk states ([80214a8](https://github.com/lewis6991/gitsigns.nvim/commit/80214a857ce512cc64964abddc1d8eb5a3e28396)) * string.buffer not found ([8639036](https://github.com/lewis6991/gitsigns.nvim/commit/863903631e676b33e8be2acb17512fdc1b80b4fb)), closes [#1126](https://github.com/lewis6991/gitsigns.nvim/issues/1126) * support blame for git < 2.41 ([a5b801e](https://github.com/lewis6991/gitsigns.nvim/commit/a5b801e7b16220e75d459919edcb5eb37b1de9cb)), closes [#1093](https://github.com/lewis6991/gitsigns.nvim/issues/1093) * toggle_current_line_blame ([0e39e9a](https://github.com/lewis6991/gitsigns.nvim/commit/0e39e9afcfc180d55ac8f0691a230703683ddb0f)), closes [#1072](https://github.com/lewis6991/gitsigns.nvim/issues/1072) * typo on dprint ([6f8dbdb](https://github.com/lewis6991/gitsigns.nvim/commit/6f8dbdbd41725fa11178e78d6e4c987038a8ece9)) * upstream fixes for system() ([c2a2739](https://github.com/lewis6991/gitsigns.nvim/commit/c2a273980eb2cbcabcd54690f06f041ea0c225c6)) * use non-deprecated versions of vim.validate ([0883d0f](https://github.com/lewis6991/gitsigns.nvim/commit/0883d0f67c1b728713deeddfcec4aabf71410801)) * use norm! to prevent user remapping ([4daf702](https://github.com/lewis6991/gitsigns.nvim/commit/4daf7022f1481edf1e8fb9947df13bb07c18e89a)) * **util:** ignore endofline when running blame ([def49e4](https://github.com/lewis6991/gitsigns.nvim/commit/def49e48c6329527e344d0c99a0d2cd9fdf6bb84)) * wait for buffer to attach in M.show ([1c128d4](https://github.com/lewis6991/gitsigns.nvim/commit/1c128d4585d89f39ddea9ef9f5f6b84edd3b66b9)), closes [#1091](https://github.com/lewis6991/gitsigns.nvim/issues/1091) * **watcher:** do not ignore any updates ([5840f89](https://github.com/lewis6991/gitsigns.nvim/commit/5840f89c50b7af6b2f9c30e7fe37b797aef60ba9)) * **watcher:** fix debounce ([f846c50](https://github.com/lewis6991/gitsigns.nvim/commit/f846c507242a74d9a458bff2d029bd2eae8c0ca1)), closes [#1046](https://github.com/lewis6991/gitsigns.nvim/issues/1046) * wipeout buf after closing the blame_line/preview_hunk window ([abcd00a](https://github.com/lewis6991/gitsigns.nvim/commit/abcd00a7d5bc1a9470cb21b023c575acade3e4db)) ### Performance Improvements * **blame:** some improvements ([9cdfcb5](https://github.com/lewis6991/gitsigns.nvim/commit/9cdfcb5f038586c36ad8b010f7e479f6a6f95a63)) ## [0.9.0](https://github.com/lewis6991/gitsigns.nvim/compare/v0.8.1...v0.9.0) (2024-06-12) ### ⚠ BREAKING CHANGES * **setup:** make setup() synchronous * drop support for nvim v0.8 ### Features * drop support for nvim v0.8 ([d9d94e0](https://github.com/lewis6991/gitsigns.nvim/commit/d9d94e055a19415767bb073e8dd86028105c4319)) * **setup:** make setup() synchronous ([720061a](https://github.com/lewis6991/gitsigns.nvim/commit/720061aa152faedfe4099dfb92d2b3fcb0e55edc)) ### Bug Fixes * add workaround for Lazy issue ([e31d214](https://github.com/lewis6991/gitsigns.nvim/commit/e31d2149d9f3fb056bfd5b3416b2e818be10aabe)) * **attach:** allow attaching inside .git/ ([9cafac3](https://github.com/lewis6991/gitsigns.nvim/commit/9cafac31a091267838e1e90fd6e083d37611f516)), closes [#923](https://github.com/lewis6991/gitsigns.nvim/issues/923) * **attach:** detach on when the buffer name changes ([75dc649](https://github.com/lewis6991/gitsigns.nvim/commit/75dc649106827183547d3bedd4602442340d2f7f)), closes [#1021](https://github.com/lewis6991/gitsigns.nvim/issues/1021) * **attach:** fix worktree attaching ([54b9df4](https://github.com/lewis6991/gitsigns.nvim/commit/54b9df401b8f21f4e6ca537ec47a109394aaccd7)), closes [#1020](https://github.com/lewis6991/gitsigns.nvim/issues/1020) * **blame:** avoid right-aligned blame overlapping buftext ([20f305d](https://github.com/lewis6991/gitsigns.nvim/commit/20f305d63bc86852821ac47d9967e73931f7130b)) * handle untracked files for custom bases ([af3fdad](https://github.com/lewis6991/gitsigns.nvim/commit/af3fdad8ddcadbdad835975204f6503310526fd9)), closes [#1022](https://github.com/lewis6991/gitsigns.nvim/issues/1022) * scheduling in cwd watching ([c96e3cf](https://github.com/lewis6991/gitsigns.nvim/commit/c96e3cf4767ee98030bff855e7a6f07cfc6d427f)) * **update:** always get object contents from object names ([a28bb1d](https://github.com/lewis6991/gitsigns.nvim/commit/a28bb1db506df663b063cc63f44fbbda178255a7)), closes [#847](https://github.com/lewis6991/gitsigns.nvim/issues/847) * use latest api in 0.10 ([bc933d2](https://github.com/lewis6991/gitsigns.nvim/commit/bc933d24a669608968ff4791b14d2d9554813a65)) * **util:** close file after reading ([f65d1d8](https://github.com/lewis6991/gitsigns.nvim/commit/f65d1d82013e032ca6c199b62f08089b420b068c)) * **watcher:** throttle watcher handler ([de18f6b](https://github.com/lewis6991/gitsigns.nvim/commit/de18f6b749f6129eb9042a2038590872df4c94a9)) * **watcher:** workaround weird annoying libuv bug ([4b53134](https://github.com/lewis6991/gitsigns.nvim/commit/4b53134ce5fdd58e6c52c49fb906b6e7a347d137)), closes [#1027](https://github.com/lewis6991/gitsigns.nvim/issues/1027) * wrong api name in stable ([805610a](https://github.com/lewis6991/gitsigns.nvim/commit/805610a9393fa231f2c2b49cb521bfa413fadb3d)) ## [0.8.1](https://github.com/lewis6991/gitsigns.nvim/compare/v0.8.0...v0.8.1) (2024-04-30) ### Bug Fixes * **blame:** check win is valid after running blame ([7e38f07](https://github.com/lewis6991/gitsigns.nvim/commit/7e38f07cab0e5387f9f41e92474db174a63a4725)) * **reset:** handle 'endofline' when resetting hunks ([7aa9a56](https://github.com/lewis6991/gitsigns.nvim/commit/7aa9a567127d679c6ca639e9e88c546d72924296)) * **yadm:** correct ls-files check ([035da03](https://github.com/lewis6991/gitsigns.nvim/commit/035da036e68e509ed158414416c827d022d914bd)) ## [0.8.0](https://github.com/lewis6991/gitsigns.nvim/compare/v0.7.0...v0.8.0) (2024-04-17) ### ⚠ BREAKING CHANGES * **docs:** Use the new attached_to_untracked setting * change default of attached_to_untracked to false ### Features * **actions:** add callback to async actions ([4e90cf9](https://github.com/lewis6991/gitsigns.nvim/commit/4e90cf984ced787b7439c42678ec957da3583049)) * **blame:** add rev option to blame_line() ([0994d89](https://github.com/lewis6991/gitsigns.nvim/commit/0994d89323c2ebb4abb38cab15aad00913588b0f)), closes [#952](https://github.com/lewis6991/gitsigns.nvim/issues/952) * **blame:** support extra options ([3358280](https://github.com/lewis6991/gitsigns.nvim/commit/3358280054808b45f711191df481fcffc12ca761)), closes [#953](https://github.com/lewis6991/gitsigns.nvim/issues/953) [#959](https://github.com/lewis6991/gitsigns.nvim/issues/959) * change default of attached_to_untracked to false ([590d077](https://github.com/lewis6991/gitsigns.nvim/commit/590d077c551c0bd2fc8b9f658e4704ccd0423a2e)) * configurable auto attach ([#918](https://github.com/lewis6991/gitsigns.nvim/issues/918)) ([3e6e91b](https://github.com/lewis6991/gitsigns.nvim/commit/3e6e91b09f0468c32d3b96dcacf4b947f037ce25)) * enable the new version of inline_preview ([d195f0c](https://github.com/lewis6991/gitsigns.nvim/commit/d195f0c35ced5174d3ecce1c4c8ebb3b5bc23fa9)) * **nav:** add nav_hunk() ([59bdc18](https://github.com/lewis6991/gitsigns.nvim/commit/59bdc1851c7aba8a86ded87fe075ef6de499045c)) * **popup:** add `q` keymap to quit ([b45ff86](https://github.com/lewis6991/gitsigns.nvim/commit/b45ff86f5618d1421a88c12d4feb286b80a1e2d3)) * publish releases to luarocks ([070875f](https://github.com/lewis6991/gitsigns.nvim/commit/070875f9e4eb81eb20cb60996cd1d9086d94b05e)) * update issue templates ([e93a158](https://github.com/lewis6991/gitsigns.nvim/commit/e93a158b8773946dc9940a4321d35c1b52c8e293)) * **yadm:** deprecate ([1bb277b](https://github.com/lewis6991/gitsigns.nvim/commit/1bb277b41d65f68b091e4ab093f59e68a0def2a6)) ### Bug Fixes * [#986](https://github.com/lewis6991/gitsigns.nvim/issues/986) ([05226b4](https://github.com/lewis6991/gitsigns.nvim/commit/05226b4d41226af8045841b3e56b6cc12d7a1cd0)) * [#989](https://github.com/lewis6991/gitsigns.nvim/issues/989) ([36d961d](https://github.com/lewis6991/gitsigns.nvim/commit/36d961d3d11b72229aaa576dfc8e7f5e05510af8)) * **actions:** prev_hunk works with wrap on line 1 ([2b96835](https://github.com/lewis6991/gitsigns.nvim/commit/2b96835a2b700f31303ebad0696f0abdbe8477ed)), closes [#806](https://github.com/lewis6991/gitsigns.nvim/issues/806) * attach to fugitive and gitsigns buffers ([81369ed](https://github.com/lewis6991/gitsigns.nvim/commit/81369ed5405ec0c5d55a9608b495dbf827415116)), closes [#593](https://github.com/lewis6991/gitsigns.nvim/issues/593) * bad deprecation message ([a4db718](https://github.com/lewis6991/gitsigns.nvim/commit/a4db718c78bff65198e3b63f1043f1e7bb5e05c8)), closes [#965](https://github.com/lewis6991/gitsigns.nvim/issues/965) * **blame:** check buffer still exists after loading ([70584ff](https://github.com/lewis6991/gitsigns.nvim/commit/70584ff9aae8078b64430c574079d79620b8f06d)), closes [#946](https://github.com/lewis6991/gitsigns.nvim/issues/946) * **blame:** put ignore-revs-file in correct position ([5f267aa](https://github.com/lewis6991/gitsigns.nvim/commit/5f267aa2fec145eb9fa11be8ae7b3d8b1939fe00)), closes [#975](https://github.com/lewis6991/gitsigns.nvim/issues/975) * changedelete symbol with linematch enabled ([41dc075](https://github.com/lewis6991/gitsigns.nvim/commit/41dc075ef67b556b0752ad3967649371bd95cb95)) * check bcache in get_hunks ([1a50b94](https://github.com/lewis6991/gitsigns.nvim/commit/1a50b94066def8591d5f65bd60a4233902e9def4)), closes [#979](https://github.com/lewis6991/gitsigns.nvim/issues/979) [#981](https://github.com/lewis6991/gitsigns.nvim/issues/981) * check for WinResized ([c093623](https://github.com/lewis6991/gitsigns.nvim/commit/c0936237f24d01eb4974dd3de38df7888414be3e)) * **cli:** do not print result ([7e31d81](https://github.com/lewis6991/gitsigns.nvim/commit/7e31d8123f14d55f4a3f982d05ddae4f3bf9276a)) * **current_line_blame:** update on WinResized ([f0733b7](https://github.com/lewis6991/gitsigns.nvim/commit/f0733b793a5e2663fd6d101de5beda68eec33967)), closes [#966](https://github.com/lewis6991/gitsigns.nvim/issues/966) * **diffthis:** populate b:gitsigns_head ([50577f0](https://github.com/lewis6991/gitsigns.nvim/commit/50577f0186686b404d12157d463fb6bc4abba726)), closes [#949](https://github.com/lewis6991/gitsigns.nvim/issues/949) * do not error when cwd does not exist ([826ad69](https://github.com/lewis6991/gitsigns.nvim/commit/826ad6942907ff08b02b8310b783e7275fdfb761)) * **docs:** Use the new attached_to_untracked setting ([2c2463d](https://github.com/lewis6991/gitsigns.nvim/commit/2c2463dbd82eddd7dbab881c3a62cfbfbe3c67ae)) * **dos:** correct check for dos files ([aeab36f](https://github.com/lewis6991/gitsigns.nvim/commit/aeab36f4b5524a765381ef84a2c57b2e799c934d)) * followup ([690f298](https://github.com/lewis6991/gitsigns.nvim/commit/690f298c4cac9190ddb7eedeeee2a3cc446622f7)) * **git:** support older versions of git ([4e34864](https://github.com/lewis6991/gitsigns.nvim/commit/4e348641b8206c3b8d23080999e3ddbe4ca90efc)) * **hl:** highlights for Nvim v0.9 ([fb9fd53](https://github.com/lewis6991/gitsigns.nvim/commit/fb9fd5312476b51a42a98122616e1c448d823d5c)), closes [#939](https://github.com/lewis6991/gitsigns.nvim/issues/939) * **manager:** manager.update() never resolve when buf_check() fails ([6e05045](https://github.com/lewis6991/gitsigns.nvim/commit/6e05045fb1a4845fe44f5c54aafe024444c422ba)) * **nav:** followup for [#976](https://github.com/lewis6991/gitsigns.nvim/issues/976) ([ee5b6ba](https://github.com/lewis6991/gitsigns.nvim/commit/ee5b6ba0b55707628704bcd8d3554d1a05207b99)) * release-please branch ([031abb0](https://github.com/lewis6991/gitsigns.nvim/commit/031abb065452248c30ce8d8fb4d4eb9eeb69d1f0)) * **setqflist:** CLI ([e20c96e](https://github.com/lewis6991/gitsigns.nvim/commit/e20c96e9c3b9b2241939ce437d03926ba7315eaa)), closes [#907](https://github.com/lewis6991/gitsigns.nvim/issues/907) * **stage:** staging of files with no nl at eof ([c097cb2](https://github.com/lewis6991/gitsigns.nvim/commit/c097cb255096f333e14d341082a84f572b394fa2)) * trigger GitSignsUpdate autocmd more often ([1389134](https://github.com/lewis6991/gitsigns.nvim/commit/1389134ba94643dd3b8ce2e1bf142d1c0432a4f2)) * typo in README ([4aaacbf](https://github.com/lewis6991/gitsigns.nvim/commit/4aaacbf5e5e2218fd05eb75703fe9e0f85335803)) * update lua-guide link in README ([c5ff762](https://github.com/lewis6991/gitsigns.nvim/commit/c5ff7628e19a47ec14d3657294cc074ecae27b99)) * use documented highlight groups as fallback ([300a306](https://github.com/lewis6991/gitsigns.nvim/commit/300a306da9973e81c2c06460f71fd7a079df1f36)) * **version:** handle version checks more gracefully ([3cb0f84](https://github.com/lewis6991/gitsigns.nvim/commit/3cb0f8431f56996a4af2924d78a98a09b6add095)), closes [#948](https://github.com/lewis6991/gitsigns.nvim/issues/948) [#960](https://github.com/lewis6991/gitsigns.nvim/issues/960) * **watcher:** improve buffer check in handler ([078041e](https://github.com/lewis6991/gitsigns.nvim/commit/078041e9d060a386b0c9d3a8c7a7b019a35d3fb0)) neovim-gitsigns-2.0.0/CONTRIBUTING.md000066400000000000000000000014231513053142700170650ustar00rootroot00000000000000**Please do not raise PR's for new features, only bug fixes will be reviewed.** ## Requirements - [Luarocks](https://luarocks.org/) - `brew install luarocks` ## Generating docs Most of the documentation is handwritten however the documentation for the configuration is generated from `lua/gitsigns/config.lua` which contains the configuration schema. The help file is generated by `gen_help.lua` and uses `emmylua_doc_cli` to extract API documentation from the Lua sources. The documentation can be updated with: ```bash make doc ``` ## Testsuite The testsuite uses the same framework as Neovims functionaltest suite. This is just busted with lots of helper code to create headless neovim instances which are controlled via RPC. To run the testsuite: ```bash make test ``` neovim-gitsigns-2.0.0/LICENSE000066400000000000000000000020561513053142700156440ustar00rootroot00000000000000MIT License Copyright (c) 2020 Lewis Russell 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. neovim-gitsigns-2.0.0/Makefile000066400000000000000000000110051513053142700162710ustar00rootroot00000000000000export XDG_DATA_HOME ?= $(HOME)/.data export PJ_ROOT=$(PWD) ifeq ($(shell uname -s),Darwin) UNAME ?= MACOS else UNAME ?= LINUX endif MAKEFLAGS += --no-builtin-rules MAKEARGS += --warn-undefined-variables .DEFAULT_GOAL := build .PHONY: build build: doc format ################################################################################ # nvim-test ################################################################################ export NVIM_RUNNER_VERSION := v0.11.0 export NVIM_TEST_VERSION ?= v0.11.0 NVIM_TEST := deps/nvim-test .PHONY: nvim-test nvim-test: $(NVIM_TEST) $(NVIM_TEST): git clone --depth 1 --branch v1.3.0 https://github.com/lewis6991/nvim-test $@ $@/bin/nvim-test --init ################################################################################ # Testsuite ################################################################################ FILTER ?= .* .PHONY: test test: $(NVIM_TEST) $(NVIM_TEST)/bin/nvim-test test \ --lpath=$(PWD)/lua/?.lua \ --verbose \ --filter="$(FILTER)" -@stty sane .PHONY: test-all test-all: test-095 test-010 test-nightly .PHONY: test-010 test-010: $(MAKE) $(MAKEFLAGS) test NVIM_TEST_VERSION=v0.10.4 .PHONY: test-011 test-011: $(MAKE) $(MAKEFLAGS) test NVIM_TEST_VERSION=v0.11.0 .PHONY: test-nightly test-nightly: $(MAKE) $(MAKEFLAGS) test NVIM_TEST_VERSION=nightly NVIM := $(XDG_DATA_HOME)/nvim-test/nvim-runner-$(NVIM_RUNNER_VERSION)/bin/nvim ################################################################################ # Stylua ################################################################################ STYLUA_PLATFORM_MACOS := macos-aarch64 STYLUA_PLATFORM_LINUX := linux-x86_64 STYLUA_PLATFORM := $(STYLUA_PLATFORM_$(UNAME)) STYLUA_VERSION := v2.0.2 STYLUA_ZIP := stylua-$(STYLUA_PLATFORM).zip STYLUA_URL_BASE := https://github.com/JohnnyMorganz/StyLua/releases/download STYLUA_URL := $(STYLUA_URL_BASE)/$(STYLUA_VERSION)/$(STYLUA_ZIP) STYLUA := deps/stylua .INTERMEDIATE: $(STYLUA_ZIP) $(STYLUA_ZIP): wget $(STYLUA_URL) .PHONY: stylua stylua: $(STYLUA) $(STYLUA): $(STYLUA_ZIP) unzip $< -d $(dir $@) LUA_FILES := $(shell git ls-files lua test) .PHONY: format-check format-check: $(STYLUA) $(STYLUA) --check $(LUA_FILES) .PHONY: format format: $(STYLUA) $(STYLUA) $(LUA_FILES) ################################################################################ # Emmylua ################################################################################ ifeq ($(shell uname -m),arm64) EMMYLUA_ARCH ?= arm64 else EMMYLUA_ARCH ?= x64 endif EMMYLUA_REF := 0.19.0 EMMYLUA_OS ?= $(shell uname -s | tr '[:upper:]' '[:lower:]') EMMYLUA_RELEASE_URL_BASE := https://github.com/EmmyLuaLs/emmylua-analyzer-rust/releases/download/$(EMMYLUA_REF) EMMYLUA_DIR := deps/emmylua-$(EMMYLUA_REF) EMMYLUA_RELEASE_URL := $(EMMYLUA_RELEASE_URL_BASE)/emmylua_check-$(EMMYLUA_OS)-$(EMMYLUA_ARCH).tar.gz EMMYLUA_RELEASE_TAR := deps/emmylua_check-$(EMMYLUA_REF)-$(EMMYLUA_OS)-$(EMMYLUA_ARCH).tar.gz EMMYLUA_BIN := $(EMMYLUA_DIR)/emmylua_check EMMYLUADOC_RELEASE_URL := $(EMMYLUA_RELEASE_URL_BASE)/emmylua_doc_cli-$(EMMYLUA_OS)-$(EMMYLUA_ARCH).tar.gz EMMYLUADOC_RELEASE_TAR := deps/emmylua_doc-$(EMMYLUA_REF)-$(EMMYLUA_OS)-$(EMMYLUA_ARCH).tar.gz EMMYLUADOC_BIN := $(EMMYLUA_DIR)/emmylua_doc_cli .PHONY: emmylua emmylua: $(EMMYLUA_BIN) $(EMMYLUA_BIN): mkdir -p $(EMMYLUA_DIR) curl -L $(EMMYLUA_RELEASE_URL) -o $(EMMYLUA_RELEASE_TAR) tar -xzf $(EMMYLUA_RELEASE_TAR) -C $(EMMYLUA_DIR) $(EMMYLUADOC_BIN): mkdir -p $(EMMYLUA_DIR) curl -L $(EMMYLUADOC_RELEASE_URL) -o $(EMMYLUADOC_RELEASE_TAR) tar -xzf $(EMMYLUADOC_RELEASE_TAR) -C $(EMMYLUA_DIR) NVIM_TEST_RUNTIME=$(XDG_DATA_HOME)/nvim-test/nvim-test-$(NVIM_TEST_VERSION)/share/nvim/runtime $(NVIM_TEST_RUNTIME): $(NVIM_TEST) $^/bin/nvim-test --init ################################################################################ # Type check ################################################################################ .PHONY: emmylua-check emmylua-check: $(EMMYLUA_BIN) $(NVIM_TEST_RUNTIME) VIMRUNTIME=$(NVIM_TEST_RUNTIME) \ $(EMMYLUA_BIN) . \ --ignore 'test/**/*' \ --ignore gen_help.lua ################################################################################ # Docs ################################################################################ .PHONY: doc doc: $(NVIM_TEST) $(NVIM_TEST_RUNTIME) $(EMMYLUADOC_BIN) VIMRUNTIME=$(NVIM_TEST_RUNTIME) \ $(EMMYLUADOC_BIN) lua --output emydoc --output-format json $(NVIM) -l ./gen_help.lua @echo Updated help .PHONY: doc-check doc-check: doc git diff --exit-code -- doc neovim-gitsigns-2.0.0/README.md000066400000000000000000000242131513053142700161150ustar00rootroot00000000000000# gitsigns.nvim [![CI](https://github.com/lewis6991/gitsigns.nvim/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/lewis6991/gitsigns.nvim/actions?query=workflow%3ACI) [![Version](https://img.shields.io/github/v/release/lewis6991/gitsigns.nvim)](https://github.com/lewis6991/gitsigns.nvim/releases) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Dotfyle](https://dotfyle.com/plugins/lewis6991/gitsigns.nvim/shield)](https://dotfyle.com/plugins/lewis6991/gitsigns.nvim) Deep buffer integration for Git ## 👀 Preview | Hunk Actions | Line Blame | | --- | ----------- | | | | ## ✨ Features
Signs - Adds signs to the sign column to indicate added, changed, and deleted lines. ![image](https://github.com/user-attachments/assets/e49ea0bf-c427-41fb-a67f-77c2d413a7cf) - Supports different signs for staged changes. ![image](https://github.com/user-attachments/assets/28a3e286-96fa-478c-93a3-8028f9bd7123) - Add counts to signs. ![image](https://github.com/user-attachments/assets/d007b924-6811-44ea-b936-d8da4dc00b68)
Hunk Actions - Stage/unstage hunks with `:Gitsigns stage_hunk`. - Reset hunks with `:Gitsigns reset_hunk`. - Also works on partial hunks in visual mode. - Preview hunks inline with `:Gitsigns preview_hunk_inline` ![image](https://github.com/user-attachments/assets/60acd664-f4a8-4737-ba65-969f1efa7971) - Preview hunks in popup with `:Gitsigns preview_hunk` ![image](https://github.com/user-attachments/assets/d2a9b801-5857-4054-80a8-195d111f4e8c) - Navigate between hunks with `:Gitsigns nav_hunk next/prev`.
Blame - Show blame of current buffer using `:Gitsigns blame`. ![image](https://github.com/user-attachments/assets/7d881e94-6e16-4f98-a526-7e785b11acf9) - Show blame information for the current line in popup with `:Gitsigns blame_line`. ![image](https://github.com/user-attachments/assets/03ff7557-b538-4cd1-9478-f893bf7e616e) - Show blame information for the current line in virtual text. ![image](https://github.com/user-attachments/assets/0c79e880-6a6d-4c3f-aa62-33f734725cfd) - Enable with `setup({ current_line_blame = true })`. - Toggle with `:Gitsigns toggle_current_line_blame`
Diff - Change the revision for the signs with `:Gitsigns change_base `. - Show the diff of the current buffer with the index or any revision with `:Gitsigns diffthis `. - Show intra-line word-diff in the buffer. ![image](https://github.com/user-attachments/assets/409a1f91-5cee-404b-8b12-66b7db3ecac7) - Enable with `setup({ word_diff = true })`. - Toggle with `:Gitsigns toggle_word_diff`.
Show hunks Quickfix/Location List - Set the quickfix/location list with changes with `:Gitsign setqflist/setloclist`. ![image](https://github.com/user-attachments/assets/c17001a5-b9cf-4a00-9891-5b130c0b4745) Can show hunks for: - whole repository (`target=all`) - attached buffers (`target=attached`) - a specific buffer (`target=[integer]`).
Text Object - Select hunks as a text object. - Can use `vim.keymap.set({'o', 'x'}, 'ih', 'Gitsigns select_hunk')`
Status Line Integration Use `b:gitsigns_status` or `b:gitsigns_status_dict`. `b:gitsigns_status` is formatted using `config.status_formatter`. `b:gitsigns_status_dict` is a dictionary with the keys `added`, `removed`, `changed` and `head`. Example: ```viml set statusline+=%{get(b:,'gitsigns_status','')} ``` For the current branch use the variable `b:gitsigns_head`.
Show different revisions of buffers - Use `:Gitsigns show ` to `:edit` the current buffer at ``
## 📋 Requirements - Neovim >= 0.9.0 > [!TIP] > If your version of Neovim is too old, then you can use a past [release]. > [!WARNING] > If you are running a development version of Neovim (aka `master`), then > breakage may occur if your build is behind latest. - Newish version of git. Older versions may not work with some features. ## 🛠️ Installation & Usage Install using your package manager of choice. No setup required. Optional configuration can be passed to the setup function. Here is an example with most of the default settings: ```lua require('gitsigns').setup { signs = { add = { text = '┃' }, change = { text = '┃' }, delete = { text = '_' }, topdelete = { text = '‾' }, changedelete = { text = '~' }, untracked = { text = '┆' }, }, signs_staged = { add = { text = '┃' }, change = { text = '┃' }, delete = { text = '_' }, topdelete = { text = '‾' }, changedelete = { text = '~' }, untracked = { text = '┆' }, }, signs_staged_enable = true, signcolumn = true, -- Toggle with `:Gitsigns toggle_signs` numhl = false, -- Toggle with `:Gitsigns toggle_numhl` linehl = false, -- Toggle with `:Gitsigns toggle_linehl` word_diff = false, -- Toggle with `:Gitsigns toggle_word_diff` watch_gitdir = { follow_files = true }, auto_attach = true, attach_to_untracked = false, current_line_blame = false, -- Toggle with `:Gitsigns toggle_current_line_blame` current_line_blame_opts = { virt_text = true, virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align' delay = 1000, ignore_whitespace = false, virt_text_priority = 100, use_focus = true, }, current_line_blame_formatter = ', - ', sign_priority = 6, update_debounce = 100, status_formatter = nil, -- Use default max_file_length = 40000, -- Disable if file is longer than this (in lines) preview_config = { -- Options passed to nvim_open_win style = 'minimal', relative = 'cursor', row = 0, col = 1 }, } ``` For information on configuring Neovim via lua please see [nvim-lua-guide]. ### 🎹 Keymaps Gitsigns provides an `on_attach` callback which can be used to setup buffer mappings. Here is a suggested example: ```lua require('gitsigns').setup{ ... on_attach = function(bufnr) local gitsigns = require('gitsigns') local function map(mode, l, r, opts) opts = opts or {} opts.buffer = bufnr vim.keymap.set(mode, l, r, opts) end -- Navigation map('n', ']c', function() if vim.wo.diff then vim.cmd.normal({']c', bang = true}) else gitsigns.nav_hunk('next') end end) map('n', '[c', function() if vim.wo.diff then vim.cmd.normal({'[c', bang = true}) else gitsigns.nav_hunk('prev') end end) -- Actions map('n', 'hs', gitsigns.stage_hunk) map('n', 'hr', gitsigns.reset_hunk) map('v', 'hs', function() gitsigns.stage_hunk({ vim.fn.line('.'), vim.fn.line('v') }) end) map('v', 'hr', function() gitsigns.reset_hunk({ vim.fn.line('.'), vim.fn.line('v') }) end) map('n', 'hS', gitsigns.stage_buffer) map('n', 'hR', gitsigns.reset_buffer) map('n', 'hp', gitsigns.preview_hunk) map('n', 'hi', gitsigns.preview_hunk_inline) map('n', 'hb', function() gitsigns.blame_line({ full = true }) end) map('n', 'hd', gitsigns.diffthis) map('n', 'hD', function() gitsigns.diffthis('~') end) map('n', 'hQ', function() gitsigns.setqflist('all') end) map('n', 'hq', gitsigns.setqflist) -- Toggles map('n', 'tb', gitsigns.toggle_current_line_blame) map('n', 'tw', gitsigns.toggle_word_diff) -- Text object map({'o', 'x'}, 'ih', gitsigns.select_hunk) end } ``` ## 🔗 Plugin Integrations ### [vim-fugitive] When viewing revisions of a file (via `:0Gclog` for example), Gitsigns will attach to the fugitive buffer with the base set to the commit immediately before the commit of that revision. This means the signs placed in the buffer reflect the changes introduced by that revision of the file. ### [trouble.nvim] If installed and enabled (via `config.trouble`; defaults to true if installed), `:Gitsigns setqflist` or `:Gitsigns setloclist` will open Trouble instead of Neovim's built-in quickfix or location list windows. ## 🚫 Non-Goals ### Implement every feature in [vim-fugitive] This plugin is actively developed and by one of the most well regarded vim plugin developers. Gitsigns will only implement features of this plugin if: it is simple, or, the technologies leveraged by Gitsigns (LuaJIT, Libuv, Neovim's API, etc) can provide a better experience. ### Support for other VCS There aren't any active developers of this plugin who use other kinds of VCS, so adding support for them isn't feasible. However a well written PR with a commitment of future support could change this. ## 🔌 Similar plugins - [mini.diff] - [coc-git] - [vim-gitgutter] - [vim-signify] [mini.diff]: https://github.com/echasnovski/mini.diff [coc-git]: https://github.com/neoclide/coc-git [diff-linematch]: https://github.com/neovim/neovim/pull/14537 [luv]: https://github.com/luvit/luv/blob/master/docs.md [nvim-lua-guide]: https://neovim.io/doc/user/lua-guide.html [release]: https://github.com/lewis6991/gitsigns.nvim/releases [trouble.nvim]: https://github.com/folke/trouble.nvim [vim-fugitive]: https://github.com/tpope/vim-fugitive [vim-gitgutter]: https://github.com/airblade/vim-gitgutter [vim-signify]: https://github.com/mhinz/vim-signify [virtual lines]: https://github.com/neovim/neovim/pull/15351 [lspsaga.nvim]: https://github.com/glepnir/lspsaga.nvim neovim-gitsigns-2.0.0/doc/000077500000000000000000000000001513053142700154015ustar00rootroot00000000000000neovim-gitsigns-2.0.0/doc/gitsigns.txt000066400000000000000000001576721513053142700200130ustar00rootroot00000000000000*gitsigns.txt* Gitsigns *gitsigns.nvim* Author: Lewis Russell Version: v2.0.0 Homepage: License: MIT license ============================================================================== INTRODUCTION *gitsigns* Gitsigns is a plugin for Neovim that provides integration with Git via a feature set which includes (but not limited to): • Provides signs in the |signcolumn| to show changed/added/removed lines. • Mappings to operate on hunks to stage, undo or reset against Git's index. Gitsigns is implemented entirely in Lua which is built into Neovim and requires no external dependencies other than git. This is unlike other plugins that require python, node, etc, which need to communicate with Neovim using |RPC|. By default, Gitsigns also uses Neovim's built-in diff library (`vim.diff`) unlike other similar plugins that need to run `git-diff` as an external process which is less efficient, has tighter bottlenecks and requires file IO. ============================================================================== USAGE *gitsigns-usage* No setup required. >lua Optional configuration can be passed to the setup function. Here is an example with most of the default settings: >lua require('gitsigns').setup { signs = { add = { text = '┃' }, change = { text = '┃' }, delete = { text = '_' }, topdelete = { text = '‾' }, changedelete = { text = '~' }, untracked = { text = '┆' }, }, signs_staged = { add = { text = '┃' }, change = { text = '┃' }, delete = { text = '_' }, topdelete = { text = '‾' }, changedelete = { text = '~' }, untracked = { text = '┆' }, }, signs_staged_enable = true, signcolumn = true, -- Toggle with `:Gitsigns toggle_signs` numhl = false, -- Toggle with `:Gitsigns toggle_numhl` linehl = false, -- Toggle with `:Gitsigns toggle_linehl` word_diff = false, -- Toggle with `:Gitsigns toggle_word_diff` watch_gitdir = { follow_files = true }, auto_attach = true, attach_to_untracked = false, current_line_blame = false, -- Toggle with `:Gitsigns toggle_current_line_blame` current_line_blame_opts = { virt_text = true, virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align' delay = 1000, ignore_whitespace = false, virt_text_priority = 100, use_focus = true, }, current_line_blame_formatter = ', - ', sign_priority = 6, update_debounce = 100, status_formatter = nil, -- Use default max_file_length = 40000, -- Disable if file is longer than this (in lines) preview_config = { -- Options passed to nvim_open_win style = 'minimal', relative = 'cursor', row = 0, col = 1 }, } < ============================================================================== MAPPINGS *gitsigns-mappings* Custom mappings can be defined in the `on_attach` callback in the config table passed to |gitsigns-setup()|. See |gitsigns-config-on_attach|. Most actions can be repeated with `.` if you have |vim-repeat| installed. ============================================================================== FUNCTIONS *gitsigns-functions* Note functions with the {async} attribute are run asynchronously and accept an optional {callback} argument. statuscolumn({bufnr}, {lnum}) *gitsigns.statuscolumn()* Parameters: ~ {bufnr} (`integer?`): Buffer number. Defaults to current buffer. {lnum} (`integer?`): Line number to return status for. Defaults to `vim.v.lnum` Returns: ~ (`string`) setup({cfg}) *gitsigns.setup()* Setup and start Gitsigns. Parameters: ~ {cfg} (`table?`): Configuration for Gitsigns. See |gitsigns-usage| for more details. refresh({callback}) *gitsigns.refresh()* Refresh all buffers. Attributes: ~ {async} Parameters: ~ {callback} (`(fun(err: string?))?`) get_actions() *gitsigns.get_actions()* Get all the available line specific actions for the current buffer at the cursor position. Returns: ~ (`table?`): Dictionary of action name to function which when called performs action. setloclist({nr}, {target}) *gitsigns.setloclist()* Populate the location list with hunks. Automatically opens the location list window. Alias for: `setqflist({target}, { use_location_list = true, nr = {nr} }` Attributes: ~ {async} Parameters: ~ {nr} (`integer?`): Window number or the |window-ID|. `0` for the current window (default). {target} (`(integer|"attached"|"all")?`): See |gitsigns.setqflist()|. setqflist({target}, {opts}, {callback}) *gitsigns.setqflist()* Populate the quickfix list with hunks. Automatically opens the quickfix window. Attributes: ~ {async} Parameters: ~ {target} (`(integer|"attached"|"all")?`): Specifies which files hunks are collected from. Possible values. • [integer]: The buffer with the matching buffer number. `0` for current buffer (default). • `"attached"`: All attached buffers. • `"all"`: All modified files for each git directory of all attached buffers in addition to the current working directory. {opts} (`Gitsigns.SetqflistOpts?`): Additional options. • {use_location_list}: (`boolean?`) Populate the location list instead of the quickfix list. • {nr}: (`integer?`) Window number or ID when using location list. Defaults to `0`. • {open}: (`boolean?`) Open the quickfix/location list viewer. Defaults to `true`. {callback} (`(fun(err: string?))?`) show_commit({revision}, {open}, {callback}) *gitsigns.show_commit()* Show revision {base} commit in split or tab Parameters: ~ {revision} (`string?`): (default: 'HEAD') {open} (`("vsplit"|"tabnew")?`) {callback} (`(fun(err: string?))?`) show({revision}, {callback}) *gitsigns.show()* Show revision {base} of the current file, if it is given, or with the currently set base (index by default). If {base} is the index, then the opened buffer is editable and any written changes will update the index accordingly. Examples: >lua -- View the index version of the file require('gitsigns').show() -- :Gitsigns show -- View revision of file in the last commit require('gitsigns').show('~1') -- :Gitsigns show ~1 < For a more complete list of ways to specify bases, see |gitsigns-revision|. Attributes: ~ {async} Parameters: ~ {revision} (`string?`) {callback} (`(fun(err: string?))?`) diffthis({base}, {opts}, {callback}) *gitsigns.diffthis()* Perform a |vimdiff| on the given file with {base} if it is given, or with the currently set base (index by default). If {base} is the index, then the opened buffer is editable and any written changes will update the index accordingly. Examples: >lua -- Diff against the index require('gitsigns').diffthis() -- :Gitsigns diffthis -- Diff against the last commit require('gitsigns').diffthis('~1') -- :Gitsigns diffthis ~1 < For a more complete list of ways to specify bases, see |gitsigns-revision|. Attributes: ~ {async} Parameters: ~ {base} (`string?`): Revision to diff against. Defaults to index. {opts} (`Gitsigns.DiffthisOpts?`): Additional options. • {vertical}: (`boolean?`) Split window vertically. Default to `config.diff_opts.vertical`. If running via command line, then shi is taken from the command modifiers. • {split}: (`("aboveleft"|"belowright"|"topleft"|"botright")?`) {callback} (`(fun(err: string?))?`) reset_base({global}) *gitsigns.reset_base()* Reset the base revision to diff against back to the index. Alias for `change_base(nil, {global})` . Parameters: ~ {global} (`any`) change_base({base}, {global}, {callback}) *gitsigns.change_base()* Change the base revision to diff against. If {base} is not given, then the original base is used. If {global} is given and true, then change the base revision of all buffers, including any new buffers. Attributes: ~ {async} Examples: >lua -- Change base to 1 commit behind head require('gitsigns').change_base('HEAD~1') -- :Gitsigns change_base HEAD~1 -- Also works using the Gitsigns command :Gitsigns change_base HEAD~1 -- Other variations require('gitsigns').change_base('~1') -- :Gitsigns change_base ~1 require('gitsigns').change_base('~') -- :Gitsigns change_base ~ require('gitsigns').change_base('^') -- :Gitsigns change_base ^ -- Commits work too require('gitsigns').change_base('92eb3dd') -- :Gitsigns change_base 92eb3dd -- Revert to original base require('gitsigns').change_base() -- :Gitsigns change_base < For a more complete list of ways to specify bases, see |gitsigns-revision|. Parameters: ~ {base} (`string?`): The object/revision to diff against. {global} (`boolean?`): Change the base of all buffers. {callback} (`(fun(err: string?))?`) blame({opts}, {callback}) *gitsigns.blame()* Run git-blame on the current file and open the results in a scroll-bound vertical split. Mappings: is mapped to open a menu with the other mappings Note: must be held to activate the mappings whilst the menu is open. s [Show commit] in a vertical split. S [Show commit] in a new tab. r [Reblame at commit] Attributes: ~ {async} Parameters: ~ {opts} (`Gitsigns.BlameOpts?`): Additional options. • {ignore_whitespace}: (`boolean?`) Ignore whitespace when running blame. • {extra_opts}: (`string[]?`) Extra options passed to `git-blame`. {callback} (`(fun(err: string?))?`) blame_line({opts}, {callback}) *gitsigns.blame_line()* Run git blame on the current line and show the results in a floating window. If already open, calling this will cause the window to get focus. Attributes: ~ {async} Parameters: ~ {opts} (`Gitsigns.LineBlameOpts?`): Additional options. • {full}: (`boolean?`) • {ignore_whitespace}: (`boolean?`) Ignore whitespace when running blame. • {extra_opts}: (`string[]?`) Extra options passed to `git-blame`. {callback} (`(fun(err: string?))?`) get_hunks({bufnr}) *gitsigns.get_hunks()* Get hunk array for specified buffer. Parameters: ~ {bufnr} (`integer`): Buffer number, if not provided (or 0) will use current buffer. Returns: ~ (`table?`): Array of hunk objects. Each hunk object has keys: • `"type"`: String with possible values: "add", "change", "delete" • `"head"`: Header that appears in the unified diff output. • `"lines"`: Line contents of the hunks prefixed with either `"-"` or `"+"`. • `"removed"`: Sub-table with fields: • `"start"`: Line number (1-based) • `"count"`: Line count • `"added"`: Sub-table with fields: • `"start"`: Line number (1-based) • `"count"`: Line count select_hunk({opts}) *gitsigns.select_hunk()* Select the hunk under the cursor. Parameters: ~ {opts} (`Gitsigns.HunkOpts?`): Additional options. • {greedy}: (`boolean?`) Operate on/select all contiguous hunks. Only useful if 'diff_opts' contains `linematch`. Defaults to `true`. preview_hunk_inline({callback}) *gitsigns.preview_hunk_inline()* Preview the hunk at the cursor position inline in the buffer. Parameters: ~ {callback} (`(fun(err: string?))?`) preview_hunk() *gitsigns.preview_hunk()* Preview the hunk at the cursor position in a floating window. If the preview is already open, calling this will cause the window to get focus. prev_hunk({opts}, {callback}) *gitsigns.prev_hunk()* DEPRECATED: use |gitsigns.nav_hunk()| Jump to the previous hunk in the current buffer. If a hunk preview (popup or inline) was previously opened, it will be re-opened at the previous hunk. See |gitsigns.nav_hunk()|. Attributes: ~ {async} Parameters: ~ {opts} (`any`) {callback} (`any`) next_hunk({opts}, {callback}) *gitsigns.next_hunk()* DEPRECATED: use |gitsigns.nav_hunk()| Jump to the next hunk in the current buffer. If a hunk preview (popup or inline) was previously opened, it will be re-opened at the next hunk. See |gitsigns.nav_hunk()|. Attributes: ~ {async} Parameters: ~ {opts} (`any`) {callback} (`any`) nav_hunk({direction}, {opts}, {callback}) *gitsigns.nav_hunk()* Jump to hunk in the current buffer. If a hunk preview (popup or inline) was previously opened, it will be re-opened at the next hunk. Attributes: ~ {async} Parameters: ~ {direction} (`("first"|"last"|"next"|"prev")`) {opts} (`Gitsigns.NavOpts?`): Configuration options. • {wrap}: (`boolean`) Whether to loop around file or not. Defaults to the value 'wrapscan' • {foldopen}: (`boolean`) Expand folds when navigating to a hunk which is inside a fold. Defaults to `true` if 'foldopen' contains `search`. • {navigation_message}: (`boolean`) Whether to show navigation messages or not. Looks at 'shortmess' for default behaviour. • {greedy}: (`boolean`) Only navigate between non-contiguous hunks. Only useful if 'diff_opts' contains `linematch`. Defaults to `true`. • {preview}: (`boolean?`) Automatically open preview_hunk() upon navigating to a hunk. • {count}: (`integer`) Number of times to advance. Defaults to |v:count1|. • {target}: (`("unstaged"|"staged"|"all")`) Which kinds of hunks to target. Defaults to `'unstaged'`. {callback} (`(fun(err: string?))?`) reset_buffer_index({callback}) *gitsigns.reset_buffer_index()* Unstage all hunks for current buffer in the index. Note: Unlike |gitsigns.undo_stage_hunk()| this doesn't simply undo stages, this runs an `git reset` on current buffers file. Attributes: ~ {async} Parameters: ~ {callback} (`(fun(err: string?))?`) stage_buffer({callback}) *gitsigns.stage_buffer()* Stage all hunks in current buffer. Attributes: ~ {async} Parameters: ~ {callback} (`(fun(err: string?))?`) undo_stage_hunk({callback}) *gitsigns.undo_stage_hunk()* DEPRECATED: use |gitsigns.stage_hunk()| on staged signs Undo the last call of stage_hunk(). Note: only the calls to stage_hunk() performed in the current session can be undone. Attributes: ~ {async} Parameters: ~ {callback} (`(fun(err: string?))?`) reset_buffer() *gitsigns.reset_buffer()* Reset the lines of all hunks in the buffer. reset_hunk({range}, {opts}, {callback}) *gitsigns.reset_hunk()* Reset the lines of the hunk at the cursor position, or all lines in the given range. If {range} is provided, all lines in the given range are reset. This supports partial-hunks, meaning if a range only includes a portion of a particular hunk, only the lines within the range will be reset. Parameters: ~ {range} (`(integer,integer)?`): List-like table of two integers making up the line range from which you want to reset the hunks. If running via command line, then this is taken from the command modifiers. {opts} (`Gitsigns.HunkOpts?`): Additional options. • {greedy}: (`boolean?`) Operate on/select all contiguous hunks. Only useful if 'diff_opts' contains `linematch`. Defaults to `true`. {callback} (`(fun(err: string?))?`) stage_hunk({range}, {opts}, {callback}) *gitsigns.stage_hunk()* Stage the hunk at the cursor position, or all lines in the given range. If {range} is provided, all lines in the given range are staged. This supports partial-hunks, meaning if a range only includes a portion of a particular hunk, only the lines within the range will be staged. Attributes: ~ {async} Parameters: ~ {range} (`(integer,integer)?`): List-like table of two integers making up the line range from which you want to stage the hunks. If running via command line, then this is taken from the command modifiers. {opts} (`Gitsigns.HunkOpts?`): Additional options. • {greedy}: (`boolean?`) Operate on/select all contiguous hunks. Only useful if 'diff_opts' contains `linematch`. Defaults to `true`. {callback} (`(fun(err: string?))?`) toggle_deleted({value}) *gitsigns.toggle_deleted()* DEPRECATED: Use |gitsigns.preview_hunk_inline()| Toggle |gitsigns-config-show_deleted| Parameters: ~ {value} (`boolean?`): Value to set toggle. If `nil` the toggle value is inverted. Returns: ~ (`boolean`): Current value of |gitsigns-config-show_deleted| toggle_current_line_blame({value}) *gitsigns.toggle_current_line_blame()* Toggle |gitsigns-config-current_line_blame| Parameters: ~ {value} (`boolean?`): Value to set toggle. If `nil` the toggle value is inverted. Returns: ~ (`boolean`): Current value of |gitsigns-config-current_line_blame| toggle_word_diff({value}) *gitsigns.toggle_word_diff()* Toggle |gitsigns-config-word_diff| Parameters: ~ {value} (`boolean?`): Value to set toggle. If `nil` the toggle value is inverted. Returns: ~ (`boolean`): Current value of |gitsigns-config-word_diff| toggle_linehl({value}) *gitsigns.toggle_linehl()* Toggle |gitsigns-config-linehl| Parameters: ~ {value} (`boolean?`): Value to set toggle. If `nil` the toggle value is inverted. Returns: ~ (`boolean`): Current value of |gitsigns-config-linehl| toggle_numhl({value}) *gitsigns.toggle_numhl()* Toggle |gitsigns-config-numhl| Parameters: ~ {value} (`boolean?`): Value to set toggle. If `nil` the toggle value is inverted. Returns: ~ (`boolean`): Current value of |gitsigns-config-numhl| toggle_signs({value}) *gitsigns.toggle_signs()* Toggle |gitsigns-config-signbooleancolumn| Parameters: ~ {value} (`boolean?`): Value to set toggle. If `nil` the toggle value is inverted. Returns: ~ (`boolean`): Current value of |gitsigns-config-signcolumn| attach({bufnr}, {ctx}, {trigger}, {callback}) *gitsigns.attach()* Attach Gitsigns to the buffer. Attributes: ~ {async} Parameters: ~ {bufnr} (`integer`): Buffer number {ctx} (`Gitsigns.GitContext?`): • {file}: (`string`) Path to the file represented by the buffer. • {toplevel}: (`string?`) Path to the top-level of the parent git repository. • {gitdir}: (`string?`) Path to the git directory of the parent git repository. • {base}: (`string?`) Git revision to compare against. {trigger} (`string?`) {callback} (`(fun(err: string?))?`) detach({bufnr}) *gitsigns.detach()* Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not provided then the current buffer is used. Parameters: ~ {bufnr} (`integer`): Buffer number detach_all() *gitsigns.detach_all()* Detach Gitsigns from all buffers it is attached to. ============================================================================== CONFIGURATION *gitsigns-config* This section describes the configuration fields which can be passed to |gitsigns.setup()|. Note fields of type `table` may be marked with extended meaning the field is merged with the default, with the user value given higher precedence. This allows only specific sub-fields to be configured without having to redefine the whole field. signs *gitsigns-config-signs* Type: `table[extended]` Default: > { add = { text = '┃' }, change = { text = '┃' }, delete = { text = '▁' }, topdelete = { text = '▔' }, changedelete = { text = '~' }, untracked = { text = '┆' }, } < Configuration for signs: • `text` specifies the character to use for the sign. • `show_count` to enable showing count of hunk, e.g. number of deleted lines. The highlights `GitSigns[kind][type]` is used for each kind of sign. E.g. 'add' signs uses the highlights: • `GitSignsAdd` (for normal text signs) • `GitSignsAddNr` (for signs when `config.numhl == true`) • `GitSignsAddLn `(for signs when `config.linehl == true`) • `GitSignsAddCul `(for signs when `config.culhl == true`) See |gitsigns-highlight-groups|. signs_staged *gitsigns-config-signs_staged* Type: `table[extended]` Default: > { add = { text = '┃' }, change = { text = '┃' }, delete = { text = '▁' }, topdelete = { text = '▔' }, changedelete = { text = '~' }, } < Configuration for signs of staged hunks. See |gitsigns-config-signs|. signs_staged_enable *gitsigns-config-signs_staged_enable* Type: `boolean`, Default: `true` Show signs for staged hunks. When enabled the signs defined in |git-config-signs_staged| are used. worktrees *gitsigns-config-worktrees* Type: `table`, Default: `{}` Detached working trees. Array of tables with the keys `gitdir` and `toplevel`. If normal attaching fails, then each entry in the table is attempted with the work tree details set. Example: >lua worktrees = { { toplevel = vim.env.HOME, gitdir = vim.env.HOME .. '/projects/dotfiles/.git' } } on_attach *gitsigns-config-on_attach* Type: `function`, Default: `nil` Callback called when attaching to a buffer. Mainly used to setup keymaps. The buffer number is passed as the first argument. This callback can return `false` to prevent attaching to the buffer. Example: >lua on_attach = function(bufnr) if vim.api.nvim_buf_get_name(bufnr):match() then -- Don't attach to specific buffers whose name matches a pattern return false end -- Setup keymaps vim.api.nvim_buf_set_keymap(bufnr, 'n', 'hs', 'lua require"gitsigns".stage_hunk()', {}) ... -- More keymaps end < watch_gitdir *gitsigns-config-watch_gitdir* Type: `table[extended]` Default: > `{ enable = true, follow_files = true }` < When opening a file, a libuv watcher is placed on the respective `.git` directory to detect when changes happen to use as a trigger to update signs. Fields: ~ • `enable`: Whether the watcher is enabled. • `follow_files`: If a file is moved with `git mv`, switch the buffer to the new location. sign_priority *gitsigns-config-sign_priority* Type: `number`, Default: `6` Priority to use for signs. signcolumn *gitsigns-config-signcolumn* Type: `boolean`, Default: `true` Enable/disable symbols in the sign column. When enabled the highlights defined in `signs.*.hl` and symbols defined in `signs.*.text` are used. numhl *gitsigns-config-numhl* Type: `boolean`, Default: `false` Enable/disable line number highlights. When enabled the highlights defined in `signs.*.numhl` are used. If the highlight group does not exist, then it is automatically defined and linked to the corresponding highlight group in `signs.*.hl`. linehl *gitsigns-config-linehl* Type: `boolean`, Default: `false` Enable/disable line highlights. When enabled the highlights defined in `signs.*.linehl` are used. If the highlight group does not exist, then it is automatically defined and linked to the corresponding highlight group in `signs.*.hl`. culhl *gitsigns-config-culhl* Type: `boolean`, Default: `false` Enable/disable highlights for the sign column when the cursor is on the same line. When enabled the highlights defined in `signs.*.culhl` are used. If the highlight group does not exist, then it is automatically defined and linked to the corresponding highlight group in `signs.*.hl`. show_deleted *gitsigns-config-show_deleted* DEPRECATED Type: `boolean`, Default: `false` Show the old version of hunks inline in the buffer (via virtual lines). Note: Virtual lines currently use the highlight `GitSignsDeleteVirtLn`. diff_opts *gitsigns-config-diff_opts* Type: `table[extended]`, Default: derived from 'diffopt' Diff options. If the default value is used, then changes to 'diffopt' are automatically applied. Fields: ~ • algorithm: string Diff algorithm to use. Values: • "myers" the default algorithm • "minimal" spend extra time to generate the smallest possible diff • "patience" patience diff algorithm • "histogram" histogram diff algorithm • internal: boolean Use Neovim's built in xdiff library for running diffs. • indent_heuristic: boolean Use the indent heuristic for the internal diff library. • vertical: boolean Start diff mode with vertical splits. • linematch: integer Enable second-stage diff on hunks to align lines. Requires `internal=true`. • ignore_blank_lines: boolean Ignore changes where lines are blank. • ignore_whitespace_change: boolean Ignore changes in amount of white space. It should ignore adding trailing white space, but not leading white space. • ignore_whitespace: boolean Ignore all white space changes. • ignore_whitespace_change_at_eol: boolean Ignore white space changes at end of line. diffthis *gitsigns-config-diffthis* Type: `table` Default: > `{ split = "aboveleft" }` < Options for the `:Gitsigns diffthis` command. base *gitsigns-config-base* Type: `string`, Default: index The object/revision to diff against. See |gitsigns-revision|. count_chars *gitsigns-config-count_chars* Type: `table` Default: > `{ "1", "2", "3", "4", "5", "6", "7", "8", "9", ["+"] = ">" }` < The count characters used when `signs.*.show_count` is enabled. The `+` entry is used as a fallback. With the default, any count outside of 1-9 uses the `>` character in the sign. Possible use cases for this field: • to specify unicode characters for the counts instead of 1-9. • to define characters to be used for counts greater than 9. status_formatter *gitsigns-config-status_formatter* Type: `function` Default: > function(status) local added, changed, removed = status.added, status.changed, status.removed local status_txt = {} if added and added > 0 then table.insert(status_txt, '+'..added ) end if changed and changed > 0 then table.insert(status_txt, '~'..changed) end if removed and removed > 0 then table.insert(status_txt, '-'..removed) end return table.concat(status_txt, ' ') end < Function used to format `b:gitsigns_status`. max_file_length *gitsigns-config-max_file_length* Type: `number`, Default: `40000` Max file length (in lines) to attach to. preview_config *gitsigns-config-preview_config* Type: `table[extended]` Default: > `{ col = 1, relative = "cursor", row = 0, style = "minimal" }` < Option overrides for the Gitsigns preview window. Table is passed directly to `nvim_open_win`. auto_attach *gitsigns-config-auto_attach* Type: `boolean`, Default: `true` Automatically attach to files. attach_to_untracked *gitsigns-config-attach_to_untracked* Type: `boolean`, Default: `false` Attach to untracked files. update_debounce *gitsigns-config-update_debounce* Type: `number`, Default: `100` Debounce time for updates (in milliseconds). current_line_blame *gitsigns-config-current_line_blame* Type: `boolean`, Default: `false` Adds an unobtrusive and customisable blame annotation at the end of the current line. The highlight group used for the text is `GitSignsCurrentLineBlame`. current_line_blame_opts *gitsigns-config-current_line_blame_opts* Type: `table[extended]` Default: > `{ delay = 1000, use_focus = true, virt_text = true, virt_text_pos = "eol", virt_text_priority = 100 }` < Options for the current line blame annotation. Fields: ~ • virt_text: boolean Whether to show a virtual text blame annotation. • virt_text_pos: string Blame annotation position. Available values: `eol` Right after eol character. `overlay` Display over the specified column, without shifting the underlying text. `right_align` Display right aligned in the window. • delay: integer Sets the delay (in milliseconds) before blame virtual text is displayed. • ignore_whitespace: boolean Ignore whitespace when running blame. • virt_text_priority: integer Priority of virtual text. • use_focus: boolean Enable only when buffer is in focus • extra_opts: string[] Extra options passed to `git-blame`. current_line_blame_formatter *gitsigns-config-current_line_blame_formatter* Type: `string|function`, Default: `" , - "` String or function used to format the virtual text of |gitsigns-config-current_line_blame|. When a string, accepts the following format specifiers: • `` • `` • `` • `` • `` • `` or `` • `` • `` • `` • `` or `` • `` • `` • `` • `` For `` and ``, `FORMAT` can be any valid date format that is accepted by `os.date()` with the addition of `%R` (defaults to `%Y-%m-%d`): • `%a` abbreviated weekday name (e.g., Wed) • `%A` full weekday name (e.g., Wednesday) • `%b` abbreviated month name (e.g., Sep) • `%B` full month name (e.g., September) • `%c` date and time (e.g., 09/16/98 23:48:10) • `%d` day of the month (16) [01-31] • `%H` hour, using a 24-hour clock (23) [00-23] • `%I` hour, using a 12-hour clock (11) [01-12] • `%M` minute (48) [00-59] • `%m` month (09) [01-12] • `%p` either "am" or "pm" (pm) • `%S` second (10) [00-61] • `%w` weekday (3) [0-6 = Sunday-Saturday] • `%x` date (e.g., 09/16/98) • `%X` time (e.g., 23:48:10) • `%Y` full year (1998) • `%y` two-digit year (98) [00-99] • `%%` the character `%` • `%R` relative (e.g., 4 months ago) When a function: Parameters: ~ {name} Git user name returned from `git config user.name` . {blame_info} Table with the following keys: • `abbrev_sha`: string • `orig_lnum`: integer • `final_lnum`: integer • `author`: string • `author_mail`: string • `author_time`: integer • `author_tz`: string • `committer`: string • `committer_mail`: string • `committer_time`: integer • `committer_tz`: string • `summary`: string • `previous`: string • `filename`: string • `boundary`: true? Note that the keys map onto the output of: `git blame --line-porcelain` Return: ~ The result of this function is passed directly to the `opts.virt_text` field of |nvim_buf_set_extmark| and thus must be a list of [text, highlight] tuples. current_line_blame_formatter_nc *gitsigns-config-current_line_blame_formatter_nc* Type: `string|function`, Default: `" "` String or function used to format the virtual text of |gitsigns-config-current_line_blame| for lines that aren't committed. See |gitsigns-config-current_line_blame_formatter| for more information. trouble *gitsigns-config-trouble* Type: `boolean`, Default: true if installed When using setqflist() or setloclist(), open Trouble instead of the quickfix/location list window. gh *gitsigns-config-gh* Type: `boolean`, Default: `false` Enable GitHub integration. This allows the following features: • `:Gitsigns blame_line` will show PR numbers (with a hyperlink) word_diff *gitsigns-config-word_diff* Type: `boolean`, Default: `false` Highlight intra-line word differences in the buffer. Requires `config.diff_opts.internal = true` . Uses the highlights: • For word diff in previews: • `GitSignsAddInline` • `GitSignsChangeInline` • `GitSignsDeleteInline` • For word diff in buffer: • `GitSignsAddLnInline` • `GitSignsChangeLnInline` • `GitSignsDeleteLnInline` • For word diff in virtual lines (e.g. show_deleted): • `GitSignsAddVirtLnInline` • `GitSignsChangeVirtLnInline` • `GitSignsDeleteVirtLnInline` debug_mode *gitsigns-config-debug_mode* Type: `boolean`, Default: `false` Enables debug logging and makes the following functions available: `dump_cache`, `debug_messages`, `clear_debug`. ============================================================================== HIGHLIGHT GROUPS *gitsigns-highlight-groups* These are the highlights groups used by Gitsigns. Note if a highlight is not defined, it will be automatically derived by searching for other defined highlights in order. *hl-GitSignsAdd* GitSignsAdd Used for the text of 'add' signs. Fallbacks: `GitGutterAdd`, `SignifySignAdd`, `DiffAddedGutter`, `Added`, `DiffAdd` *hl-GitSignsChange* GitSignsChange Used for the text of 'change' signs. Fallbacks: `GitGutterChange`, `SignifySignChange`, `DiffModifiedGutter`, `Changed`, `DiffChange` *hl-GitSignsDelete* GitSignsDelete Used for the text of 'delete' signs. Fallbacks: `GitGutterDelete`, `SignifySignDelete`, `DiffRemovedGutter`, `Removed`, `DiffDelete` *hl-GitSignsChangedelete* GitSignsChangedelete Used for the text of 'changedelete' signs. Fallbacks: `GitSignsChange` *hl-GitSignsTopdelete* GitSignsTopdelete Used for the text of 'topdelete' signs. Fallbacks: `GitSignsDelete` *hl-GitSignsUntracked* GitSignsUntracked Used for the text of 'untracked' signs. Fallbacks: `GitSignsAdd` *hl-GitSignsAddNr* GitSignsAddNr Used for number column (when `config.numhl == true`) of 'add' signs. Fallbacks: `GitGutterAddLineNr`, `GitSignsAdd` *hl-GitSignsChangeNr* GitSignsChangeNr Used for number column (when `config.numhl == true`) of 'change' signs. Fallbacks: `GitGutterChangeLineNr`, `GitSignsChange` *hl-GitSignsDeleteNr* GitSignsDeleteNr Used for number column (when `config.numhl == true`) of 'delete' signs. Fallbacks: `GitGutterDeleteLineNr`, `GitSignsDelete` *hl-GitSignsChangedeleteNr* GitSignsChangedeleteNr Used for number column (when `config.numhl == true`) of 'changedelete' signs. Fallbacks: `GitSignsChangeNr` *hl-GitSignsTopdeleteNr* GitSignsTopdeleteNr Used for number column (when `config.numhl == true`) of 'topdelete' signs. Fallbacks: `GitSignsDeleteNr` *hl-GitSignsUntrackedNr* GitSignsUntrackedNr Used for number column (when `config.numhl == true`) of 'untracked' signs. Fallbacks: `GitSignsAddNr` *hl-GitSignsAddLn* GitSignsAddLn Used for buffer line (when `config.linehl == true`) of 'add' signs. Fallbacks: `GitGutterAddLine`, `SignifyLineAdd`, `DiffAdd` *hl-GitSignsChangeLn* GitSignsChangeLn Used for buffer line (when `config.linehl == true`) of 'change' signs. Fallbacks: `GitGutterChangeLine`, `SignifyLineChange`, `DiffChange` *hl-GitSignsChangedeleteLn* GitSignsChangedeleteLn Used for buffer line (when `config.linehl == true`) of 'changedelete' signs. Fallbacks: `GitSignsChangeLn` *hl-GitSignsTopdeleteLn* GitSignsTopdeleteLn Used for buffer line (when `config.linehl == true`) of 'topdelete' signs. Fallbacks: `GitSignsDeleteLn` *hl-GitSignsUntrackedLn* GitSignsUntrackedLn Used for buffer line (when `config.linehl == true`) of 'untracked' signs. Fallbacks: `GitSignsAddLn` *hl-GitSignsAddCul* GitSignsAddCul Used for the text (when the cursor is on the same line as the sign) of 'add' signs. Fallbacks: `GitSignsAdd` *hl-GitSignsChangeCul* GitSignsChangeCul Used for the text (when the cursor is on the same line as the sign) of 'change' signs. Fallbacks: `GitSignsChange` *hl-GitSignsDeleteCul* GitSignsDeleteCul Used for the text (when the cursor is on the same line as the sign) of 'delete' signs. Fallbacks: `GitSignsDelete` *hl-GitSignsChangedeleteCul* GitSignsChangedeleteCul Used for the text (when the cursor is on the same line as the sign) of 'changedelete' signs. Fallbacks: `GitSignsChangeCul` *hl-GitSignsTopdeleteCul* GitSignsTopdeleteCul Used for the text (when the cursor is on the same line as the sign) of 'topdelete' signs. Fallbacks: `GitSignsDeleteCul` *hl-GitSignsUntrackedCul* GitSignsUntrackedCul Used for the text (when the cursor is on the same line as the sign) of 'untracked' signs. Fallbacks: `GitSignsAddCul` *hl-GitSignsStagedAdd* GitSignsStagedAdd Used for the text of 'add' staged signs. Fallbacks: `GitSignsAdd` (fg=50%) *hl-GitSignsStagedChange* GitSignsStagedChange Used for the text of 'change' staged signs. Fallbacks: `GitSignsChange` (fg=50%) *hl-GitSignsStagedDelete* GitSignsStagedDelete Used for the text of 'delete' staged signs. Fallbacks: `GitSignsDelete` (fg=50%) *hl-GitSignsStagedChangedelete* GitSignsStagedChangedelete Used for the text of 'changedelete' staged signs. Fallbacks: `GitSignsChangedelete` (fg=50%) *hl-GitSignsStagedTopdelete* GitSignsStagedTopdelete Used for the text of 'topdelete' staged signs. Fallbacks: `GitSignsTopdelete` (fg=50%) *hl-GitSignsStagedUntracked* GitSignsStagedUntracked Used for the text of 'untracked' staged signs. Fallbacks: `GitSignsUntracked` (fg=50%) *hl-GitSignsStagedAddNr* GitSignsStagedAddNr Used for number column (when `config.numhl == true`) of 'add' staged signs. Fallbacks: `GitSignsAddNr` (fg=50%) *hl-GitSignsStagedChangeNr* GitSignsStagedChangeNr Used for number column (when `config.numhl == true`) of 'change' staged signs. Fallbacks: `GitSignsChangeNr` (fg=50%) *hl-GitSignsStagedDeleteNr* GitSignsStagedDeleteNr Used for number column (when `config.numhl == true`) of 'delete' staged signs. Fallbacks: `GitSignsDeleteNr` (fg=50%) *hl-GitSignsStagedChangedeleteNr* GitSignsStagedChangedeleteNr Used for number column (when `config.numhl == true`) of 'changedelete' staged signs. Fallbacks: `GitSignsChangedeleteNr` (fg=50%) *hl-GitSignsStagedTopdeleteNr* GitSignsStagedTopdeleteNr Used for number column (when `config.numhl == true`) of 'topdelete' staged signs. Fallbacks: `GitSignsTopdeleteNr` (fg=50%) *hl-GitSignsStagedUntrackedNr* GitSignsStagedUntrackedNr Used for number column (when `config.numhl == true`) of 'untracked' staged signs. Fallbacks: `GitSignsUntrackedNr` (fg=50%) *hl-GitSignsStagedAddLn* GitSignsStagedAddLn Used for buffer line (when `config.linehl == true`) of 'add' staged signs. Fallbacks: `GitSignsAddLn` (fg=50%) *hl-GitSignsStagedChangeLn* GitSignsStagedChangeLn Used for buffer line (when `config.linehl == true`) of 'change' staged signs. Fallbacks: `GitSignsChangeLn` (fg=50%) *hl-GitSignsStagedChangedeleteLn* GitSignsStagedChangedeleteLn Used for buffer line (when `config.linehl == true`) of 'changedelete' staged signs. Fallbacks: `GitSignsChangedeleteLn` (fg=50%) *hl-GitSignsStagedTopdeleteLn* GitSignsStagedTopdeleteLn Used for buffer line (when `config.linehl == true`) of 'topdelete' staged signs. Fallbacks: `GitSignsTopdeleteLn` (fg=50%) *hl-GitSignsStagedUntrackedLn* GitSignsStagedUntrackedLn Used for buffer line (when `config.linehl == true`) of 'untracked' staged signs. Fallbacks: `GitSignsUntrackedLn` (fg=50%) *hl-GitSignsStagedAddCul* GitSignsStagedAddCul Used for the text (when the cursor is on the same line as the sign) of 'add' staged signs. Fallbacks: `GitSignsAddCul` (fg=50%) *hl-GitSignsStagedChangeCul* GitSignsStagedChangeCul Used for the text (when the cursor is on the same line as the sign) of 'change' staged signs. Fallbacks: `GitSignsChangeCul` (fg=50%) *hl-GitSignsStagedDeleteCul* GitSignsStagedDeleteCul Used for the text (when the cursor is on the same line as the sign) of 'delete' staged signs. Fallbacks: `GitSignsDeleteCul` (fg=50%) *hl-GitSignsStagedChangedeleteCul* GitSignsStagedChangedeleteCul Used for the text (when the cursor is on the same line as the sign) of 'changedelete' staged signs. Fallbacks: `GitSignsChangedeleteCul` (fg=50%) *hl-GitSignsStagedTopdeleteCul* GitSignsStagedTopdeleteCul Used for the text (when the cursor is on the same line as the sign) of 'topdelete' staged signs. Fallbacks: `GitSignsTopdeleteCul` (fg=50%) *hl-GitSignsStagedUntrackedCul* GitSignsStagedUntrackedCul Used for the text (when the cursor is on the same line as the sign) of 'untracked' staged signs. Fallbacks: `GitSignsUntrackedCul` (fg=50%) *hl-GitSignsAddPreview* GitSignsAddPreview Used for added lines in previews. Fallbacks: `GitGutterAddLine`, `SignifyLineAdd`, `DiffAdd` *hl-GitSignsDeletePreview* GitSignsDeletePreview Used for deleted lines in previews. Fallbacks: `GitGutterDeleteLine`, `SignifyLineDelete`, `DiffDelete` *hl-GitSignsNoEOLPreview* GitSignsNoEOLPreview Used for "No newline at end of file". Fallbacks: `DiffNoEOL`, `Constant` *hl-GitSignsCurrentLineBlame* GitSignsCurrentLineBlame Used for current line blame. Fallbacks: `NonText` *hl-GitSignsAddInline* GitSignsAddInline Used for added word diff regions in inline previews. Fallbacks: `TermCursor` *hl-GitSignsDeleteInline* GitSignsDeleteInline Used for deleted word diff regions in inline previews. Fallbacks: `TermCursor` *hl-GitSignsChangeInline* GitSignsChangeInline Used for changed word diff regions in inline previews. Fallbacks: `TermCursor` *hl-GitSignsAddLnInline* GitSignsAddLnInline Used for added word diff regions when `config.word_diff == true`. Fallbacks: `GitSignsAddInline` *hl-GitSignsChangeLnInline* GitSignsChangeLnInline Used for changed word diff regions when `config.word_diff == true`. Fallbacks: `GitSignsChangeInline` *hl-GitSignsDeleteLnInline* GitSignsDeleteLnInline Used for deleted word diff regions when `config.word_diff == true`. Fallbacks: `GitSignsDeleteInline` *hl-GitSignsDeleteVirtLn* GitSignsDeleteVirtLn Used for deleted lines shown by inline `preview_hunk_inline()` or `show_deleted()`. Fallbacks: `GitGutterDeleteLine`, `SignifyLineDelete`, `DiffDelete` *hl-GitSignsDeleteVirtLnInLine* GitSignsDeleteVirtLnInLine Used for word diff regions in lines shown by inline `preview_hunk_inline()` or `show_deleted()`. Fallbacks: `GitSignsDeleteLnInline` *hl-GitSignsVirtLnum* GitSignsVirtLnum Used for line numbers in inline hunks previews. Fallbacks: `GitSignsDeleteVirtLn` ============================================================================== COMMAND *gitsigns-command* *:Gitsigns* :Gitsigns {subcmd} {args} Run a Gitsigns command. {subcmd} can be any function documented in |gitsigns-functions|. Each argument in {args} will be attempted to be parsed as a Lua value using `loadstring`, however if this fails the argument will remain as the string given by ||. Note this command is equivalent to: >vim :lua require('gitsigns').{subcmd}({args}) < Examples: >vim :Gitsigns diffthis HEAD~1 :Gitsigns blame_line :Gitsigns toggle_signs :Gitsigns toggle_current_line_blame :Gitsigns change_base ~ :Gitsigns reset_buffer :Gitsigns change_base nil true < ============================================================================== SPECIFYING OBJECTS *gitsigns-object* *gitsigns-revision* Gitsigns objects are Git revisions as defined in the "SPECIFYING REVISIONS" section in the gitrevisions(7) man page. For commands that accept an optional base, the default is the file in the index. Examples: Additionally, Gitsigns also accepts the value `FILE` to specify the working version of a file. Object Meaning ~ @ Version of file in the commit referenced by @ aka HEAD main Version of file in the commit referenced by main main^ Version of file in the parent of the commit referenced by main main~ " main~1 " main...other Version of file in the merge base of main and other @^ Version of file in the parent of HEAD @~2 Version of file in the grandparent of HEAD 92eb3dd Version of file in the commit 92eb3dd :1 The file's common ancestor during a conflict :2 The alternate file in the target branch during a conflict ============================================================================== STATUSLINE *gitsigns-statusline* *b:gitsigns_status* *b:gitsigns_status_dict* The buffer variables `b:gitsigns_status` and `b:gitsigns_status_dict` are provided. `b:gitsigns_status` is formatted using `config.status_formatter` . `b:gitsigns_status_dict` is a dictionary with the keys: • `added` - Number of added lines. • `changed` - Number of changed lines. • `removed` - Number of removed lines. • `head` - Name of current HEAD (branch or short commit hash). • `root` - Top level directory of the working tree. • `gitdir` - .git directory. Example: >vim set statusline+=%{get(b:,'gitsigns_status','')} < *b:gitsigns_head* *g:gitsigns_head* Use `g:gitsigns_head` and `b:gitsigns_head` to return the name of the current HEAD (usually branch name). If the current HEAD is detached then this will be a short commit hash. `g:gitsigns_head` returns the current HEAD for the current working directory, whereas `b:gitsigns_head` returns the current HEAD for each buffer. *b:gitsigns_blame_line* *b:gitsigns_blame_line_dict* Provided if |gitsigns-config-current_line_blame| is enabled. `b:gitsigns_blame_line` if formatted using `config.current_line_blame_formatter`. `b:gitsigns_blame_line_dict` is a dictionary containing of the blame object for the current line. For complete list of keys, see the {blame_info} argument from |gitsigns-config-current_line_blame_formatter|. ============================================================================== TEXT OBJECTS *gitsigns-textobject* Since text objects are defined via keymaps, these are exposed and configurable via the config, see |gitsigns-config-keymaps|. The lua implementation is exposed through |gitsigns.select_hunk()|. ============================================================================== EVENTS *gitsigns-events* |User| |autocommands| provided to allow extending behaviors. Example: >lua vim.api.nvim_create_autocmd('User', { pattern = 'GitSignsUpdate', callback = function(args) print(os.time(), ' Gitsigns made an update on ', args.data.buffer) end }) < *User_GitSignsUpdate* GitSignsUpdate After Gitsigns updates its knowledge about hunks. Provides `bufnr` in the autocmd user data. *User_GitSignsChanged* GitSignsChanged After any event in which Gitsigns can potentially change the repository. Provides `file` in the autocmd user data. ------------------------------------------------------------------------------ vim:tw=78:ts=8:ft=help:norl: neovim-gitsigns-2.0.0/etc/000077500000000000000000000000001513053142700154075ustar00rootroot00000000000000neovim-gitsigns-2.0.0/etc/doc_template.txt000066400000000000000000000201031513053142700206040ustar00rootroot00000000000000*gitsigns.txt* Gitsigns *gitsigns.nvim* Author: Lewis Russell Version: {{VERSION}} Homepage: License: MIT license ============================================================================== INTRODUCTION *gitsigns* Gitsigns is a plugin for Neovim that provides integration with Git via a feature set which includes (but not limited to): • Provides signs in the |signcolumn| to show changed/added/removed lines. • Mappings to operate on hunks to stage, undo or reset against Git's index. Gitsigns is implemented entirely in Lua which is built into Neovim and requires no external dependencies other than git. This is unlike other plugins that require python, node, etc, which need to communicate with Neovim using |RPC|. By default, Gitsigns also uses Neovim's built-in diff library (`vim.diff`) unlike other similar plugins that need to run `git-diff` as an external process which is less efficient, has tighter bottlenecks and requires file IO. ============================================================================== USAGE *gitsigns-usage* No setup required. >lua Optional configuration can be passed to the setup function. Here is an example with most of the default settings: >lua {{SETUP}} < ============================================================================== MAPPINGS *gitsigns-mappings* Custom mappings can be defined in the `on_attach` callback in the config table passed to |gitsigns-setup()|. See |gitsigns-config-on_attach|. Most actions can be repeated with `.` if you have |vim-repeat| installed. ============================================================================== FUNCTIONS *gitsigns-functions* Note functions with the {async} attribute are run asynchronously and accept an optional {callback} argument. {{FUNCTIONS}} ============================================================================== CONFIGURATION *gitsigns-config* This section describes the configuration fields which can be passed to |gitsigns.setup()|. Note fields of type `table` may be marked with extended meaning the field is merged with the default, with the user value given higher precedence. This allows only specific sub-fields to be configured without having to redefine the whole field. {{CONFIG}} ============================================================================== HIGHLIGHT GROUPS *gitsigns-highlight-groups* These are the highlights groups used by Gitsigns. Note if a highlight is not defined, it will be automatically derived by searching for other defined highlights in order. {{HIGHLIGHTS}} ============================================================================== COMMAND *gitsigns-command* *:Gitsigns* :Gitsigns {subcmd} {args} Run a Gitsigns command. {subcmd} can be any function documented in |gitsigns-functions|. Each argument in {args} will be attempted to be parsed as a Lua value using `loadstring`, however if this fails the argument will remain as the string given by ||. Note this command is equivalent to: >vim :lua require('gitsigns').{subcmd}({args}) < Examples: >vim :Gitsigns diffthis HEAD~1 :Gitsigns blame_line :Gitsigns toggle_signs :Gitsigns toggle_current_line_blame :Gitsigns change_base ~ :Gitsigns reset_buffer :Gitsigns change_base nil true < ============================================================================== SPECIFYING OBJECTS *gitsigns-object* *gitsigns-revision* Gitsigns objects are Git revisions as defined in the "SPECIFYING REVISIONS" section in the gitrevisions(7) man page. For commands that accept an optional base, the default is the file in the index. Examples: Additionally, Gitsigns also accepts the value `FILE` to specify the working version of a file. Object Meaning ~ @ Version of file in the commit referenced by @ aka HEAD main Version of file in the commit referenced by main main^ Version of file in the parent of the commit referenced by main main~ " main~1 " main...other Version of file in the merge base of main and other @^ Version of file in the parent of HEAD @~2 Version of file in the grandparent of HEAD 92eb3dd Version of file in the commit 92eb3dd :1 The file's common ancestor during a conflict :2 The alternate file in the target branch during a conflict ============================================================================== STATUSLINE *gitsigns-statusline* *b:gitsigns_status* *b:gitsigns_status_dict* The buffer variables `b:gitsigns_status` and `b:gitsigns_status_dict` are provided. `b:gitsigns_status` is formatted using `config.status_formatter` . `b:gitsigns_status_dict` is a dictionary with the keys: • `added` - Number of added lines. • `changed` - Number of changed lines. • `removed` - Number of removed lines. • `head` - Name of current HEAD (branch or short commit hash). • `root` - Top level directory of the working tree. • `gitdir` - .git directory. Example: >vim set statusline+=%{get(b:,'gitsigns_status','')} < *b:gitsigns_head* *g:gitsigns_head* Use `g:gitsigns_head` and `b:gitsigns_head` to return the name of the current HEAD (usually branch name). If the current HEAD is detached then this will be a short commit hash. `g:gitsigns_head` returns the current HEAD for the current working directory, whereas `b:gitsigns_head` returns the current HEAD for each buffer. *b:gitsigns_blame_line* *b:gitsigns_blame_line_dict* Provided if |gitsigns-config-current_line_blame| is enabled. `b:gitsigns_blame_line` if formatted using `config.current_line_blame_formatter`. `b:gitsigns_blame_line_dict` is a dictionary containing of the blame object for the current line. For complete list of keys, see the {blame_info} argument from |gitsigns-config-current_line_blame_formatter|. ============================================================================== TEXT OBJECTS *gitsigns-textobject* Since text objects are defined via keymaps, these are exposed and configurable via the config, see |gitsigns-config-keymaps|. The lua implementation is exposed through |gitsigns.select_hunk()|. ============================================================================== EVENTS *gitsigns-events* |User| |autocommands| provided to allow extending behaviors. Example: >lua vim.api.nvim_create_autocmd('User', { pattern = 'GitSignsUpdate', callback = function(args) print(os.time(), ' Gitsigns made an update on ', args.data.buffer) end }) < *User_GitSignsUpdate* GitSignsUpdate After Gitsigns updates its knowledge about hunks. Provides `bufnr` in the autocmd user data. *User_GitSignsChanged* GitSignsChanged After any event in which Gitsigns can potentially change the repository. Provides `file` in the autocmd user data. ------------------------------------------------------------------------------ vim:tw=78:ts=8:ft=help:norl: neovim-gitsigns-2.0.0/gen_help.lua000077500000000000000000000545101513053142700171300ustar00rootroot00000000000000#!/usr/bin/env -S nvim -l -- Simple script to update the help doc by reading the config schema. local inspect = vim.inspect local list_extend = vim.list_extend local startswith = vim.startswith local config = require('lua.gitsigns.config') local INDENT = 4 local INDENT_STR = string.rep(' ', INDENT) -- To make sure the output is consistent between runs (to minimise diffs), we -- need to iterate through the schema keys in a deterministic way. To do this we -- do a smple scan over the file the schema is defined in and collect the keys -- in the order they are defined. --- @return string[] local function get_ordered_schema_keys() local ci = io.lines('lua/gitsigns/config.lua') --- @type Iterator[string] for l in ci do if startswith(l, 'M.schema = {') then break end end local keys = {} for l in ci do if startswith(l, '}') then break end if l:find('^ (%w+).*') then local lc = l:gsub('^%s*([%w_]+).*', '%1') table.insert(keys, lc) end end return keys end --- @alias EmmyDocLoc { file: string, line: integer } --- @alias EmmyDocParam { name: string, typ: string, desc: string? } --- @alias EmmyDocReturn { name: string?, typ: string, desc: string? } --- @alias EmmyDocModule { name: string, members: EmmyDocFn[] } --- @class EmmyDocFn --- @field type 'fn' --- @field name string --- @field description string? --- @field deprecated boolean --- @field deprecation_reason string? --- @field loc EmmyDocLoc --- @field params EmmyDocParam[] --- @field returns EmmyDocReturn[] --- @class EmmyDocTypeField --- @field type 'field' --- @field name string --- @field description string? --- @field typ string --- @alias EmmyDocTypeMember EmmyDocTypeField | EmmyDocFn --- @class EmmyDocTypeClass --- @field type 'class' --- @field name string --- @field bases string[]? --- @field members EmmyDocTypeMember[] --- @class EmmyDocTypeAlias --- @field type 'alias' --- @field name string --- @field members EmmyDocTypeMember[] --- @alias EmmyDocType EmmyDocTypeClass | EmmyDocTypeAlias --- @class EmmyDocJson --- @field modules EmmyDocModule[] --- @field types EmmyDocType[]? --- @return EmmyDocJson local function load_emmy_doc() local path = 'emydoc/doc.json' local raw = vim.fn.readfile(path) local json = table.concat(raw, '\n') return vim.json.decode(json, { luanil = { object = true, array = true } }) end --- @param dep_info boolean|{new_field: string, message: string, hard: boolean} --- @param out fun(_: string?) local function gen_config_doc_deprecated(dep_info, out) if type(dep_info) == 'table' and dep_info.hard then out(INDENT_STR .. 'HARD-DEPRECATED') else out(INDENT_STR .. 'DEPRECATED') end if type(dep_info) == 'table' then if dep_info.message then out(INDENT_STR .. dep_info.message) end if dep_info.new_field then out('') local opts_key, field = dep_info.new_field:match('(.*)%.(.*)') if opts_key and field then out( (INDENT_STR .. 'Please instead use the field `%s` in |gitsigns-config-%s|.'):format( field, opts_key ) ) else out((INDENT_STR .. 'Please instead use |gitsigns-config-%s|.'):format(dep_info.new_field)) end end end out('') end --- @class IndentDocOpts --- @field dedent? boolean --- @field dedent_start? integer --- @field tilde_block? boolean --- @param lines string[] --- @param opts? IndentDocOpts --- @return string[] local function indent_lines(lines, opts) opts = opts or {} local dedent = opts.dedent if dedent == nil then dedent = true end local dedent_start = opts.dedent_start or 1 local tilde_block = opts.tilde_block or false local min_pad --- @type integer? if dedent then for i = dedent_start, #lines do local line = lines[i] if line ~= '' and line ~= '<' then local pad = #(line:match('^%s*') or '') if not min_pad or pad < min_pad then min_pad = pad end end end end local res = {} --- @type string[] local in_tilde_block = false for i, line in ipairs(lines) do if line == '' or line == '<' then res[#res + 1] = line else local mp = dedent and (min_pad or 0) or 0 if i < dedent_start then mp = 0 end local extra = tilde_block and in_tilde_block and INDENT or 0 res[#res + 1] = string.rep(' ', INDENT + extra) .. line:sub(mp + 1) end in_tilde_block = tilde_block and line:match(': ~%s*$') ~= nil end return res end --- @param v Gitsigns.SchemaElem --- @return string local function vtype(v) local ty = v.type_help or v.type if type(ty) == 'function' then error('type of type function must have type_help defined') elseif ty == 'table' and v.deep_extend then return 'table[extended]' elseif type(ty) == 'table' then --- @cast ty type[] return table.concat(ty, '|') end return ty end --- @param field string --- @param out fun(_: string?) local function gen_config_doc_field(field, out) local v = config.schema[field] -- Field heading and tag local t = ('*gitsigns-config-%s*'):format(field) if #field + #t < 80 then out(('%-29s %48s'):format(field, t)) else out(('%-29s'):format(field)) out(('%78s'):format(t)) end local deprecated = v.deprecated if deprecated then gen_config_doc_deprecated(deprecated, out) end if v.description then local d --- @type string local default_help = v.default_help if default_help ~= nil then d = default_help else d = ('`%s`'):format(inspect(v.default)) end if d:find('\n') then out((INDENT_STR .. 'Type: `%s`'):format(vtype(v))) out(INDENT_STR .. 'Default: >') local dlines = vim.split(d, '\n') while dlines[1] == '' do table.remove(dlines, 1) end while dlines[#dlines] == '' do table.remove(dlines, #dlines) end local normalized = indent_lines(dlines, { dedent_start = 2 }) for _, line in ipairs(normalized) do out(line) end out('<') else out((INDENT_STR .. 'Type: `%s`, Default: %s'):format(vtype(v), d)) out() end local desc_lines = vim.split(v.description:gsub(' +$', ''), '\n') while desc_lines[1] == '' do table.remove(desc_lines, 1) end while desc_lines[#desc_lines] == '' do table.remove(desc_lines, #desc_lines) end local normalized = indent_lines(desc_lines) for _, line in ipairs(normalized) do out(line) end end end --- @return string local function gen_config_doc() local res = {} ---@type string[] local function out(line) res[#res + 1] = line or '' end local first = true for _, k in ipairs(get_ordered_schema_keys()) do if first then first = false else out('') end gen_config_doc_field(k, out) end return table.concat(res, '\n') end --- @param x string[] --- @return string[] local function trim_lines(x) local min_pad --- @type integer? for _, e in ipairs(x) do local _, i = e:find('^ *') if not min_pad or min_pad > i then min_pad = i end end local r = {} --- @type string[] for _, e in ipairs(x) do r[#r + 1] = e:sub(assert(min_pad) + 1) end return r end --- @param s string --- @return string local function md_links_to_vimdoc(s) return (s:gsub('%[%[([^%]]+)%]%]', '|%1|')) end local function parse_fence_lang(s) local lang = s:match('^```%s*([%w_-]+)%s*$') if lang then return lang end if s:match('^```%s*$') then return '' end end --- Convert a small markdown subset into vimdoc. --- Supports: --- - Unordered list items (`- foo`) -> (`- foo`) --- - Code fences (```lua) -> (>lua) blocks --- - `Examples:` + code fence -> `Examples: >lua` blocks --- - `Attributes:` + bullet list -> `Attributes: ~` blocks --- --- @param lines string[] --- @return string[] local function markdown_to_vimdoc(lines) local out = {} --- @type string[] local i = 1 while i <= #lines do local line = assert(lines[i]) local next_line = lines[i + 1] local indent = line:match('^%s*') or '' local trimmed = line:sub(#indent + 1) local handled = false if trimmed:match('^Examples?:%s*$') and type(next_line) == 'string' then local next_indent = next_line:match('^%s*') or '' local next_trimmed = next_line:sub(#next_indent + 1) local lang = parse_fence_lang(next_trimmed) if lang and lang ~= '' then handled = true out[#out + 1] = md_links_to_vimdoc(indent .. trimmed:gsub('%s*$', '') .. ' >' .. lang) i = i + 2 while i <= #lines do local l = assert(lines[i]) local l_indent = l:match('^%s*') or '' local l_trimmed = l:sub(#l_indent + 1) if l_trimmed:match('^```%s*$') then out[#out + 1] = '<' i = i + 1 break end out[#out + 1] = l i = i + 1 end end end if not handled and trimmed:match('^Attributes:%s*$') then handled = true out[#out + 1] = indent .. 'Attributes: ~' i = i + 1 while i <= #lines do local l = assert(lines[i]) local l_indent = l:match('^%s*') or '' local l_trimmed = l:sub(#l_indent + 1) if l_trimmed == '' then out[#out + 1] = '' i = i + 1 break end local item = l_trimmed:match('^[-*+]%s+(.*)$') if not item then break end item = item:gsub('^`', ''):gsub('`$', '') out[#out + 1] = md_links_to_vimdoc(item) i = i + 1 end end if not handled then local lang = parse_fence_lang(trimmed) if lang then handled = true out[#out + 1] = indent .. '>' .. lang i = i + 1 while i <= #lines do local l = assert(lines[i]) local l_indent = l:match('^%s*') or '' local l_trimmed = l:sub(#l_indent + 1) if l_trimmed:match('^```%s*$') then out[#out + 1] = '<' i = i + 1 break end out[#out + 1] = l i = i + 1 end end end if not handled then local item = trimmed:match('^[-*+]%s+(.*)$') if item then handled = true out[#out + 1] = md_links_to_vimdoc(indent .. '• ' .. item) i = i + 1 end end if not handled then out[#out + 1] = md_links_to_vimdoc(line) i = i + 1 end end return out end --- @param first_prefix string --- @param next_prefix string --- @param text string --- @param max_width integer --- @return string[] local function wrap_words(first_prefix, next_prefix, text, max_width) if #first_prefix + #text <= max_width then return { first_prefix .. text } end local out = {} --- @type string[] local prefix = first_prefix local line = prefix local line_len = #line for word in text:gmatch('%S+') do local sep = (line_len == #prefix) and '' or ' ' if line_len + #sep + #word > max_width then if line_len > #prefix then out[#out + 1] = line prefix = next_prefix line = prefix .. word line_len = #line else if prefix:match('^%s*$') then -- If indentation makes the first word exceed max_width, reduce indent. local keep = math.max(0, max_width - #word) out[#out + 1] = prefix:sub(1, keep) .. word else -- Fall back to putting the (long) word on its own line. out[#out + 1] = line .. sep .. word end prefix = next_prefix line = prefix line_len = #line end else line = line .. sep .. word line_len = #line end end if line_len > #prefix then out[#out + 1] = line end return out end --- @param line string --- @param max_width integer --- @return string[] local function wrap_help_line(line, max_width) if #line <= max_width then return { line } end if line:match('^%s*$') or line:match('^%s*<%s*$') then return { line } end -- Don't wrap function tag headers; they rely on column alignment. if line:match('%*gitsigns%.') then return { line } end -- Param/return wrapping: keep the prefix (up to ': ') fixed and align continuations. local idx = line:find('): ') if idx then local prefix = line:sub(1, idx + 2) local rest = line:sub(idx + 3) return wrap_words(prefix, string.rep(' ', #prefix), rest, max_width) end local indent = line:match('^%s*') or '' local text = line:sub(#indent + 1) return wrap_words(indent, indent, text, max_width) end --- @param lines string[] --- @param max_width integer --- @return string[] local function wrap_help_lines(lines, max_width) local out = {} --- @type string[] local in_block = false for _, line in ipairs(lines) do if in_block then out[#out + 1] = line if line:match('^%s*<%s*$') then in_block = false end else -- Help "literal" blocks (examples/defaults) start with a line ending in '>' or '>lang'. if line:match('>%s*$') or line:match('>%w+%s*$') then out[#out + 1] = line in_block = true else list_extend(out, wrap_help_line(line, max_width)) end end end return out end --- @param ty EmmyDocTypeClass --- @param classes table --- @param fields_seen? table --- @return EmmyDocTypeField[] local function get_fields(ty, classes, fields_seen) fields_seen = fields_seen or {} local ret = {} --- @type EmmyDocTypeField[] for _, m in ipairs(ty.members or {}) do if not fields_seen[m.name] and m.type == 'field' then fields_seen[m.name] = true ret[#ret + 1] = m end end for _, b in ipairs(ty.bases or {}) do if classes[b] then list_extend(ret, get_fields(classes[b], classes)) end end return ret end --- @param name? string --- @param classes table --- @return string[]? local function build_type_field_docs(name, classes) local t = classes[name] if not t then return end local lines = {} --- @type string[] for _, m in ipairs(get_fields(t, classes)) do if m.typ then lines[#lines + 1] = string.format('• {%s}: (`%s`)', m.name, m.typ:gsub('`', '')) if m.description and m.description ~= '' then lines[#lines + 1] = ' ' .. m.description end end end return lines end --- @param name? string --- @param ty string --- @param desc? string[] --- @param name_pad? integer --- @return string[] local function render_param_or_return(name, ty, desc, name_pad) if type(ty) ~= 'string' then ty = 'any' end ty = ty:gsub('`', '') local ty_fmt = ('`%s`'):format(ty) name_pad = name_pad and (name_pad + 3) or 0 local name_str = '' --- @type string if name == ':' then name_str = '' elseif name then local nf = '%-' .. tostring(name_pad) .. 's' name_str = nf:format(string.format('{%s} ', name)) end desc = desc or {} if #desc == 0 then return { string.format(' %s(%s)', name_str, ty_fmt) } end local desc_vd = markdown_to_vimdoc(desc) local r = {} --- @type string[] local desc1_raw = assert(desc_vd[1]):gsub('^%s*:%s*', '') local desc1 = desc1_raw == '' and '' or ' ' .. desc1_raw r[#r + 1] = string.format(' %s(%s):%s', name_str, ty_fmt, desc1) local remain_desc = trim_lines(vim.list_slice(desc_vd, 2)) for _, d in ipairs(remain_desc) do r[#r + 1] = ' ' .. string.rep(' ', name_pad) .. d end return r end --- @param header string --- @param desc string[] --- @param params [string, string, string[]][] --- @param returns [string?, string, string[]?][] --- @param deprecated string? --- @return string[] local function render_block(header, desc, params, returns, deprecated) local res = {} if deprecated then list_extend(res, { INDENT_STR .. 'DEPRECATED: ' .. md_links_to_vimdoc(deprecated), '', }) end list_extend(res, indent_lines(markdown_to_vimdoc(desc), { dedent = false, tilde_block = true })) if #params > 0 then if res[#res] ~= '' then res[#res + 1] = '' end local param_block = { 'Parameters: ~' } local name_pad = 0 for _, v in ipairs(params) do if #v[1] > name_pad then name_pad = #v[1] end end for _, v in ipairs(params) do local name, ty, pdesc = v[1], v[2], v[3] list_extend(param_block, render_param_or_return(name, ty, pdesc, name_pad)) end list_extend(res, indent_lines(param_block, { dedent = false })) end if #returns > 0 then res[#res + 1] = '' local returns_block = { 'Returns: ~' } for _, v in ipairs(returns) do local name, ty, rdesc = v[1], v[2], v[3] list_extend(returns_block, render_param_or_return(name, ty, rdesc)) end list_extend(res, indent_lines(returns_block, { dedent = false })) end res = wrap_help_lines(res, 80) table.insert(res, 1, header) return res end --- @param classes table --- @param class_name string --- @return EmmyDocFn[] local function get_class_functions(classes, class_name) local t = classes[class_name] if not t or t.type ~= 'class' then return {} end local res = {} --- @type EmmyDocFn[] for _, member in ipairs(t.members) do if member.type == 'fn' and not startswith(member.name, '_') then res[#res + 1] = member end end table.sort(res, function(a, b) return a.loc.line > b.loc.line end) return res end --- @param ty? string --- @return string? local function strip_optional(ty) if not ty then return end return (ty:gsub('%?$', '')) end --- @param classes table --- @param member EmmyDocFn --- @return string[] local function render_fn_block(classes, member) local args = {} --- @type string[] for _, p in ipairs(member.params) do args[#args + 1] = ('{%s}'):format(p.name) end local deprecated --- @type string? local desc = member.description and vim.split(member.description, '\n') or {} if member.deprecated then local dep_lines = vim.split(assert(member.deprecation_reason), '\n') deprecated = (dep_lines[1] or ''):gsub('^%s+', ''):gsub('%s+$', '') -- EmmyLua currently includes subsequent doc lines in the deprecation reason. -- Treat those as normal description text, stripping the one leading space -- it prefixes onto each line to preserve intended indentation. for i = 2, #dep_lines do desc[#desc + 1] = dep_lines[i]:gsub('^ ', '') end end local params = {} --- @type [string, string, string[]][] for _, p in ipairs(member.params) do local d = p.desc and vim.split(p.desc, '\n') or {} local type_field_docs = build_type_field_docs(strip_optional(p.typ), classes) if type_field_docs then list_extend(d, type_field_docs) end params[#params + 1] = { p.name, p.typ, d } end local returns = {} --- @type [string?, string, string[]?][] if not (#member.returns == 1 and member.returns[1].typ == 'nil') then for _, ret in ipairs(member.returns) do local d = type(ret.desc) == 'string' and vim.split(ret.desc, '\n') or {} returns[#returns + 1] = { ret.name, ret.typ, d } end end local sig = ('%s(%s)'):format(member.name, table.concat(args, ', ')) local header = ('%-40s%38s'):format(sig, '*gitsigns.' .. member.name .. '()*') return render_block(header, desc, params, returns, deprecated) end --- @return string local function gen_functions_doc() local doc = load_emmy_doc() local classes = {} --- @type table for _, t in ipairs(doc.types or {}) do if t.type == 'class' then classes[t.name] = t end end local out = {} --- @type string[] for _, class_name in ipairs({ 'gitsigns.main', 'gitsigns.actions' }) do for _, member in ipairs(get_class_functions(classes, class_name)) do local b = render_fn_block(classes, member) for _, line in ipairs(b) do out[#out + 1] = line:match('^ *$') and '' or line end out[#out + 1] = '' end end return table.concat(out, '\n') end --- @return string local function gen_highlights_doc() local res = {} --- @type string[] local highlights = require('lua.gitsigns.highlight') local name_max = 0 for _, hl in ipairs(highlights.hls) do for name, _ in pairs(hl) do if name:len() > name_max then name_max = name:len() end end end for _, hl in ipairs(highlights.hls) do for name, spec in pairs(hl) do if not spec.hidden then local fallbacks_tbl = {} --- @type string[] for _, f in ipairs(spec) do fallbacks_tbl[#fallbacks_tbl + 1] = string.format('`%s`', f) end local fallbacks = table.concat(fallbacks_tbl, ', ') if spec.fg_factor then fallbacks = fallbacks .. (' (fg=%d%%)'):format(spec.fg_factor * 100) end res[#res + 1] = string.format('%s*hl-%s*', string.rep(' ', 56), name) res[#res + 1] = string.format('%s', name) if spec.desc then res[#res + 1] = string.format('%s%s', INDENT_STR, spec.desc) res[#res + 1] = '' end res[#res + 1] = string.format('%sFallbacks: %s', INDENT_STR, fallbacks) end end end return table.concat(res, '\n') end --- @return string local function get_setup_from_readme() local readme = io.lines('README.md') --- @type Iterator[string] local res = {} --- @type string[] local function append(line) res[#res + 1] = line ~= '' and INDENT_STR .. line or '' end for l in readme do if l:match("require%('gitsigns'%).setup {") then append(l) break end end for l in readme do append(l) if l == '}' then break end end return table.concat(res, '\n') end --- @param marker string --- @return string|fun():string local function get_marker_text(marker) return ({ VERSION = 'v2.0.0', -- x-release-please-version CONFIG = gen_config_doc, FUNCTIONS = gen_functions_doc, HIGHLIGHTS = gen_highlights_doc, SETUP = get_setup_from_readme, })[marker] or error('Unknown marker: ' .. marker) end local function main() local template = io.lines('etc/doc_template.txt') --- @type Iterator[string] local out = assert(io.open('doc/gitsigns.txt', 'w')) for l in template do local l1 = l local marker = l1:match('{{(.*)}}') if marker then local sub = get_marker_text(marker) if sub then if type(sub) == 'function' then sub = sub() end --- @type string sub = sub:gsub('%%', '%%%%') l1 = l1:gsub('{{' .. marker .. '}}', sub) end end out:write(l1 or '', '\n') end end main() neovim-gitsigns-2.0.0/gitsigns.nvim-scm-1.rockspec000066400000000000000000000013241513053142700221040ustar00rootroot00000000000000local _MODREV, _SPECREV = 'scm', '-1' rockspec_format = "3.0" package = 'gitsigns.nvim' version = _MODREV .. _SPECREV description = { summary = 'Git signs written in pure lua', detailed = [[ Super fast git decorations implemented purely in Lua. ]], homepage = 'http://github.com/lewis6991/gitsigns.nvim', license = 'MIT/X11', labels = { 'neovim' } } dependencies = { 'lua == 5.1', } source = { url = 'http://github.com/lewis6991/gitsigns.nvim/archive/v' .. _MODREV .. '.zip', dir = 'gitsigns.nvim-' .. _MODREV, } if _MODREV == 'scm' then source = { url = 'git://github.com/lewis6991/gitsigns.nvim', } end build = { type = 'builtin', copy_directories = { 'doc', 'plugin' } } neovim-gitsigns-2.0.0/lua/000077500000000000000000000000001513053142700154155ustar00rootroot00000000000000neovim-gitsigns-2.0.0/lua/gitsigns.lua000066400000000000000000000160701513053142700177530ustar00rootroot00000000000000local api = vim.api local uv = vim.uv or vim.loop ---@diagnostic disable-line: deprecated --- @class gitsigns.main: gitsigns.actions,gitsigns.attach,gitsigns.debug local M = {} local cwd_watcher ---@type uv.uv_fs_event_t? local function log() return require('gitsigns.debug.log') end local function config() return require('gitsigns.config').config end local function async() return require('gitsigns.async') end --- @async --- @return string? gitdir --- @return string? head local function get_gitdir_and_head() local cwd = uv.cwd() if not cwd then return end -- Run on the main loop to avoid: -- https://github.com/LazyVim/LazyVim/discussions/3407#discussioncomment-9622211 async().schedule() -- Look in the cache first if package.loaded['gitsigns.cache'] then for _, bcache in pairs(require('gitsigns.cache').cache) do local repo = bcache.git_obj.repo if repo.toplevel == cwd then return repo.gitdir, repo.abbrev_head end end end local info = require('gitsigns.git').Repo.get_info(cwd) if info then return info.gitdir, info.abbrev_head end end ---Sets up the cwd watcher to detect branch changes using uv.loop ---Uses module local variable cwd_watcher ---@async ---@param cwd string current working directory ---@param towatch string Directory to watch local function setup_cwd_watcher(cwd, towatch) if cwd_watcher then cwd_watcher:stop() -- TODO(lewis6991): (#1027) Running `fs_event:stop()` -> `fs_event:start()` -- in the same loop event, on Windows, causes Nvim to hang on quit. if vim.fn.has('win32') == 1 then async().schedule() end else cwd_watcher = assert(uv.new_fs_event()) end if cwd_watcher:getpath() == towatch then -- Already watching return end local debounce_trailing = require('gitsigns.debounce').debounce_trailing local update_head = debounce_trailing(100, function() async() .run(function() local git = require('gitsigns.git') local info = git.Repo.get_info(cwd) or {} local new_head = info.abbrev_head async().schedule() if new_head ~= vim.g.gitsigns_head then vim.g.gitsigns_head = new_head api.nvim_exec_autocmds('User', { pattern = 'GitSignsUpdate', modeline = false, }) end end) :raise_on_error() end) -- Watch .git/HEAD to detect branch changes cwd_watcher:start(towatch, {}, function() async().run(function(err) local __FUNC__ = 'cwd_watcher_cb' if err then log().dprintf('Git dir update error: %s', err) return end log().dprint('Git cwd dir update') update_head() -- git often (always?) replaces .git/HEAD which can change the inode being -- watched so we need to stop the current watcher and start another one to -- make sure we keep getting future events setup_cwd_watcher(cwd, towatch) end) end) end --- @async local function update_cwd_head() local cwd = uv.cwd() if not cwd then return end local paths = vim.fs.find('.git', { limit = 1, upward = true, type = 'directory', }) if #paths == 0 then return end local gitdir, head = get_gitdir_and_head() async().schedule() api.nvim_exec_autocmds('User', { pattern = 'GitSignsUpdate', modeline = false, }) vim.g.gitsigns_head = head if not gitdir then return end local towatch = gitdir .. '/HEAD' setup_cwd_watcher(cwd, towatch) end local function setup_cli() api.nvim_create_user_command('Gitsigns', function(params) async().run(function() require('gitsigns.cli').run(params) end) end, { force = true, nargs = '*', range = true, complete = function(arglead, line) return require('gitsigns.cli').complete(arglead, line) end, }) end local function setup_attach() local attach_autocmd_disabled = false -- Need to attach in 'BufFilePost' since we always detach in 'BufFilePre' api.nvim_create_autocmd({ 'BufFilePost', 'BufRead', 'BufNewFile', 'BufWritePost' }, { group = 'gitsigns', desc = 'Gitsigns: attach', callback = function(args) if not config().auto_attach then return end local bufnr = args.buf if attach_autocmd_disabled then local __FUNC__ = 'attach_autocmd' log().dprint('Attaching is disabled') return end require('gitsigns.actions').attach(bufnr, nil, args.event) end, }) --- vimpgrep creates and deletes lots of buffers so attaching to each one will --- waste lots of resource and slow down vimgrep. api.nvim_create_autocmd({ 'QuickFixCmdPre', 'QuickFixCmdPost' }, { group = 'gitsigns', pattern = '*vimgrep*', desc = 'Gitsigns: disable attach during vimgrep', callback = function(args) attach_autocmd_disabled = args.event == 'QuickFixCmdPre' end, }) -- Attach to all open buffers if config().auto_attach then for _, buf in ipairs(api.nvim_list_bufs()) do if api.nvim_buf_is_loaded(buf) and api.nvim_buf_get_name(buf) ~= '' then -- Make sure to run each attach in its on async context in case one of the -- attaches is aborted. require('gitsigns.actions').attach(buf, nil, 'setup') end end end end local function setup_cwd_head() local debounce_trailing = require('gitsigns.debounce').debounce_trailing local update_cwd_head_debounced = debounce_trailing(100, function() async().run(update_cwd_head):raise_on_error() end) update_cwd_head_debounced() -- Need to debounce in case some plugin changes the cwd too often -- (like vim-grepper) api.nvim_create_autocmd('DirChanged', { group = 'gitsigns', callback = function() update_cwd_head_debounced() end, }) end -- When setup() is called when this is true, setup autocmads and define -- highlights. If false then rebuild the configuration and re-setup -- modules that depend on the configuration. local init = true --- Setup and start Gitsigns. --- --- @param cfg table|nil Configuration for Gitsigns. --- See |gitsigns-usage| for more details. function M.setup(cfg) if vim.fn.executable('git') == 0 then print('gitsigns: git not in path. Aborting setup') return end if cfg then require('gitsigns.config').build(cfg) end -- Only do this once if init then api.nvim_create_augroup('gitsigns', {}) setup_cli() -- TODO(lewis6991): do this lazily require('gitsigns.highlight').setup() setup_attach() setup_cwd_head() init = false end end --- @param bufnr? integer Buffer number. Defaults to current buffer. --- @param lnum? integer Line number to return status for. Defaults to `vim.v.lnum` --- @return string function M.statuscolumn(bufnr, lnum) return require('gitsigns.manager').statuscolumn(bufnr, lnum) end M = setmetatable(M, { __index = function(_, f) local actions = require('gitsigns.actions') if actions[f] then return actions[f] end if config().debug_mode then local debug = require('gitsigns.debug') if debug[f] then return debug[f] end end end, }) return M neovim-gitsigns-2.0.0/lua/gitsigns/000077500000000000000000000000001513053142700172445ustar00rootroot00000000000000neovim-gitsigns-2.0.0/lua/gitsigns/actions.lua000066400000000000000000000656331513053142700214240ustar00rootroot00000000000000local async = require('gitsigns.async') local Hunks = require('gitsigns.hunks') local manager = require('gitsigns.manager') local message = require('gitsigns.message') local util = require('gitsigns.util') local config = require('gitsigns.config').config local mk_repeatable = require('gitsigns.repeat').mk_repeatable local cache = require('gitsigns.cache').cache local api = vim.api local current_buf = api.nvim_get_current_buf local tointeger = util.tointeger --- @class gitsigns.actions local M = {} --- @class Gitsigns.CmdParams.Smods --- @field vertical boolean --- @field split 'aboveleft'|'belowright'|'topleft'|'botright' --- @class Gitsigns.CmdArgs --- @field vertical? boolean --- @field split? boolean --- @field global? boolean --- @field [integer] any --- @class Gitsigns.CmdParams : vim.api.keyset.create_user_command.command_args --- @field smods Gitsigns.CmdParams.Smods --- @class (exact) Gitsigns.HunkOpts --- Operate on/select all contiguous hunks. Only useful if 'diff_opts' --- contains `linematch`. Defaults to `true`. --- @field greedy? boolean --- @class (exact) Gitsigns.SetqflistOpts --- @field use_location_list? boolean Populate the location list instead of the quickfix list. --- @field nr? integer Window number or ID when using location list. Defaults to `0`. --- @field open? boolean Open the quickfix/location list viewer. Defaults to `true`. --- Variations of functions from M which are used for the Gitsigns command --- @type table local C = {} --- Completion functions for the respective actions in C local CP = {} --- @generic T --- @param callback? fun(err?: string) --- @param func async fun(...:T...) # The async function to wrap --- @return Gitsigns.async.Task local function async_run(callback, func, ...) assert(type(func) == 'function') local task = async.run(func, ...) if callback and type(callback) == 'function' then task:await(callback) else task:raise_on_error() end return task end --- @param arglead string --- @return string[] local function complete_heads(arglead) --- @type string[] local all = vim.fn.systemlist({ 'git', 'rev-parse', '--symbolic', '--branches', '--tags', '--remotes' }) return vim.tbl_filter( --- @param x string --- @return boolean function(x) return vim.startswith(x, arglead) end, all ) end --- Detach Gitsigns from all buffers it is attached to. function M.detach_all() require('gitsigns.attach').detach_all() end --- Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not --- provided then the current buffer is used. --- --- @param bufnr integer Buffer number function M.detach(bufnr) require('gitsigns.attach').detach(bufnr) end --- Attach Gitsigns to the buffer. --- --- Attributes: --- - {async} --- --- @param bufnr integer Buffer number --- @param ctx Gitsigns.GitContext? --- Git context data that may optionally be used to attach to any buffer that represents a git --- object. --- @param trigger? string --- @param callback? fun(err?: string) function M.attach(bufnr, ctx, trigger, callback) async_run(callback, require('gitsigns.attach').attach, bufnr or current_buf(), ctx, trigger) end --- Toggle [[gitsigns-config-signbooleancolumn]] --- --- @param value boolean|nil Value to set toggle. If `nil` --- the toggle value is inverted. --- @return boolean : Current value of [[gitsigns-config-signcolumn]] function M.toggle_signs(value) if value ~= nil then config.signcolumn = value else config.signcolumn = not config.signcolumn end return config.signcolumn end --- Toggle [[gitsigns-config-numhl]] --- --- @param value boolean|nil Value to set toggle. If `nil` --- the toggle value is inverted. --- --- @return boolean : Current value of [[gitsigns-config-numhl]] function M.toggle_numhl(value) if value ~= nil then config.numhl = value else config.numhl = not config.numhl end return config.numhl end --- Toggle [[gitsigns-config-linehl]] --- --- @param value boolean|nil Value to set toggle. If `nil` --- the toggle value is inverted. --- @return boolean : Current value of [[gitsigns-config-linehl]] M.toggle_linehl = function(value) if value ~= nil then config.linehl = value else config.linehl = not config.linehl end return config.linehl end --- Toggle [[gitsigns-config-word_diff]] --- --- @param value boolean|nil Value to set toggle. If `nil` --- the toggle value is inverted. --- @return boolean : Current value of [[gitsigns-config-word_diff]] function M.toggle_word_diff(value) if value ~= nil then config.word_diff = value else config.word_diff = not config.word_diff end -- Don't use refresh() to avoid flicker util.redraw({ buf = 0, range = { vim.fn.line('w0') - 1, vim.fn.line('w$') } }) return config.word_diff end --- Toggle [[gitsigns-config-current_line_blame]] --- --- @param value boolean|nil Value to set toggle. If `nil` --- the toggle value is inverted. --- @return boolean : Current value of [[gitsigns-config-current_line_blame]] function M.toggle_current_line_blame(value) if value ~= nil then config.current_line_blame = value else config.current_line_blame = not config.current_line_blame end return config.current_line_blame end --- @deprecated Use [[gitsigns.preview_hunk_inline()]] --- Toggle [[gitsigns-config-show_deleted]] --- --- @param value boolean|nil Value to set toggle. If `nil` --- the toggle value is inverted. --- @return boolean : Current value of [[gitsigns-config-show_deleted]] function M.toggle_deleted(value) if value ~= nil then config.show_deleted = value else config.show_deleted = not config.show_deleted end return config.show_deleted end --- @async --- @param bufnr integer local function update(bufnr) local bcache = cache[bufnr] if not bcache then return end manager.update(bufnr) if not bcache:schedule() then return end if vim.wo.diff then require('gitsigns.actions.diffthis').update(bufnr) end end --- @param params Gitsigns.CmdParams --- @return [integer, integer]? range Range of lines to operate on. local function get_range(params) local range --- @type [integer, integer]? if params.range > 0 then range = { params.line1, params.line2 } end return range end --- Stage the hunk at the cursor position, or all lines in the --- given range. If {range} is provided, all lines in the given --- range are staged. This supports partial-hunks, meaning if a --- range only includes a portion of a particular hunk, only the --- lines within the range will be staged. --- --- Attributes: --- - {async} --- --- @param range [integer, integer]? List-like table of two integers making --- up the line range from which you want to stage the hunks. --- If running via command line, then this is taken from the --- command modifiers. --- @param opts Gitsigns.HunkOpts? Additional options. --- @param callback? fun(err?: string) function M.stage_hunk(range, opts, callback) --- @cast range [integer, integer]? opts = opts or {} local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end if not util.Path.exists(bcache.file) then print('Error: Cannot stage lines. Please add the file to the working tree.') return end async_run(callback, function() bcache.git_obj:lock(function() local hunk = bcache:get_hunk(range, opts.greedy ~= false, false) local invert = false if not hunk then invert = true hunk = bcache:get_hunk(range, opts.greedy ~= false, true) end if not hunk then api.nvim_echo({ { 'No hunk to stage', 'WarningMsg' } }, false, {}) return end local err = bcache.git_obj:stage_hunks({ hunk }, invert) if err then message.error(err) return end table.insert(bcache.staged_diffs, hunk) end) bcache:invalidate(true) update(bufnr) end) end M.stage_hunk = mk_repeatable(M.stage_hunk) C.stage_hunk = function(_, params) M.stage_hunk(get_range(params)) end --- @param bufnr integer --- @param hunk Gitsigns.Hunk.Hunk local function reset_hunk(bufnr, hunk) local lstart, lend --- @type integer, integer if hunk.type == 'delete' then lstart = hunk.added.start lend = hunk.added.start else lstart = hunk.added.start - 1 lend = hunk.added.start - 1 + hunk.added.count end if hunk.removed.no_nl_at_eof ~= hunk.added.no_nl_at_eof then local no_eol = hunk.added.no_nl_at_eof or false vim.bo[bufnr].endofline = no_eol vim.bo[bufnr].fixendofline = no_eol end util.set_lines(bufnr, lstart, lend, hunk.removed.lines) end --- Reset the lines of the hunk at the cursor position, or all --- lines in the given range. If {range} is provided, all lines in --- the given range are reset. This supports partial-hunks, --- meaning if a range only includes a portion of a particular --- hunk, only the lines within the range will be reset. --- --- @param range [integer, integer]? List-like table of two integers making --- up the line range from which you want to reset the hunks. --- If running via command line, then this is taken from the --- command modifiers. --- @param opts Gitsigns.HunkOpts? Additional options. --- @param callback? fun(err?: string) function M.reset_hunk(range, opts, callback) --- @cast range [integer, integer]? async_run(callback, function() opts = opts or {} local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end local hunk = bcache:get_hunk(range, opts.greedy ~= false, false) if not hunk then api.nvim_echo({ { 'No hunk to reset', 'WarningMsg' } }, false, {}) return end reset_hunk(bufnr, hunk) end) end M.reset_hunk = mk_repeatable(M.reset_hunk) function C.reset_hunk(_, params) M.reset_hunk(get_range(params)) end --- Reset the lines of all hunks in the buffer. function M.reset_buffer() local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end local hunks = bcache.hunks if not hunks or #hunks == 0 then api.nvim_echo({ { 'No unstaged changes in the buffer to reset', 'WarningMsg' } }, false, {}) return end for i = #hunks, 1, -1 do reset_hunk(bufnr, hunks[i] --[[@as Gitsigns.Hunk.Hunk]]) end end --- @deprecated use [[gitsigns.stage_hunk()]] on staged signs --- Undo the last call of stage_hunk(). --- --- Note: only the calls to stage_hunk() performed in the current --- session can be undone. --- --- Attributes: --- - {async} --- --- @param callback? fun(err?: string) function M.undo_stage_hunk(callback) async_run(callback, function() local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end bcache.git_obj:lock(function() local hunk = table.remove(bcache.staged_diffs) if not hunk then print('No hunks to undo') return end local err = bcache.git_obj:stage_hunks({ hunk }, true) if err then message.error(err) return end end) bcache:invalidate(true) update(bufnr) end) end --- Stage all hunks in current buffer. --- --- Attributes: --- - {async} --- --- @param callback? fun(err?: string) function M.stage_buffer(callback) async_run(callback, function() local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end bcache.git_obj:lock(function() -- Only process files with existing hunks local hunks = bcache.hunks if not hunks or #hunks == 0 then print('No unstaged changes in file to stage') return end if not util.Path.exists(bcache.git_obj.file) then print('Error: Cannot stage file. Please add it to the working tree.') return end local err = bcache.git_obj:stage_hunks(hunks) if err then message.error(err) return end for _, hunk in ipairs(hunks) do table.insert(bcache.staged_diffs, hunk) end end) bcache:invalidate(true) update(bufnr) end) end --- Unstage all hunks for current buffer in the index. Note: --- Unlike [[gitsigns.undo_stage_hunk()]] this doesn't simply undo --- stages, this runs an `git reset` on current buffers file. --- --- Attributes: --- - {async} --- --- @param callback? fun(err?: string) function M.reset_buffer_index(callback) async_run(callback, function() local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end bcache.git_obj:lock(function() -- `bcache.staged_diffs` won't contain staged changes outside of current -- neovim session so signs added from this unstage won't be complete They will -- however be fixed by gitdir watcher and properly updated We should implement -- some sort of initial population from git diff, after that this function can -- be improved to check if any staged hunks exists and it can undo changes -- using git apply line by line instead of resetting whole file bcache.staged_diffs = {} bcache.git_obj:unstage_file() end) bcache:invalidate(true) update(bufnr) end) end --- Jump to hunk in the current buffer. If a hunk preview --- (popup or inline) was previously opened, it will be re-opened --- at the next hunk. --- --- Attributes: --- - {async} --- --- @param direction 'first'|'last'|'next'|'prev' --- @param opts Gitsigns.NavOpts? Configuration options. --- @param callback? fun(err?: string) function M.nav_hunk(direction, opts, callback) async_run(callback, function() --- @cast opts Gitsigns.NavOpts? require('gitsigns.actions.nav').nav_hunk(direction, opts) end) end function C.nav_hunk(args, _) --- @diagnostic disable-next-line: param-type-mismatch M.nav_hunk(args[1], args) end --- @deprecated use [[gitsigns.nav_hunk()]] --- Jump to the next hunk in the current buffer. If a hunk preview --- (popup or inline) was previously opened, it will be re-opened --- at the next hunk. --- --- See [[gitsigns.nav_hunk()]]. --- --- Attributes: --- - {async} function M.next_hunk(opts, callback) async_run(callback, function() require('gitsigns.actions.nav').nav_hunk('next', opts) end) end function C.next_hunk(args, _) --- @diagnostic disable-next-line: param-type-mismatch M.nav_hunk('next', args) end --- @deprecated use [[gitsigns.nav_hunk()]] --- Jump to the previous hunk in the current buffer. If a hunk preview --- (popup or inline) was previously opened, it will be re-opened --- at the previous hunk. --- --- See [[gitsigns.nav_hunk()]]. --- --- Attributes: --- - {async} function M.prev_hunk(opts, callback) async_run(callback, function() require('gitsigns.actions.nav').nav_hunk('prev', opts) end) end function C.prev_hunk(args, _) --- @diagnostic disable-next-line: param-type-mismatch M.nav_hunk('prev', args) end --- Preview the hunk at the cursor position in a floating --- window. If the preview is already open, calling this --- will cause the window to get focus. function M.preview_hunk() require('gitsigns.actions.preview').preview_hunk() end --- Preview the hunk at the cursor position inline in the buffer. --- @param callback? fun(err?: string) function M.preview_hunk_inline(callback) async_run(callback, function() require('gitsigns.actions.preview').preview_hunk_inline() end) end --- Select the hunk under the cursor. --- --- @param opts Gitsigns.HunkOpts? Additional options. function M.select_hunk(opts) local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end opts = opts or {} local hunk --- @type Gitsigns.Hunk.Hunk? async .run(function() hunk = bcache:get_hunk(nil, opts.greedy ~= false) end) :wait() if not hunk then return end if vim.fn.mode():find('v') ~= nil then vim.cmd('normal! ' .. hunk.added.start .. 'GoV' .. hunk.vend .. 'G') else vim.cmd('normal! ' .. hunk.added.start .. 'GV' .. hunk.vend .. 'G') end end --- Get hunk array for specified buffer. --- --- @param bufnr integer Buffer number, if not provided (or 0) --- will use current buffer. --- @return table? : Array of hunk objects. --- Each hunk object has keys: --- - `"type"`: String with possible values: "add", "change", --- "delete" --- - `"head"`: Header that appears in the unified diff --- output. --- - `"lines"`: Line contents of the hunks prefixed with --- either `"-"` or `"+"`. --- - `"removed"`: Sub-table with fields: --- - `"start"`: Line number (1-based) --- - `"count"`: Line count --- - `"added"`: Sub-table with fields: --- - `"start"`: Line number (1-based) --- - `"count"`: Line count M.get_hunks = function(bufnr) if (bufnr or 0) == 0 then bufnr = current_buf() end if not cache[bufnr] then return end local ret = {} --- @type Gitsigns.Hunk.Hunk_Public[] -- TODO(lewis6991): allow this to accept a greedy option for _, h in ipairs(cache[bufnr].hunks or {}) do ret[#ret + 1] = { head = h.head, lines = Hunks.patch_lines(h, vim.bo[bufnr].fileformat), type = h.type, added = h.added, removed = h.removed, } end return ret end --- Run git blame on the current line and show the results in a --- floating window. If already open, calling this will cause the --- window to get focus. --- --- Attributes: --- - {async} --- --- @param opts Gitsigns.LineBlameOpts? Additional options. --- @param callback? fun(err?: string) function M.blame_line(opts, callback) --- @cast opts Gitsigns.LineBlameOpts? async_run(callback, require('gitsigns.actions.blame_line'), opts) end C.blame_line = function(args, _) --- @diagnostic disable-next-line: param-type-mismatch M.blame_line(args) end --- Run git-blame on the current file and open the results --- in a scroll-bound vertical split. --- --- Mappings: --- is mapped to open a menu with the other mappings --- Note: must be held to activate the mappings whilst the menu is --- open. --- s [Show commit] in a vertical split. --- S [Show commit] in a new tab. --- r [Reblame at commit] --- --- Attributes: --- - {async} --- --- @param opts Gitsigns.BlameOpts? Additional options. --- @param callback? fun(err?: string) function M.blame(opts, callback) async_run(callback, require('gitsigns.actions.blame').blame, opts) end --- @async --- @param bcache Gitsigns.CacheEntry --- @param base string? local function update_buf_base(bcache, base) bcache.file_mode = base == 'FILE' if not bcache.file_mode then bcache.git_obj:change_revision(base) end bcache:invalidate(true) update(bcache.bufnr) end --- Change the base revision to diff against. If {base} is not --- given, then the original base is used. If {global} is given --- and true, then change the base revision of all buffers, --- including any new buffers. --- --- Attributes: --- - {async} --- --- Examples: --- ```lua --- -- Change base to 1 commit behind head --- require('gitsigns').change_base('HEAD~1') --- -- :Gitsigns change_base HEAD~1 --- --- -- Also works using the Gitsigns command --- :Gitsigns change_base HEAD~1 --- --- -- Other variations --- require('gitsigns').change_base('~1') --- -- :Gitsigns change_base ~1 --- require('gitsigns').change_base('~') --- -- :Gitsigns change_base ~ --- require('gitsigns').change_base('^') --- -- :Gitsigns change_base ^ --- --- -- Commits work too --- require('gitsigns').change_base('92eb3dd') --- -- :Gitsigns change_base 92eb3dd --- --- -- Revert to original base --- require('gitsigns').change_base() --- -- :Gitsigns change_base --- ``` --- --- For a more complete list of ways to specify bases, see --- [[gitsigns-revision]]. --- --- @param base string? The object/revision to diff against. --- @param global boolean? Change the base of all buffers. --- @param callback? fun(err?: string) function M.change_base(base, global, callback) async_run(callback, function() base = util.norm_base(base) if global then config.base = base for _, bcache in pairs(cache) do update_buf_base(bcache, base) end else local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end update_buf_base(bcache, base) end end) end C.change_base = function(args, _) M.change_base(args[1], (args[2] or args.global)) end CP.change_base = complete_heads --- Reset the base revision to diff against back to the --- index. --- --- Alias for `change_base(nil, {global})` . M.reset_base = function(global) M.change_base(nil, global) end C.reset_base = function(args, _) M.change_base(nil, (args[1] or args.global)) end --- Perform a [[vimdiff]] on the given file with {base} if it is --- given, or with the currently set base (index by default). --- --- If {base} is the index, then the opened buffer is editable and --- any written changes will update the index accordingly. --- --- Examples: --- ```lua --- -- Diff against the index --- require('gitsigns').diffthis() --- -- :Gitsigns diffthis --- --- -- Diff against the last commit --- require('gitsigns').diffthis('~1') --- -- :Gitsigns diffthis ~1 --- ``` --- --- For a more complete list of ways to specify bases, see --- [[gitsigns-revision]]. --- --- Attributes: --- - {async} --- --- @param base string|nil Revision to diff against. Defaults to index. --- @param opts Gitsigns.DiffthisOpts? Additional options. --- @param callback? fun(err?: string) function M.diffthis(base, opts, callback) --- @cast opts Gitsigns.DiffthisOpts -- TODO(lewis6991): can't pass numbers as strings from the command line if base ~= nil then base = tostring(base) end opts = opts or {} if opts.vertical == nil then opts.vertical = config.diff_opts.vertical end async_run(callback, require('gitsigns.actions.diffthis').diffthis, base, opts) end function C.diffthis(args, params) -- TODO(lewis6991): validate these local opts = { vertical = config.diff_opts.vertical, split = args.split, } if args.vertical ~= nil then opts.vertical = args.vertical end if params.smods then if params.smods.split ~= '' and opts.split == nil then opts.split = params.smods.split end if opts.vertical == nil then opts.vertical = params.smods.vertical end end M.diffthis(args[1], opts) end CP.diffthis = complete_heads -- C.test = function(pos_args: {any}, named_args: {string:any}, params: api.UserCmdParams) -- print('POS ARGS:', vim.inspect(pos_args)) -- print('NAMED ARGS:', vim.inspect(named_args)) -- print('PARAMS:', vim.inspect(params)) -- end --- Show revision {base} of the current file, if it is given, or --- with the currently set base (index by default). --- --- If {base} is the index, then the opened buffer is editable and --- any written changes will update the index accordingly. --- --- Examples: --- ```lua --- -- View the index version of the file --- require('gitsigns').show() --- -- :Gitsigns show --- --- -- View revision of file in the last commit --- require('gitsigns').show('~1') --- -- :Gitsigns show ~1 --- ``` --- --- For a more complete list of ways to specify bases, see --- [[gitsigns-revision]]. --- --- Attributes: --- - {async} --- --- @param revision string? --- @param callback? fun(err?: string) function M.show(revision, callback) async_run(callback, require('gitsigns.actions.diffthis').show, nil, revision) end function C.show(args) local revision = args[1] if revision ~= nil then revision = tostring(revision) end M.show(revision) end CP.show = complete_heads --- Show revision {base} commit in split or tab --- --- @param revision? string? (default: 'HEAD') --- @param open? 'vsplit'|'tabnew' --- @param callback? fun(err?: string) function M.show_commit(revision, open, callback) async_run(callback, require('gitsigns.actions.show_commit'), revision, open) end function C.show_commit(args) local revision, open = args[1], args[2] M.show_commit(revision, open) end CP.show_commit = complete_heads --- Populate the quickfix list with hunks. Automatically opens the --- quickfix window. --- --- Attributes: --- - {async} --- --- @param target integer|'attached'|'all'? # --- Specifies which files hunks are collected from. --- Possible values. --- - [integer]: The buffer with the matching buffer --- number. `0` for current buffer (default). --- - `"attached"`: All attached buffers. --- - `"all"`: All modified files for each git --- directory of all attached buffers in addition --- to the current working directory. --- @param opts Gitsigns.SetqflistOpts? Additional options. --- @param callback? fun(err?: string) function M.setqflist(target, opts, callback) async_run(callback, require('gitsigns.actions.qflist').setqflist, target, opts) end function C.setqflist(args) local target = tointeger(args[1]) or args[1] --- @diagnostic disable-next-line: param-type-mismatch M.setqflist(target, args) end --- Populate the location list with hunks. Automatically opens the --- location list window. --- --- Alias for: `setqflist({target}, { use_location_list = true, nr = {nr} }` --- --- Attributes: --- - {async} --- --- @param nr? integer Window number or the [[window-ID]]. --- `0` for the current window (default). --- @param target integer|'attached'|'all'|nil See [[gitsigns.setqflist()]]. function M.setloclist(nr, target) M.setqflist(target, { nr = nr, use_location_list = true, }) end function C.setloclist(args) local target = tointeger(args[2]) or args[2] M.setloclist(tointeger(args[1]), target) end --- Get all the available line specific actions for the current --- buffer at the cursor position. --- --- @return table|nil : Dictionary of action name to function which when called --- performs action. M.get_actions = function() local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end local hunk = bcache:get_cursor_hunk() --- @type string[] local actions_l = {} if hunk then vim.list_extend(actions_l, { 'stage_hunk', 'reset_hunk', 'preview_hunk', 'select_hunk', }) else actions_l[#actions_l + 1] = 'blame_line' end if not vim.tbl_isempty(bcache.staged_diffs) then actions_l[#actions_l + 1] = 'undo_stage_hunk' end local actions = {} --- @type table for _, a in ipairs(actions_l) do actions[a] = M[a] --[[@as function]] end return actions end for name, f in pairs(M --[[@as table]]) do if vim.startswith(name, 'toggle') then C[name] = function(args) f(args[1]) end end end --- Refresh all buffers. --- --- Attributes: --- - {async} --- --- @param callback? fun(err?: string) function M.refresh(callback) manager.reset_signs() require('gitsigns.highlight').setup_highlights() require('gitsigns.current_line_blame').setup() async_run(callback, function() for k, v in pairs(cache) do v:invalidate(true) manager.update(k) end end) end --- @param name string --- @return fun(args: table, params: Gitsigns.CmdParams) function M._get_cmd_func(name) return C[name] end --- @param name string --- @return (fun(arglead: string): string[])? function M._get_cmp_func(name) return CP[name] end return M neovim-gitsigns-2.0.0/lua/gitsigns/actions/000077500000000000000000000000001513053142700207045ustar00rootroot00000000000000neovim-gitsigns-2.0.0/lua/gitsigns/actions/blame.lua000066400000000000000000000363771513053142700225070ustar00rootroot00000000000000local async = require('gitsigns.async') local cache = require('gitsigns.cache').cache local log = require('gitsigns.debug.log') local util = require('gitsigns.util') local get_temp_hl = require('gitsigns.highlight').get_temp_hl local api = vim.api local hash_colors = {} --- @type table local ns = api.nvim_create_namespace('gitsigns_blame_win') local ns_hl = api.nvim_create_namespace('gitsigns_blame_win_hl') --- Convert a hex char to a rgb color component --- --- Taken from vim-fugitive: --- Avoid color components lower than 0x20 and higher than 0xdf to help --- avoid colors that blend into the background, light or dark. --- @param x string hex char --- @return integer local function mod(x) local y = assert(tonumber(x, 16)) return math.min(0xdf, 0x20 + math.floor((y * 0x10 + (15 - y)) * 0.75)) end --- Highlight a line in the blame window --- @param bufnr integer --- @param nsl integer --- @param lnum integer --- @param hl_group string local function hl_line(bufnr, nsl, lnum, hl_group) api.nvim_buf_set_extmark(bufnr, nsl, lnum - 1, 0, { end_row = lnum, hl_eol = true, end_col = 0, hl_group = hl_group, }) end --- Taken from vim-fugitive --- Use 3 characters of the commit hash, limiting the maximum total colors to --- 4,096. --- @param sha string --- @return string local function get_hash_color(sha) local r, g, b = sha:match('(%x)%x(%x)%x(%x)') assert(r and g and b, 'Invalid hash color') local color = mod(r) * 0x10000 + mod(g) * 0x100 + mod(b) if hash_colors[color] then return hash_colors[color] end local hl_name = string.format('GitSignsBlameColor.%s%s%s', r, g, b) api.nvim_set_hl(0, hl_name, { fg = color }) hash_colors[color] = hl_name return hl_name end --- Create a right-side extmark for the blame heatmap --- @param bufnr integer --- @param lnum integer (1-indexed) --- @param win_width integer --- @param min_time integer --- @param max_time integer --- @param author_time integer local function set_right_extmark(bufnr, lnum, win_width, min_time, max_time, author_time) api.nvim_buf_set_extmark(bufnr, ns, lnum - 1, 0, { virt_text_win_col = win_width, virt_text = { { '┃', get_temp_hl(min_time, max_time, author_time, 0.5, true), }, }, }) end ---@param amount integer ---@param text string ---@return string local function lalign(amount, text) local len = vim.fn.strdisplaywidth(text) return text .. string.rep(' ', math.max(0, amount - len)) end local chars = { first = '┍', mid = '│', last = '┕', single = '╺', } local M = {} --- @param blame Gitsigns.CacheEntry.Blame --- @param win integer --- @param main_win integer --- @param buf_sha? string --- @return table commit_lines local function render(blame, win, main_win, buf_sha) local max_author_len = 0 local entries = blame.entries for _, b in pairs(entries) do max_author_len = math.max(max_author_len, vim.fn.strdisplaywidth(b.commit.author)) end local lines = {} --- @type string[] local last_sha --- @type string? local cnt = 0 local commit_lines = {} --- @type table for i, b in pairs(entries) do local commit = b.commit local sha = commit.abbrev_sha local next_sha = entries[i + 1] and entries[i + 1].commit.abbrev_sha or nil if sha == last_sha then cnt = cnt + 1 local c = sha == next_sha and chars.mid or chars.last lines[i] = cnt == 1 and ('%s %s'):format(c, commit.summary) or c if commit_lines[i - 1] then assert(lines[i - 1], 'Previous line should exist') lines[i - 1] = chars.first .. lines[i - 1]:sub(#chars.single + 1) end else cnt = 0 commit_lines[i] = true lines[i] = ('%s %s %s %s'):format( chars.single, sha, lalign(max_author_len, commit.author), util.expand_format('', commit) ) end last_sha = sha end local win_width = #lines[1] api.nvim_win_set_width(win, win_width + 1) local bufnr = api.nvim_win_get_buf(win) local main_buf = api.nvim_win_get_buf(main_win) api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) local min_time, max_time = assert(cache[main_buf]):get_blame_times() -- Apply highlights for i, blame_info in ipairs(entries) do local hash_hl = get_hash_color(blame_info.commit.abbrev_sha) api.nvim_buf_set_extmark(bufnr, ns, i - 1, 0, { end_col = commit_lines[i] and 12 or 1, hl_group = hash_hl, }) set_right_extmark(bufnr, i, win_width, min_time, max_time, blame_info.commit.author_time) if not commit_lines[i] then api.nvim_buf_set_extmark(bufnr, ns, i - 1, 2, { end_row = i, end_col = 0, hl_group = 'Comment', }) end if buf_sha == blame_info.commit.sha then hl_line(bufnr, ns, i, '@markup.italic') end end return commit_lines end --- @async --- @param opts Gitsigns.BlameOpts? --- @param blame table --- @param win integer --- @param revision? string --- @param parent? boolean local function reblame(opts, blame, win, revision, parent) local blm_win = api.nvim_get_current_win() local lnum = api.nvim_win_get_cursor(blm_win)[1] local sha = assert(blame[lnum]).commit.sha if parent then sha = sha .. '^' end if sha == revision then return end vim.cmd.quit() api.nvim_set_current_win(win) local did_attach = require('gitsigns.actions.diffthis').show(nil, sha) if not did_attach then return end async.schedule() M.blame(opts) end --- @async --- @param win integer --- @param bwin integer --- @param open 'vsplit'|'tabnew' --- @param bcache Gitsigns.CacheEntry local function show_commit(win, bwin, open, bcache) local cursor = api.nvim_win_get_cursor(bwin)[1] local blame = assert(bcache.blame) local sha = assert(blame.entries[cursor]).commit.sha api.nvim_set_current_win(win) require('gitsigns.actions.show_commit')(sha, open) end --- @param augroup integer --- @param wins integer[] local function sync_cursors(augroup, wins) local cursor_save --- @type integer? ---@param w integer local function sync_cursor(w) local b = api.nvim_win_get_buf(w) api.nvim_create_autocmd('BufLeave', { buffer = b, group = augroup, callback = function() if api.nvim_win_is_valid(w) then cursor_save = api.nvim_win_get_cursor(w)[1] end end, }) api.nvim_create_autocmd('BufEnter', { group = augroup, buffer = b, callback = function() if not api.nvim_win_is_valid(w) then return end local cur_cursor, cur_cursor_col = unpack(api.nvim_win_get_cursor(w)) if cursor_save and cursor_save ~= cur_cursor then api.nvim_win_set_cursor(w, { cursor_save, vim.o.startofline and 0 or cur_cursor_col }) end end, }) end for _, w in ipairs(wins) do sync_cursor(w) end end --- @param name string --- @param items [string, string][] local function menu(name, items) local max_len = 0 for _, item in ipairs(items) do max_len = math.max(max_len, #item[1]) --- @type integer end for _, item in ipairs(items) do local item_nm, action = item[1], item[2] local pad = string.rep(' ', max_len - #item_nm) local lhs = string.format('%s%s (%s)', item_nm, pad, action):gsub(' ', [[\ ]]) local cmd = string.format('nmenu ]%s.%s %s', name, lhs, action) vim.cmd(cmd) end end --- @param bufnr integer --- @param blm_win integer --- @param blame table --- @param commit_lines table local function on_cursor_moved(bufnr, blm_win, blame, commit_lines) local blm_bufnr = api.nvim_get_current_buf() local lnum = api.nvim_win_get_cursor(blm_win)[1] local cur_sha = assert(blame[lnum]).commit.abbrev_sha for i, info in pairs(blame) do if info.commit.abbrev_sha == cur_sha then hl_line(blm_bufnr, ns_hl, i, 'CursorLine') hl_line(blm_bufnr, ns_hl, i, '@markup.strong') hl_line(bufnr, ns_hl, i, 'CursorLine') end end if commit_lines[lnum] and commit_lines[lnum + 1] then local blame_info = assert(blame[lnum]) local hash_hl = get_hash_color(blame_info.commit.abbrev_sha) api.nvim_buf_set_extmark(blm_bufnr, ns_hl, lnum - 1, 0, { virt_text = { { chars.first, hash_hl } }, virt_text_pos = 'overlay', }) api.nvim_buf_set_extmark(blm_bufnr, ns_hl, lnum - 1, 0, { virt_lines = { { { chars.last, hash_hl }, { ' ' }, { blame_info.commit.summary, 'Comment' } }, }, }) local fillchar = string.rep(vim.opt.fillchars:get().diff or '-', 1000) api.nvim_buf_set_extmark(bufnr, ns_hl, lnum - 1, 0, { virt_lines = { { { fillchar, 'Comment' } } }, virt_lines_leftcol = true, }) end end --- @async --- @param bufnr integer --- @param blm_win integer --- @param blame table local function diff(bufnr, blm_win, blame) local lnum = api.nvim_win_get_cursor(blm_win)[1] local info = assert(blame[lnum]) vim.cmd.tabnew() api.nvim_set_current_buf(bufnr) require('gitsigns.actions.diffthis').show(bufnr, info.commit.sha, info.filename) if info.previous_sha then require('gitsigns.actions').diffthis(info.previous_sha) end end --- Update the right-side extmarks when the window is resized --- @param blm_bufnr integer --- @param blm_win integer --- @param entries table --- @param min_time integer --- @param max_time integer local function update_right_extmarks(blm_bufnr, blm_win, entries, min_time, max_time) if not api.nvim_win_is_valid(blm_win) or not api.nvim_buf_is_valid(blm_bufnr) then return end -- Get the actual window width (not the content width) -- Subtract 1 because virt_text_win_col is 0-indexed local win_width = api.nvim_win_get_width(blm_win) - 1 -- Get all extmarks and delete only those with virt_text_win_col -- These are the right-side heatmap indicators that need repositioning local extmarks = api.nvim_buf_get_extmarks(blm_bufnr, ns, 0, -1, { details = true }) for _, ext in ipairs(extmarks) do local id, row, col, details = ext[1], ext[2], ext[3], ext[4] if details.virt_text_win_col then api.nvim_buf_del_extmark(blm_bufnr, ns, id) end end -- Recreate the right-side extmarks with updated position for i, blame_info in ipairs(entries) do set_right_extmark(blm_bufnr, i, win_width, min_time, max_time, blame_info.commit.author_time) end end --- @param mode string --- @param lhs string --- @param cb fun() --- @param opts vim.keymap.set.Opts local function pmap(mode, lhs, cb, opts) opts.expr = true vim.keymap.set(mode, lhs, function() vim.schedule(function() cb() end) return '' end, opts) end --- @async --- @param opts Gitsigns.BlameOpts? function M.blame(opts) local bufnr = api.nvim_get_current_buf() local win = api.nvim_get_current_win() local bcache = cache[bufnr] if not bcache then log.dprint('Not attached') return end local lnum = nil bcache:get_blame(lnum, opts) local blame = assert(bcache.blame) -- Save position to align 'scrollbind' local top = vim.fn.line('w0') + vim.wo.scrolloff local current = vim.fn.line('.') vim.cmd.vsplit({ mods = { keepalt = true, split = 'aboveleft' } }) local blm_win = api.nvim_get_current_win() local blm_bufnr = api.nvim_create_buf(false, true) api.nvim_win_set_buf(blm_win, blm_bufnr) api.nvim_buf_set_name(blm_bufnr, (bcache:get_rev_bufname():gsub('^gitsigns:', 'gitsigns-blame:'))) local commit_lines = render(blame, blm_win, win, bcache.git_obj.revision) local blm_bo = vim.bo[blm_bufnr] blm_bo.buftype = 'nofile' blm_bo.bufhidden = 'wipe' blm_bo.modifiable = false blm_bo.filetype = 'gitsigns-blame' local blm_wlo = vim.wo[blm_win][0] blm_wlo.foldcolumn = '0' blm_wlo.foldenable = false blm_wlo.number = false blm_wlo.relativenumber = false blm_wlo.scrollbind = true blm_wlo.signcolumn = 'no' blm_wlo.spell = false blm_wlo.winfixwidth = true blm_wlo.wrap = false blm_wlo.list = false if vim.wo[win].winbar ~= '' and blm_wlo.winbar == '' then local name = api.nvim_buf_get_name(bufnr) blm_wlo.winbar = vim.fn.fnamemodify(name, ':.') end if vim.fn.exists('&winfixbuf') == 1 then blm_wlo.winfixbuf = true end vim.cmd(tostring(top)) vim.cmd('normal! zt') vim.cmd(tostring(current)) vim.cmd('normal! 0') local cur_wlo = vim.wo[win][0] local cur_orig_wlo = { cur_wlo.foldenable, cur_wlo.scrollbind, cur_wlo.wrap } cur_wlo.foldenable = false cur_wlo.scrollbind = true cur_wlo.wrap = false vim.cmd.redraw() vim.cmd.syncbind() vim.keymap.set('n', '', function() vim.cmd.popup(']GitsignsBlame') end, { desc = 'Open blame context menu', buffer = blm_bufnr, }) pmap('n', 'r', function() async.run(reblame, opts, blame.entries, win, bcache.git_obj.revision):raise_on_error() end, { desc = 'Reblame at commit', buffer = blm_bufnr, }) pmap('n', 'd', function() async.run(diff, bufnr, blm_win, blame.entries):raise_on_error() end, { desc = 'Diff (tab)', buffer = blm_bufnr, }) pmap('n', 'R', function() async.run(reblame, opts, blame.entries, win, bcache.git_obj.revision, true):raise_on_error() end, { desc = 'Reblame at commit parent', buffer = blm_bufnr, }) pmap('n', 's', function() async.run(show_commit, win, blm_win, 'vsplit', bcache):raise_on_error() end, { desc = 'Show commit in a vertical split', buffer = blm_bufnr, }) pmap('n', 'S', function() async.run(show_commit, win, blm_win, 'tabnew', bcache):raise_on_error() end, { desc = 'Show commit in a new tab', buffer = blm_bufnr, }) menu('GitsignsBlame', { { 'Reblame at commit', 'r' }, { 'Reblame at commit parent', 'R' }, { 'Diff (tab)', 'd' }, { 'Show commit (vsplit)', 's' }, { ' (tab)', 'S' }, }) local group = api.nvim_create_augroup('GitsignsBlame', {}) api.nvim_create_autocmd({ 'BufHidden', 'QuitPre' }, { buffer = bufnr, group = group, once = true, callback = function() if api.nvim_win_is_valid(blm_win) then api.nvim_win_close(blm_win, true) end end, }) api.nvim_create_autocmd({ 'CursorMoved', 'BufLeave' }, { buffer = blm_bufnr, group = group, callback = function() api.nvim_buf_clear_namespace(blm_bufnr, ns_hl, 0, -1) if api.nvim_buf_is_valid(bufnr) then api.nvim_buf_clear_namespace(bufnr, ns_hl, 0, -1) end end, }) -- Highlight the same commit under the cursor api.nvim_create_autocmd('CursorMoved', { buffer = blm_bufnr, group = group, callback = function() on_cursor_moved(bufnr, blm_win, blame.entries, commit_lines) end, }) -- Update right-side extmarks on window resize api.nvim_create_autocmd('WinResized', { buffer = blm_bufnr, group = group, callback = function() local min_time, max_time = assert(cache[bufnr]):get_blame_times() update_right_extmarks(blm_bufnr, blm_win, blame.entries, min_time, max_time) end, }) api.nvim_create_autocmd('WinClosed', { pattern = tostring(blm_win), group = group, callback = function() api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) if api.nvim_win_is_valid(win) then cur_wlo.foldenable, cur_wlo.scrollbind, cur_wlo.wrap = unpack(cur_orig_wlo) end end, }) sync_cursors(group, { win, blm_win }) end return M neovim-gitsigns-2.0.0/lua/gitsigns/actions/blame_line.lua000066400000000000000000000137041513053142700235030ustar00rootroot00000000000000local Hunks = require('gitsigns.hunks') local cache = require('gitsigns.cache').cache local config = require('gitsigns.config').config local log = require('gitsigns.debug.log') local popup = require('gitsigns.popup') local run_diff = require('gitsigns.diff') local util = require('gitsigns.util') local api = vim.api --- @async --- @param repo Gitsigns.Repo --- @param info Gitsigns.BlameInfoPublic --- @return Gitsigns.Hunk.Hunk hunk --- @return integer hunk_index --- @return integer num_hunks --- @return integer? guess_offset If the hunk was not found at the exact line, --- return the offset from the original line to the --- hunk start. local function get_blame_hunk(repo, info) local a = repo:get_show_text(info.previous_sha .. ':' .. info.previous_filename) local b = repo:get_show_text(info.sha .. ':' .. info.filename) local hunks = run_diff(a, b, false) local hunk, i = Hunks.find_hunk(info.orig_lnum, hunks) if hunk and i then return hunk, i, #hunks end -- git-blame output is not always correct (see #1332) -- Find the closest hunk to the original line log.dprintf('Could not find hunk using hunk info %s', vim.inspect(info)) local i_next = Hunks.find_nearest_hunk(info.orig_lnum, hunks, 'next') local i_prev = Hunks.find_nearest_hunk(info.orig_lnum, hunks, 'prev') if i_next and i_prev then -- if there is hunk before and after, find the closest local dist_n = math.abs(assert(hunks[i_next]).added.start - info.orig_lnum) local dist_p = math.abs(assert(hunks[i_prev]).added.start - info.orig_lnum) i = dist_n < dist_p and i_next or i_prev else i = assert(i_next or i_prev, 'no hunks in commit') end hunk = assert(hunks[i]) return hunk, i, #hunks, hunk.added.start - info.orig_lnum end --- @async --- @param repo Gitsigns.Repo --- @param sha string --- @return Gitsigns.LineSpec local function create_commit_msg_body_linespec(repo, sha) local body0 = repo:command({ 'show', '-s', '--format=%B', sha }, { text = true }) local body = table.concat(body0, '\n') return { { body, 'NormalFloat' } } end --- @async --- @param info Gitsigns.BlameInfoPublic --- @param repo Gitsigns.Repo --- @param fileformat string --- @return Gitsigns.LineSpec[] local function create_blame_hunk_linespec(repo, info, fileformat) if not (info.previous_sha and info.previous_filename) then return { { { 'File added in commit', 'Title' } } } end --- @type Gitsigns.LineSpec[] local ret = {} local hunk, hunk_no, num_hunks, guess_offset = get_blame_hunk(repo, info) local hunk_title = { { ('Hunk %d of %d'):format(hunk_no, num_hunks), 'Title' }, { ' ' .. hunk.head, 'LineNr' }, } if guess_offset then hunk_title[#hunk_title + 1] = { (' (guessed: %s%d offset from original line)'):format( guess_offset >= 0 and '+' or '', guess_offset ), 'WarningMsg', } end ret[#ret + 1] = hunk_title vim.list_extend(ret, Hunks.linespec_for_hunk(hunk, fileformat)) return ret end --- @async --- @param full boolean? Whether to show the full commit message and hunk --- @param result Gitsigns.BlameInfoPublic --- @param repo Gitsigns.Repo --- @param fileformat string --- @param with_gh boolean --- @return Gitsigns.LineSpec[] local function create_blame_linespec(full, result, repo, fileformat, with_gh) local is_committed = result.sha and tonumber('0x' .. result.sha) ~= 0 if not is_committed then return { { { result.author, 'Label' } }, } end local gh --- @module 'gitsigns.gh'? if config.gh and with_gh then gh = require('gitsigns.gh') end local commit_url = gh and gh.commit_url(result.sha, repo.toplevel) or nil --- @type Gitsigns.LineSpec local title = { { result.abbrev_sha, 'Directory', commit_url }, { ' ', 'NormalFloat' }, } if gh then vim.list_extend(title, gh.create_pr_linespec(result.sha, repo.toplevel)) end vim.list_extend(title, { { result.author .. ' ', 'MoreMsg' }, { util.expand_format('()', result), 'Label' }, { ':', 'NormalFloat' }, }) --- @type Gitsigns.LineSpec[] local ret = { title } if not full then ret[#ret + 1] = { { result.summary, 'NormalFloat' } } return ret end ret[#ret + 1] = create_commit_msg_body_linespec(repo, result.sha) vim.list_extend(ret, create_blame_hunk_linespec(repo, result, fileformat)) return ret end --- @class (exact) Gitsigns.LineBlameOpts : Gitsigns.BlameOpts --- @field full? boolean --- @async --- @param opts Gitsigns.LineBlameOpts? return function(opts) if popup.focus_open('blame') then return end opts = opts or {} local bufnr = api.nvim_get_current_buf() local bcache = cache[bufnr] if not bcache then return end local loading = vim.defer_fn(function() popup.create({ { { 'Loading...', 'Title' } } }, config.preview_config) end, 1000) if not bcache:schedule() then return end local fileformat = vim.bo[bufnr].fileformat local lnum = api.nvim_win_get_cursor(0)[1] local popup_winid, popup_bufnr ---@async local function is_stale() return not bcache:schedule() or api.nvim_get_current_buf() ~= popup_bufnr and (api.nvim_get_current_buf() ~= bufnr or api.nvim_win_get_cursor(0)[1] ~= lnum) end local info = bcache:get_blame(lnum, opts) pcall(function() loading:close() end) if is_stale() then return end local result = util.convert_blame_info(assert(info)) local blame_linespec = create_blame_linespec(opts.full, result, bcache.git_obj.repo, fileformat, false) if is_stale() then return end popup_winid, popup_bufnr = popup.create(blame_linespec, config.preview_config, 'blame') blame_linespec = create_blame_linespec(opts.full, result, bcache.git_obj.repo, fileformat, true) if is_stale() then return end if api.nvim_win_is_valid(popup_winid) and api.nvim_buf_is_valid(popup_bufnr) then popup.update(popup_winid, popup_bufnr, blame_linespec, config.preview_config) end end neovim-gitsigns-2.0.0/lua/gitsigns/actions/diffthis.lua000066400000000000000000000171151513053142700232140ustar00rootroot00000000000000local async = require('gitsigns.async') local config = require('gitsigns.config').config local manager = require('gitsigns.manager') local message = require('gitsigns.message') local util = require('gitsigns.util') local Status = require('gitsigns.status') local cache = require('gitsigns.cache').cache local log = require('gitsigns.debug.log') local throttle_async = require('gitsigns.debounce').throttle_async local api = vim.api local M = {} --- @async --- @param bufnr integer --- @param dbufnr integer --- @param base string? --- @param relpath string? local function bufread(bufnr, dbufnr, base, relpath) local bcache = assert(cache[bufnr]) base = util.norm_base(base) local text --- @type string[] if base == bcache.git_obj.revision then text = assert(bcache.compare_text) else local err text, err = bcache.git_obj:get_show_text(base, relpath) if err then error(err, 2) end async.schedule() if not api.nvim_buf_is_valid(bufnr) then return end end -- TODO(lewis6991): This doesn't work if the buffer is for a different file -- from bufnr. This function should take a repo object instead. vim.bo[dbufnr].fileformat = vim.bo[bufnr].fileformat vim.bo[dbufnr].filetype = vim.filetype.match({ buf = dbufnr }) vim.bo[dbufnr].bufhidden = 'wipe' local modifiable = vim.bo[dbufnr].modifiable vim.bo[dbufnr].modifiable = true Status.update(dbufnr, { head = base }) util.set_lines(dbufnr, 0, -1, text) vim.bo[dbufnr].modifiable = modifiable vim.bo[dbufnr].modified = false -- TODO(lewis6991): make this blocking require('gitsigns.attach').attach(dbufnr, nil, 'BufReadCmd') end --- @async --- @param bufnr integer --- @param dbufnr integer --- @param base string? local function bufwrite(bufnr, dbufnr, base) local bcache = assert(cache[bufnr]) local buftext = util.buf_lines(dbufnr) base = util.norm_base(base) bcache.git_obj:lock(function() bcache.git_obj:stage_lines(buftext) end) async.schedule() if not api.nvim_buf_is_valid(bufnr) then return end vim.bo[dbufnr].modified = false -- If diff buffer base matches the git_obj revision then also update the -- signs. if base == bcache.git_obj.revision then bcache.compare_text = buftext manager.update(bufnr) end end --- @async --- Create a gitsigns buffer for a certain revision of a file --- @param bufnr integer --- @param base string? --- @param relpath string? --- @return string? bufname Buffer name --- @return integer? bufnr Buffer number local function create_revision_buf(bufnr, base, relpath) local bcache = assert(cache[bufnr]) base = util.norm_base(base) local bufname = bcache:get_rev_bufname(base, relpath) if util.bufexists(bufname) then return bufname, vim.fn.bufnr(bufname) end local dbuf = api.nvim_create_buf(false, true) api.nvim_buf_set_name(dbuf, bufname) local ok, err = pcall(bufread, bufnr, dbuf, base, relpath) if not ok then message.error(err --[[@as string]]) async.schedule() api.nvim_buf_delete(dbuf, { force = true }) return end -- allow editing the index revision if not base then vim.bo[dbuf].buftype = 'acwrite' api.nvim_create_autocmd('BufReadCmd', { group = 'gitsigns', buffer = dbuf, callback = function() async.run(bufread, bufnr, dbuf, base, relpath):raise_on_error() end, }) api.nvim_create_autocmd('BufWriteCmd', { group = 'gitsigns', buffer = dbuf, callback = function() async.run(bufwrite, bufnr, dbuf, base):raise_on_error() end, }) else vim.bo[dbuf].buftype = 'nowrite' vim.bo[dbuf].modifiable = false end return bufname, dbuf end --- @async --- @param base string? --- @param opts? Gitsigns.DiffthisOpts local function diffthis_rev(base, opts) local bufnr = api.nvim_get_current_buf() local bufname, dbuf = create_revision_buf(bufnr, base) if not bufname then return end opts = opts or {} local cwin = api.nvim_get_current_win() vim.cmd.diffsplit({ bufname, mods = { vertical = opts.vertical, split = opts.split or config.diffthis.split, keepalt = true, }, }) api.nvim_set_current_win(cwin) -- Reset 'diff' option for the current window if the diff buffer is hidden api.nvim_create_autocmd('BufHidden', { buffer = assert(dbuf), callback = function() local tabpage = api.nvim_win_get_tabpage(cwin) local disable_cwin_diff = true for _, w in ipairs(api.nvim_tabpage_list_wins(tabpage)) do if w ~= cwin and vim.wo[w].diff then -- If there is another diff window open, don't disable diff disable_cwin_diff = false break end end if disable_cwin_diff then vim.wo[cwin].diff = false end end, }) end --- @async --- @param base string? --- @param opts Gitsigns.DiffthisOpts function M.diffthis(base, opts) if vim.wo.diff then log.dprint('diff is disabled') return end local bufnr = api.nvim_get_current_buf() local bcache = cache[bufnr] if not bcache then log.dprintf('buffer %d is not attached', bufnr) return end if not base and bcache.git_obj.has_conflicts then diffthis_rev(':2', opts) opts.split = 'belowright' diffthis_rev(':3', opts) else diffthis_rev(base, opts) end end --- @async --- @param bufnr integer? --- @param base string? --- @param relpath string? --- @return boolean did_attach function M.show(bufnr, base, relpath) bufnr = bufnr or api.nvim_get_current_buf() if not cache[bufnr] then print('Error: Buffer is not attached.') return false end local bufname = create_revision_buf(bufnr, base, relpath) if not bufname then log.dprint('No bufname for revision ' .. base) return false end log.dprint('bufname ' .. bufname) vim.cmd.edit(bufname) -- Wait for the buffer to attach in case the user passes a callback that -- requires the buffer to be attached. local sbufnr = api.nvim_get_current_buf() local attached = vim.wait(2000, function() return cache[sbufnr] ~= nil end) if not attached then log.eprintf("Show buffer '%s' did not attach", bufname) return false end return true end --- @async --- @param bufnr integer --- @return boolean local function should_reload(bufnr) if not vim.bo[bufnr].modified then return true end local response --- @type string? while not vim.tbl_contains({ 'O', 'L' }, response) do response = async.await(2, vim.ui.input, { prompt = 'Warning: The git index has changed and the buffer was changed as well. [O]K, (L)oad File:', }) end return response == 'L' end --- @param name string --- @return boolean local function is_fugitive_diff_window(name) return vim.startswith(name, 'fugitive://') and vim.fn.exists('*FugitiveParse') == 1 and vim.fn.FugitiveParse(name)[1] ~= ':' end --- This function needs to be throttled as there is a call to vim.ui.input --- @param bufnr integer M.update = throttle_async({ hash = 1, schedule = true }, function(bufnr) if not vim.wo.diff then return end -- Note this will be the bufname for the currently set base -- which are the only ones we want to update local bufname = assert(cache[bufnr]):get_rev_bufname() for _, w in ipairs(api.nvim_list_wins()) do if api.nvim_win_is_valid(w) then local b = api.nvim_win_get_buf(w) local bname = api.nvim_buf_get_name(b) if bname == bufname or is_fugitive_diff_window(bname) then if should_reload(b) then api.nvim_buf_call(b, function() vim.cmd.doautocmd('BufReadCmd') vim.cmd.diffthis() end) end end end end end) return M neovim-gitsigns-2.0.0/lua/gitsigns/actions/nav.lua000066400000000000000000000114151513053142700221750ustar00rootroot00000000000000local async = require('gitsigns.async') local Hunks = require('gitsigns.hunks') local cache = require('gitsigns.cache').cache local Popup = require('gitsigns.popup') local api = vim.api --- @class Gitsigns.NavOpts --- Whether to loop around file or not. Defaults --- to the value 'wrapscan' --- @field wrap boolean --- Expand folds when navigating to a hunk which is --- inside a fold. Defaults to `true` if 'foldopen' --- contains `search`. --- @field foldopen boolean --- Whether to show navigation messages or not. --- Looks at 'shortmess' for default behaviour. --- @field navigation_message boolean --- Only navigate between non-contiguous hunks. Only useful if --- 'diff_opts' contains `linematch`. Defaults to `true`. --- @field greedy boolean --- Automatically open preview_hunk() upon navigating --- to a hunk. --- @field preview? boolean --- Number of times to advance. Defaults to |v:count1|. --- @field count integer --- Which kinds of hunks to target. Defaults to `'unstaged'`. --- @field target 'unstaged'|'staged'|'all' --- @class gitsigns.nav local M = {} --- @param x string --- @param word string --- @return boolean local function findword(x, word) return string.find(x, '%f[%w_]' .. word .. '%f[^%w_]') ~= nil end --- @param opts? Gitsigns.NavOpts --- @return Gitsigns.NavOpts local function process_nav_opts(opts) opts = opts or {} -- show navigation message if opts.navigation_message == nil then opts.navigation_message = vim.o.shortmess:find('S') == nil end -- wrap around if opts.wrap == nil then opts.wrap = vim.o.wrapscan end if opts.foldopen == nil then opts.foldopen = findword(vim.o.foldopen, 'search') end if opts.greedy == nil then opts.greedy = true end if opts.count == nil then opts.count = vim.v.count1 end if opts.target == nil then opts.target = 'unstaged' end return opts end --- @async --- @param bufnr integer --- @param target 'unstaged'|'staged'|'all' --- @param greedy boolean --- @return Gitsigns.Hunk.Hunk[] local function get_nav_hunks(bufnr, target, greedy) local bcache = assert(cache[bufnr]) local hunks_main = bcache:get_hunks(greedy, false) or {} local hunks --- @type Gitsigns.Hunk.Hunk[] if target == 'unstaged' then hunks = hunks_main else local hunks_head = bcache:get_hunks(greedy, true) or {} hunks_head = Hunks.filter_common(hunks_head, hunks_main) or {} if target == 'all' then hunks = hunks_main vim.list_extend(hunks, hunks_head) table.sort(hunks, function(h1, h2) return h1.added.start < h2.added.start end) elseif target == 'staged' then hunks = hunks_head end end return hunks end --- @async --- @param direction 'first'|'last'|'next'|'prev' --- @param opts? Gitsigns.NavOpts function M.nav_hunk(direction, opts) opts = process_nav_opts(opts) local bufnr = api.nvim_get_current_buf() local bcache = cache[bufnr] if not bcache then return end local hunks = get_nav_hunks(bufnr, opts.target, opts.greedy) if not hunks or vim.tbl_isempty(hunks) then if opts.navigation_message then api.nvim_echo({ { 'No hunks', 'WarningMsg' } }, false, {}) end return end local line = api.nvim_win_get_cursor(0)[1] --[[@as integer]] local index --- @type integer? local forwards = direction == 'next' or direction == 'last' for _ = 1, opts.count do index = Hunks.find_nearest_hunk(line, hunks, direction, opts.wrap) if not index then if opts.navigation_message then api.nvim_echo({ { 'No more hunks', 'WarningMsg' } }, false, {}) end local _, col = vim.fn.getline(line):find('^%s*') --- @cast col -? api.nvim_win_set_cursor(0, { line, col }) return end local hunk = assert(hunks[index]) line = forwards and hunk.added.start or hunk.vend end -- Check if preview popup is open before moving the cursor local should_preview = opts.preview or Popup.is_open('hunk') ~= nil -- Handle topdelete line = math.max(line, 1) vim.cmd([[ normal! m' ]]) -- add current cursor position to the jump list local _, col = vim.fn.getline(line):find('^%s*') --- @cast col -? api.nvim_win_set_cursor(0, { line, col }) if opts.foldopen then vim.cmd('silent! foldopen!') end -- schedule so the cursor change can settle, otherwise the popup might -- appear in the old position async.schedule() local Preview = require('gitsigns.actions.preview') if should_preview then -- Close the popup in case one is open which will cause it to focus the -- popup Popup.close('hunk') Preview.preview_hunk() elseif Preview.has_preview_inline(bufnr) then Preview.preview_hunk_inline() end if index and opts.navigation_message then api.nvim_echo({ { ('Hunk %d of %d'):format(index, #hunks), 'None' } }, false, {}) end end return M neovim-gitsigns-2.0.0/lua/gitsigns/actions/preview.lua000066400000000000000000000171711513053142700230770ustar00rootroot00000000000000local cache = require('gitsigns.cache').cache local config = require('gitsigns.config').config local popup = require('gitsigns.popup') local Hunks = require('gitsigns.hunks') local api = vim.api local current_buf = api.nvim_get_current_buf --- @class gitsigns.preview local M = {} local ns_inline = api.nvim_create_namespace('gitsigns_preview_inline') --- @async --- @param bufnr integer --- @param greedy? boolean --- @return Gitsigns.Hunk.Hunk? hunk --- @return boolean? staged local function get_hunk_with_staged(bufnr, greedy) local bcache = cache[bufnr] if not bcache then return end local hunk = bcache:get_hunk(nil, greedy, false) if hunk then return hunk, false end hunk = bcache:get_hunk(nil, greedy, true) if hunk then return hunk, true end end local function clear_preview_inline(bufnr) api.nvim_buf_clear_namespace(bufnr, ns_inline, 0, -1) end --- @param keys string local function feedkeys(keys) local cy = api.nvim_replace_termcodes(keys, true, false, true) api.nvim_feedkeys(cy, 'n', false) end --- @param win integer --- @param lnum integer --- @param width integer --- @return string str --- @return {group:string, start:integer}[]? highlights local function build_lno_str(win, lnum, width) local has_col, statuscol = pcall(api.nvim_get_option_value, 'statuscolumn', { win = win, scope = 'local' }) if has_col and statuscol and statuscol ~= '' then --- @cast statuscol string local ok, data = pcall(api.nvim_eval_statusline, statuscol, { winid = win, use_statuscol_lnum = lnum, highlights = true, }) if ok then local data_str = data.str --[[@as string]] return data_str, data.highlights end end return string.format('%' .. width .. 'd', lnum) end --- @param bufnr integer --- @param nsw integer --- @param hunk Gitsigns.Hunk.Hunk local function show_added(bufnr, nsw, hunk) local start_row = hunk.added.start - 1 for offset = 0, hunk.added.count - 1 do local row = start_row + offset api.nvim_buf_set_extmark(bufnr, nsw, row, 0, { end_row = row + 1, hl_group = 'GitSignsAddPreview', hl_eol = true, priority = 1000, }) end local _, added_regions = require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines) for _, region in ipairs(added_regions) do local offset, rtype, scol, ecol = region[1] - 1, region[2], region[3] - 1, region[4] - 1 -- Special case to handle cr at eol in buffer but not in show text local cr_at_eol_change = rtype == 'change' and vim.endswith(assert(hunk.added.lines[offset + 1]), '\r') api.nvim_buf_set_extmark(bufnr, nsw, start_row + offset, scol, { end_col = ecol, strict = not cr_at_eol_change, hl_group = rtype == 'add' and 'GitSignsAddInline' or rtype == 'change' and 'GitSignsChangeInline' or 'GitSignsDeleteInline', priority = 1001, }) end end --- @param bufnr integer --- @param nsd integer --- @param hunk Gitsigns.Hunk.Hunk --- @param staged boolean? --- @return integer winid local function show_deleted_in_float(bufnr, nsd, hunk, staged) local cwin = api.nvim_get_current_win() local virt_lines = {} --- @type [string, string][][] local textoff = assert(vim.fn.getwininfo(cwin)[1]).textoff --[[@as integer]] for i = 1, hunk.removed.count do local sc = build_lno_str(cwin, hunk.removed.start + i, textoff - 1) virt_lines[i] = { { sc, 'LineNr' } } end local topdelete = hunk.added.start == 0 and hunk.type == 'delete' local virt_lines_above = hunk.type ~= 'delete' or topdelete local row = topdelete and 0 or hunk.added.start - 1 api.nvim_buf_set_extmark(bufnr, nsd, row, -1, { virt_lines = virt_lines, -- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166 virt_lines_above = virt_lines_above, virt_lines_leftcol = true, }) local bcache = assert(cache[bufnr]) local pbufnr = api.nvim_create_buf(false, true) local text = staged and bcache.compare_text_head or bcache.compare_text api.nvim_buf_set_lines(pbufnr, 0, -1, false, assert(text)) local width = api.nvim_win_get_width(0) local bufpos_offset = virt_lines_above and not topdelete and 1 or 0 local pwinid = api.nvim_open_win(pbufnr, false, { relative = 'win', win = cwin, width = width - textoff, height = hunk.removed.count, anchor = 'SW', bufpos = { hunk.added.start - bufpos_offset, 0 }, style = 'minimal', border = 'none', }) vim.bo[pbufnr].filetype = vim.bo[bufnr].filetype vim.bo[pbufnr].bufhidden = 'wipe' vim.wo[pwinid].scrolloff = 0 api.nvim_win_call(pwinid, function() -- Disable folds vim.wo.foldenable = false -- Navigate to hunk vim.cmd('normal! ' .. tostring(hunk.removed.start) .. 'gg') vim.cmd('normal! ' .. api.nvim_replace_termcodes('z', true, false, true)) end) -- Apply highlights for i = hunk.removed.start, hunk.removed.start + hunk.removed.count - 1 do api.nvim_buf_set_extmark(pbufnr, nsd, i - 1, 0, { hl_group = 'GitSignsDeleteVirtLn', hl_eol = true, end_row = i, priority = 1000, }) end local removed_regions = require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines) for _, region in ipairs(removed_regions) do local start_row = (hunk.removed.start - 1) + (region[1] - 1) local start_col = region[3] - 1 local end_col = region[4] - 1 api.nvim_buf_set_extmark(pbufnr, nsd, start_row, start_col, { hl_group = 'GitSignsDeleteVirtLnInline', end_col = end_col, end_row = start_row, priority = 1001, }) end return pwinid end local function noautocmd(f) return function() local ei = vim.o.eventignore vim.o.eventignore = 'all' f() vim.o.eventignore = ei end end --- Preview the hunk at the cursor position in a floating --- window. If the preview is already open, calling this --- will cause the window to get focus. M.preview_hunk = noautocmd(function() -- Wrap in noautocmd so vim-repeat continues to work if popup.focus_open('hunk') then return end local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end local hunk, index = bcache:get_cursor_hunk() if not hunk then return end --- @type Gitsigns.LineSpec[] local preview_linespec = { { { ('Hunk %d of %d'):format(index, #bcache.hunks), 'Title' } }, } vim.list_extend(preview_linespec, Hunks.linespec_for_hunk(hunk, vim.bo[bufnr].fileformat)) popup.create(preview_linespec, config.preview_config, 'hunk') end) --- Preview the hunk at the cursor position inline in the buffer. --- @async --- @return integer? winid function M.preview_hunk_inline() local bufnr = current_buf() local hunk, staged = get_hunk_with_staged(bufnr, true) if not hunk then return end clear_preview_inline(bufnr) local winid --- @type integer show_added(bufnr, ns_inline, hunk) if hunk.removed.count > 0 then winid = show_deleted_in_float(bufnr, ns_inline, hunk, staged) end api.nvim_create_autocmd({ 'CursorMoved', 'InsertEnter', 'BufLeave' }, { buffer = bufnr, desc = 'Clear gitsigns inline preview', callback = function() if winid then pcall(api.nvim_win_close, winid, true) end clear_preview_inline(bufnr) end, once = true, }) -- Virtual lines will be hidden if they are placed on the top row, so -- automatically scroll the viewport. if hunk.added.start <= 1 then feedkeys(hunk.removed.count .. '') end return winid end --- @param bufnr integer --- @return boolean function M.has_preview_inline(bufnr) return #api.nvim_buf_get_extmarks(bufnr, ns_inline, 0, -1, { limit = 1 }) > 0 end return M neovim-gitsigns-2.0.0/lua/gitsigns/actions/qflist.lua000066400000000000000000000073451513053142700227220ustar00rootroot00000000000000local async = require('gitsigns.async') local cache = require('gitsigns.cache').cache local git = require('gitsigns.git') local run_diff = require('gitsigns.diff') local config = require('gitsigns.config').config local util = require('gitsigns.util') local uv = vim.uv or vim.loop ---@diagnostic disable-line: deprecated local current_buf = vim.api.nvim_get_current_buf --- @class gitsigns.qflist local M = {} --- @param buf_or_filename string|integer --- @param hunks Gitsigns.Hunk.Hunk[] --- @param qflist table[] local function hunks_to_qflist(buf_or_filename, hunks, qflist) for _, hunk in ipairs(hunks) do local kind = hunk.type == 'add' and 'Added' or hunk.type == 'delete' and 'Removed' or 'Changed' local header = ('-%s%s +%s%s'):format( hunk.removed.start, hunk.removed.count ~= 1 and ',' .. tostring(hunk.removed.count) or '', hunk.added.start, hunk.added.count ~= 1 and ',' .. tostring(hunk.added.count) or '' ) local text = ('%-7s (%s): %s'):format( kind, header, hunk.added.lines[1] or hunk.removed.lines[1] ) qflist[#qflist + 1] = { bufnr = type(buf_or_filename) == 'number' and buf_or_filename or nil, filename = type(buf_or_filename) == 'string' and buf_or_filename or nil, lnum = hunk.added.start, text = text, } end end --- @async --- @param target 'all'|'attached'|integer? --- @return table[]? local function buildqflist(target) target = target or current_buf() if target == 0 then target = current_buf() end local qflist = {} --- @type table[] if type(target) == 'number' then local bufnr = target local bcache = cache[bufnr] if not bcache or not bcache.hunks then return end hunks_to_qflist(bufnr, bcache.hunks, qflist) elseif target == 'attached' then for bufnr, bcache in pairs(cache) do hunks_to_qflist(bufnr, assert(bcache.hunks), qflist) end elseif target == 'all' then local repos = {} --- @type table for _, bcache in pairs(cache) do local repo = bcache.git_obj.repo if not repos[repo.gitdir] then repos[repo.gitdir] = repo end end local repo = git.Repo.get((assert(uv.cwd()))) if repo and not repos[repo.gitdir] then repos[repo.gitdir] = repo end for _, r in pairs(repos) do for _, f in ipairs(r:files_changed(config.base)) do local f_abs = r.toplevel .. '/' .. f local stat = uv.fs_stat(f_abs) if stat and stat.type == 'file' then ---@type string local obj if config.base and config.base ~= ':0' then obj = config.base .. ':' .. f else obj = ':0:' .. f end local a = r:get_show_text(obj) local hunks = run_diff(a, util.file_lines(f_abs)) hunks_to_qflist(f_abs, hunks, qflist) end end end end return qflist end --- Populate the quickfix list with hunks. Automatically opens the --- quickfix window. --- @async --- @param target integer|'attached'|'all'? --- @param opts table? function M.setqflist(target, opts) opts = opts or {} if opts.open == nil then opts.open = true end --- @type vim.fn.setqflist.what local qfopts = { items = buildqflist(target), title = 'Hunks', } async.schedule() if opts.use_location_list then local nr = opts.nr or 0 vim.fn.setloclist(nr, {}, ' ', qfopts) if opts.open then if config.trouble then require('trouble').open('loclist') else vim.cmd.lopen() end end else vim.fn.setqflist({}, ' ', qfopts) if opts.open then if config.trouble then require('trouble').open('quickfix') else vim.cmd.copen() end end end end return M neovim-gitsigns-2.0.0/lua/gitsigns/actions/show_commit.lua000066400000000000000000000114071513053142700237420ustar00rootroot00000000000000local Async = require('gitsigns.async') local cache = require('gitsigns.cache').cache local Util = require('gitsigns.util') local Hunks = require('gitsigns.hunks') local config = require('gitsigns.config').config local api = vim.api local SHOW_FORMAT = table.concat({ 'commit' .. '%x20%H', 'tree' .. '%x20%T', 'parent' .. '%x20%P', 'author' .. '%x20%an%x20<%ae>%x20%ad', 'committer' .. '%x20%cn%x20<%ce>%x20%cd', 'encoding' .. '%x20%e', '', '%B', }, '%n') --- @param lnum integer --- @return Gitsigns.Hunk.Hunk --- @return string --- @return string local function get_hunk(lnum) local new_file --- @type string? local old_file --- @type string? local hunk_line --- @type string? while true do local line = assert(api.nvim_buf_get_lines(0, lnum - 1, lnum, false)[1]) new_file = line:match('^%+%+%+ b/(.*)') or new_file old_file = line:match('^%-%-%- a/(.*)') or old_file hunk_line = line:match('^@@ [^ ]+ [^ ]+ @@.*') or hunk_line if hunk_line and old_file and new_file then break end lnum = lnum - 1 end assert(hunk_line and old_file and new_file, 'Failed to find hunk header or file names') return Hunks.parse_diff_line(hunk_line), old_file, new_file end local M = {} --- @param base string? --- @param bufnr integer --- @param commit_buf integer --- @param ref_list string[] --- @param ref_list_ptr integer local function goto_action(base, bufnr, commit_buf, ref_list, ref_list_ptr) local curline = api.nvim_get_current_line() local header, ref = curline:match('^([a-z]+) (%x+)') if (header == 'tree' or header == 'parent') and ref then local ref_stack_ptr1 = ref_list_ptr + 1 ref_list[ref_stack_ptr1] = base for i = ref_stack_ptr1 + 1, #ref_list do ref_list[i] = nil end Async.run(M.show_commit, ref, 'edit', bufnr, ref_list, ref_stack_ptr1):raise_on_error() return elseif curline:match('^[%+%-]') then local lnum = api.nvim_win_get_cursor(0)[1] local hunk, old_file, new_file = get_hunk(lnum) local line = assert(api.nvim_buf_get_lines(commit_buf, lnum - 1, lnum, false)[1]) local added = line:match('^%+') local commit = assert(assert(api.nvim_buf_get_lines(commit_buf, 0, 1, false)[1]):match('^commit (%x+)$')) if not added then commit = commit .. '^' end Async.run(function() require('gitsigns.actions.diffthis').show(bufnr, commit, added and new_file or old_file) api.nvim_win_set_cursor(0, { added and hunk.added.start or hunk.removed.start, 0 }) end):raise_on_error() end end --- @async --- @param base? string? --- @param open? 'vsplit'|'tabnew'|'edit' --- @param bufnr? integer --- @param ref_list? string[] --- @param ref_list_ptr? integer --- @return integer? commit_buf function M.show_commit(base, open, bufnr, ref_list, ref_list_ptr) base = Util.norm_base(base or 'HEAD') open = open or 'vsplit' bufnr = bufnr or api.nvim_get_current_buf() ref_list = ref_list or {} ref_list_ptr = ref_list_ptr or #ref_list local bcache = cache[bufnr] if not bcache then return end local res = bcache.git_obj.repo:command({ 'show', '--unified=0', '--format=format:' .. SHOW_FORMAT, base, }) -- Remove encoding line if it's not set to something meaningful if assert(res[6]):match('^encoding (unknown)?') == nil then table.remove(res, 6) end local buffer_name = bcache:get_rev_bufname(base, false) local commit_buf = nil -- find preexisting commit buffer or create a new one for _, buf in ipairs(api.nvim_list_bufs()) do if api.nvim_buf_get_name(buf) == buffer_name then commit_buf = buf break end end if commit_buf == nil then commit_buf = api.nvim_create_buf(true, true) api.nvim_buf_set_name(commit_buf, buffer_name) api.nvim_buf_set_lines(commit_buf, 0, -1, false, res) vim.bo[commit_buf].modifiable = false vim.bo[commit_buf].buftype = 'nofile' vim.bo[commit_buf].filetype = 'git' vim.bo[commit_buf].bufhidden = 'wipe' end vim.cmd[open]({ mods = { keepalt = true } }) api.nvim_win_set_buf(0, commit_buf) if config._commit_maps then vim.keymap.set('n', '', function() goto_action(base, bufnr, commit_buf, ref_list, ref_list_ptr) end, { buffer = commit_buf, silent = true }) vim.keymap.set('n', '', function() local ref = ref_list[ref_list_ptr] if ref then Async.run(M.show_commit, ref, 'edit', bufnr, ref_list, ref_list_ptr - 1):raise_on_error() end end, { buffer = commit_buf, silent = true }) vim.keymap.set('n', '', function() local ref = ref_list[ref_list_ptr + 2] if ref then Async.run(M.show_commit, ref, 'edit', bufnr, ref_list, ref_list_ptr + 1):raise_on_error() end end, { buffer = commit_buf, silent = true }) end return commit_buf end return M.show_commit neovim-gitsigns-2.0.0/lua/gitsigns/async.lua000066400000000000000000000432341513053142700210720ustar00rootroot00000000000000local pcall = copcall or pcall --- @generic T --- @param ... T... --- @return [T...] & { n: integer } local function pack_len(...) --- @diagnostic disable-next-line: return-type-mismatch return { n = select('#', ...), ... } end --- like unpack() but use the length set by F.pack_len if present --- @generic T, Start: integer, End: integer --- @param t T & { n?: End } --- @param first? Start --- @return std.Unpack local function unpack_len(t, first) -- EmmyLuaLs/emmylua-analyzer-rust#619 --- @diagnostic disable-next-line: param-type-not-match, undefined-field, missing-return-value return unpack(t, first or 1, t.n or table.maxn(t --[[@as table]])) end --- @class Gitsigns.async local M = {} --- Weak table to keep track of running tasks --- @type table local threads = setmetatable({}, { __mode = 'k' }) --- @return Gitsigns.async.Task? local function running() local task = threads[coroutine.running()] if task and not (task:_completed() or task._closing) then return task end end --- Base class for async tasks. Async functions should return a subclass of --- this. This is designed specifically to be a base class of uv_handle_t --- @class Gitsigns.async.Handle --- @field close fun(self: Gitsigns.async.Handle, callback?: fun()) --- @field is_closing? fun(self: Gitsigns.async.Handle): boolean --- @alias Gitsigns.async.CallbackFn fun(...: any): Gitsigns.async.Handle? --- @class Gitsigns.async.Task : Gitsigns.async.Handle --- @field package _callbacks table --- @field package _callback_pos integer --- @field private _thread thread --- --- Tasks can call other async functions (task of callback functions) --- when we are waiting on a child, we store the handle to it here so we can --- cancel it. --- @field private _current_child? Gitsigns.async.Handle --- --- Error result of the task is an error occurs. --- Must use `await` to get the result. --- @field private _err? any --- --- Result of the task. --- Must use `await` to get the result. --- @field private _result? R[] local Task = {} Task.__index = Task --- @private --- @param func function --- @return Gitsigns.async.Task function Task._new(func) local thread = coroutine.create(func) --- @type Gitsigns.async.Task local self = setmetatable({ _closing = false, _thread = thread, _callbacks = {}, _callback_pos = 1, }, Task) threads[thread] = self return self end --- @param callback fun(err?: any, ...: any) function Task:await(callback) if self._closing then callback('closing') elseif self:_completed() then -- TODO(lewis6991): test -- Already finished or closed callback(self._err, unpack_len(self._result or {})) else self._callbacks[self._callback_pos] = callback self._callback_pos = self._callback_pos + 1 end end --- @package function Task:_completed() return (self._err or self._result) ~= nil end -- Use max 32-bit signed int value to avoid overflow on 32-bit systems. -- Do not use `math.huge` as it is not interpreted as a positive integer on all -- platforms. local MAX_TIMEOUT = 2 ^ 31 - 1 --- Synchronously wait (protected) for a task to finish (blocking) --- --- If an error is returned, `Task:traceback()` can be used to get the --- stack trace of the error. --- --- Example: --- ```lua --- --- local ok, err_or_result = task:pwait(10) --- --- if not ok then --- error(task:traceback(err_or_result)) --- end --- --- local _, result = assert(task:pwait(10)) --- ``` --- --- Can be called if a task is closing. --- @param timeout? integer --- @return boolean status --- @return any ... result or error function Task:pwait(timeout) local done = vim.wait(timeout or MAX_TIMEOUT, function() -- Note we use self:_completed() instead of self:await() to avoid creating a -- callback. This avoids having to cleanup/unregister any callback in the -- case of a timeout. return self:_completed() end) if not done then return false, 'timeout' elseif self._err then return false, self._err else return true, unpack_len(assert(self._result)) end end --- Synchronously wait for a task to finish (blocking) --- --- Example: --- ```lua --- local result = task:wait(10) -- wait for 10ms or else error --- --- local result = task:wait() -- wait indefinitely --- ``` --- @param timeout? integer Timeout in milliseconds --- @return R function Task:wait(timeout) local res = pack_len(self:pwait(timeout)) local stat = res[1] if not stat then error(self:traceback(res[2])) end return unpack_len(res, 2) end --- @private --- @param msg? string --- @param _lvl? integer --- @return string function Task:_traceback(msg, _lvl) _lvl = _lvl or 0 local thread = ('[%s] '):format(tostring(self._thread)) local child = self._current_child if getmetatable(child) == Task then --- @cast child Gitsigns.async.Task msg = child:_traceback(msg, _lvl + 1) end local tblvl = getmetatable(child) == Task and 2 or nil msg = (msg or '') .. debug.traceback(self._thread, '', tblvl):gsub('\n\t', '\n\t' .. thread) if _lvl == 0 then --- @type string msg = msg :gsub('\nstack traceback:\n', '\nSTACK TRACEBACK:\n', 1) :gsub('\nstack traceback:\n', '\n') :gsub('\nSTACK TRACEBACK:\n', '\nstack traceback:\n', 1) end return msg end --- Get the traceback of a task when it is not active. --- Will also get the traceback of nested tasks. --- --- @param msg? string --- @return string function Task:traceback(msg) return self:_traceback(msg) end --- If a task completes with an error, raise the error function Task:raise_on_error() self:await(function(err) if err then error(self:_traceback(err), 0) end end) return self end --- @private --- @param err? any --- @param result? any[] & { n: integer } function Task:_finish(err, result) self._current_child = nil self._err = err self._result = result threads[self._thread] = nil local errs = {} --- @type string[] for _, cb in pairs(self._callbacks) do --- @type boolean local ok, cb_err = pcall(cb, err, unpack_len(result or {})) if not ok then errs[#errs + 1] = cb_err end end if #errs > 0 then error(table.concat(errs, '\n'), 0) end end --- @return boolean function Task:is_closing() return self._closing end --- Close the task and all its children. --- If callback is provided it will run asynchronously, --- else it will run synchronously. --- --- @param callback? fun() function Task:close(callback) if self:_completed() then if callback then callback() end return end if self._closing then return end self._closing = true if callback then -- async if self._current_child then self._current_child:close(function() self:_finish('closed') callback() end) else self:_finish('closed') callback() end else -- sync if self._current_child then self._current_child:close(function() self:_finish('closed') end) else self:_finish('closed') end vim.wait(0, function() return self:_completed() end) end end --- @param obj any --- @return boolean local function is_async_handle(obj) local ty = type(obj) return (ty == 'table' or ty == 'userdata') and vim.is_callable(obj.close) end --- @param ... any function Task:_resume(...) --- @diagnostic disable-next-line: assign-type-mismatch --- @type [boolean, string|Gitsigns.async.CallbackFn] local ret = pack_len(coroutine.resume(self._thread, ...)) local stat = ret[1] if not stat then -- Coroutine had error self:_finish(ret[2]) elseif coroutine.status(self._thread) == 'dead' then -- Coroutine finished local result = pack_len(unpack_len(ret, 2)) self:_finish(nil, result) else local fn = ret[2] --- @cast fn -string -- TODO(lewis6991): refine error handler to be more specific local ok, r ok, r = pcall(fn, function(...) if is_async_handle(r) then --- @cast r Gitsigns.async.Handle -- We must close children before we resume to ensure -- all resources are collected. local args = pack_len(...) r:close(function() self:_resume(unpack_len(args)) end) else self:_resume(...) end end) if not ok then self:_finish(r) elseif is_async_handle(r) then self._current_child = r end end end --- @package function Task:_log(...) print(self._thread, ...) end --- @return 'running'|'suspended'|'normal'|'dead'? function Task:status() return coroutine.status(self._thread) end --- Run a function in an async context, asynchronously. --- --- Examples: --- ```lua --- -- The two below blocks are equivalent: --- --- -- Run a uv function and wait for it --- local stat = async.run(function() --- return async.await(2, vim.uv.fs_stat, 'foo.txt') --- end):wait() --- --- -- Since uv functions have sync versions. You can just do: --- local stat = vim.fs_stat('foo.txt') --- ``` --- @generic T, R --- @param func async fun(...:T...): R... --- @param ... T... --- @return Gitsigns.async.Task function M.run(func, ...) local task = Task._new(func) task:_resume(...) return task end --- Returns the status of a task’s thread. --- --- @param task? Gitsigns.async.Task --- @return 'running'|'suspended'|'normal'|'dead'? function M.status(task) task = task or running() if task then assert(getmetatable(task) == Task, 'Expected Task') return task:status() end end --- @async --- @param task Gitsigns.async.Task --- @return any ... local function await_task(task) --- @param callback fun(err?: string, ...: any) local res = pack_len(coroutine.yield(function(callback) task:await(callback) return task end)) local err = res[1] if err then -- TODO(lewis6991): what is the correct level to pass? error(err, 0) end return unpack_len(res, 2) end --- @async --- Asynchronous blocking wait --- @generic T, R --- @param argc integer --- @param fun fun(...:T..., cb: fun(...:R...)) --- @param ... T... --- @return R...... local function await_cbfun(argc, fun, ...) local args = pack_len(...) --- @param callback fun(...:any) --- @return any? return coroutine.yield(function(callback) args[argc] = callback args.n = math.max(args.n, argc) --- @diagnostic disable-next-line: missing-parameter return fun(unpack_len(args)) end) end --- Asynchronous blocking wait --- --- Example: --- ```lua --- local task = async.run(function() --- return 1, 'a' --- end) --- --- async.run(function() --- do -- await a callback function --- async.await(1, vim.schedule) --- end --- --- do -- await a task (new async context) --- local n, s = async.await(task) --- assert(n == 1 and s == 'a') --- end --- end) --- ``` --- @async --- @overload fun(func: Gitsigns.async.CallbackFn): any ... --- @overload fun(argc: integer, func: Gitsigns.async.CallbackFn, ...:any): any ... --- @overload fun(task: Gitsigns.async.Task): any ... function M.await(...) assert(running(), 'Not in async context') local arg1 = select(1, ...) if type(arg1) == 'number' then return await_cbfun(...) elseif type(arg1) == 'function' then return await_cbfun(1, ...) elseif getmetatable(arg1) == Task then return await_task(...) end error('Invalid arguments, expected Task or (argc, func) got: ' .. type(arg1), 2) end --- Creates an async function with a callback style function. --- --- Example: --- --- ```lua --- --- Note the callback argument is not present in the return function --- --- @type fun(timeout: integer) --- local sleep = async.wrap(2, function(timeout, callback) --- local timer = vim.uv.new_timer() --- timer:start(timeout * 1000, 0, callback) --- -- uv_timer_t provides a close method so timer will be --- -- cleaned up when this function finishes --- return timer --- end) --- --- async.run(function() --- print('hello') --- sleep(2) --- print('world') --- end) --- ``` --- --- local atimer = async.awrap( --- @generic T, R --- @param argc integer --- @param func fun(...:T..., cb: fun(...:R...)): any --- @return async fun(...:T...):R... function M.wrap(argc, func) assert(type(argc) == 'number') assert(type(func) == 'function') --- @async return function(...) return M.await(argc, func, ...) end end if vim.schedule then --- An async function that when called will yield to the Neovim scheduler to be --- able to call the API. M.schedule = M.wrap(1, vim.schedule) end do --- M.event() --- An event can be used to notify multiple tasks that some event has --- happened. An Event object manages an internal flag that can be set to true --- with the `set()` method and reset to `false` with the `clear()` method. --- The `wait()` method blocks until the flag is set to `true`. The flag is --- set to `false` initially. --- @class Gitsigns.async.Event --- @field private _is_set boolean --- @field private _waiters function[] local Event = {} Event.__index = Event --- Set the event. --- --- All tasks waiting for event to be set will be immediately awakened. --- @param max_woken? integer function Event:set(max_woken) if self._is_set then return end self._is_set = true local waiters = self._waiters local waiters_to_notify = {} --- @type function[] max_woken = max_woken or #waiters while #waiters > 0 and #waiters_to_notify < max_woken do waiters_to_notify[#waiters_to_notify + 1] = table.remove(waiters, 1) end if #waiters > 0 then self._is_set = false end for _, waiter in ipairs(waiters_to_notify) do waiter() end end --- Wait until the event is set. --- --- If the event is set, return `true` immediately. Otherwise block until --- another task calls set(). --- @async function Event:wait() M.await(function(callback) if self._is_set then callback() else table.insert(self._waiters, callback) end end) end --- Clear (unset) the event. --- --- Tasks awaiting on wait() will now block until the set() method is called --- again. function Event:clear() self._is_set = false end --- Create a new event --- --- An event can signal to multiple listeners to resume execution --- The event can be set from a non-async context. --- --- ```lua --- local event = vim.async.event() --- --- local worker = vim.async.run(function() --- sleep(1000) --- event.set() --- end) --- --- local listeners = { --- vim.async.run(function() --- event.wait() --- print("First listener notified") --- end), --- vim.async.run(function() --- event.wait() --- print("Second listener notified") --- end), --- } --- ``` --- @return Gitsigns.async.Event function M.event() return setmetatable({ _waiters = {}, _is_set = false, }, Event) end end do --- M.semaphore() --- A semaphore manages an internal counter which is decremented by each --- `acquire()` call and incremented by each `release()` call. The counter can --- never go below zero; when `acquire()` finds that it is zero, it blocks, --- waiting until some task calls `release()`. --- --- The preferred way to use a Semaphore is with the `with()` method, which --- automatically acquires and releases the semaphore around a function call. --- @class Gitsigns.async.Semaphore --- @field private _permits integer --- @field private _max_permits integer --- @field package _event Gitsigns.async.Event local Semaphore = {} Semaphore.__index = Semaphore --- Executes the given function within the semaphore's context, ensuring --- that the semaphore's constraints are respected. --- @async --- @generic R --- @param fn async fun(): R... # Function to execute within the semaphore's context. --- @return R... # Result(s) of the executed function. function Semaphore:with(fn) self:acquire() local r = pack_len(pcall(fn)) self:release() local stat = r[1] if not stat then --- @diagnostic disable-next-line: undefined-field local err = r[2] error(err) end return unpack_len(r, 2) end --- Acquire a semaphore. --- --- If the internal counter is greater than zero, decrement it by `1` and --- return immediately. If it is `0`, wait until a `release()` is called. --- @async function Semaphore:acquire() self._event:wait() self._permits = self._permits - 1 assert(self._permits >= 0, 'Semaphore value is negative') if self._permits == 0 then self._event:clear() end end --- Release a semaphore. --- --- Increments the internal counter by `1`. Can wake --- up a task waiting to acquire the semaphore. function Semaphore:release() if self._permits >= self._max_permits then error('Semaphore value is greater than max permits', 2) end self._permits = self._permits + 1 self._event:set(1) end --- Create an async semaphore that allows up to a given number of acquisitions. --- --- ```lua --- vim.async.run(function() --- local semaphore = vim.async.semaphore(2) --- --- local tasks = {} --- --- local value = 0 --- for i = 1, 10 do --- tasks[i] = vim.async.run(function() --- semaphore:with(function() --- value = value + 1 --- sleep(10) --- print(value) -- Never more than 2 --- value = value - 1 --- end) --- end) --- end --- --- vim.async.join(tasks) --- assert(value <= 2) --- end) --- ``` --- @param permits? integer (default: 1) --- @return Gitsigns.async.Semaphore function M.semaphore(permits) permits = permits or 1 local obj = setmetatable({ _max_permits = permits, _permits = permits, _event = M.event(), }, Semaphore) obj._event:set() return obj end end return M neovim-gitsigns-2.0.0/lua/gitsigns/attach.lua000066400000000000000000000277761513053142700212360ustar00rootroot00000000000000local Status = require('gitsigns.status') local async = require('gitsigns.async') local git = require('gitsigns.git') local Cache = require('gitsigns.cache') local log = require('gitsigns.debug.log') local manager = require('gitsigns.manager') local Util = require('gitsigns.util') local Path = Util.Path local cache = Cache.cache local config = require('gitsigns.config').config local dprint = log.dprint local dprintf = log.dprintf local throttle_async = require('gitsigns.debounce').throttle_async local api = vim.api local uv = vim.uv or vim.loop ---@diagnostic disable-line: deprecated --- @class gitsigns.attach local M = {} --- @param name string --- @return string? rel_path --- @return string? commit --- @return string? gitdir local function parse_git_path(name) if not vim.startswith(name, 'fugitive://') and not vim.startswith(name, 'gitsigns://') then return end local proto, gitdir, tail = unpack(vim.split(name, '//')) assert(proto and gitdir and tail) local plugin = proto:sub(1, 1):upper() .. proto:sub(2, -2) local commit, rel_path --- @type string?, string? if plugin == 'Gitsigns' then commit = tail:match('^(:?[^:]+):') rel_path = tail:match('^:?[^:]+:(.*)') else -- Fugitive commit = tail:match('^([^/]+)/') if commit and commit:match('^[0-3]$') then --- @diagnostic disable-next-line: no-unknown commit = ':' .. commit end rel_path = tail:match('^[^/]+/(.*)') end -- Fugitive worktree buffers do not have a path -- 'fugitive://////' if rel_path == '' then return end dprintf("%s buffer for file '%s' from path '%s' on commit '%s'", plugin, rel_path, name, commit) return rel_path, commit, gitdir end local function on_lines(_, bufnr, _, first, last_orig, last_new, byte_count) if first == last_orig and last_orig == last_new and byte_count == 0 then -- on_lines can be called twice for undo events; ignore the second -- call which indicates no changes. return end return manager.on_lines(bufnr, first, last_orig, last_new) end --- @param _ 'reload' --- @param bufnr integer local function on_reload(_, bufnr) assert(cache[bufnr]):invalidate() dprint('Reload') manager.update_sync_debounced(bufnr) end --- @param _ 'detach' --- @param bufnr integer local function on_detach(_, bufnr) api.nvim_clear_autocmds({ group = 'gitsigns', buffer = bufnr }) M.detach(bufnr, true) end --- @async --- @param bufnr integer --- @return string? --- @return string? local function on_attach_pre(bufnr) --- @type string?, string? local gitdir, toplevel if config._on_attach_pre then --- @type {gitdir: string?, toplevel: string?} local res = async.await(2, config._on_attach_pre, bufnr) dprintf('ran on_attach_pre with result %s', vim.inspect(res)) if type(res) == 'table' then if type(res.gitdir) == 'string' then gitdir = res.gitdir end if type(res.toplevel) == 'string' then toplevel = res.toplevel end end end return gitdir, toplevel end local setup = Util.once(function() manager.setup() require('gitsigns.current_line_blame').setup() api.nvim_create_autocmd('BufFilePre', { group = 'gitsigns', desc = 'Gitsigns: detach when changing buffer names', callback = function(args) M.detach(args.buf) end, }) api.nvim_create_autocmd('VimLeavePre', { desc = 'Gitsigns: detach from all buffers', group = 'gitsigns', callback = M.detach_all, }) end) --- @class (exact) Gitsigns.GitContext --- @field file string Path to the file represented by the buffer. --- @field toplevel? string Path to the top-level of the parent git repository. --- @field gitdir? string Path to the git directory of the parent git repository. --- @field base? string Git revision to compare against. --- @async --- @param bufnr integer --- @return Gitsigns.GitContext? ctx --- @return string? err local function get_buf_context(bufnr) if api.nvim_buf_line_count(bufnr) > config.max_file_length then return nil, 'Exceeds max_file_length' end local bufname = api.nvim_buf_get_name(bufnr) -- Resolve the buffer name to a real path (following symlinks) if we can, local bufpath = uv.fs_realpath(bufname) or bufname local rel_path, commit, gitdir_from_bufname = parse_git_path(bufpath) if not gitdir_from_bufname then if vim.bo[bufnr].buftype ~= '' then return nil, 'Non-normal buffer' elseif not Path.exists(vim.fs.dirname(bufpath)) then return nil, 'Not a path' elseif Path.is_dir(bufpath) then return nil, 'Not a file' end end local gitdir_oap, toplevel_oap = on_attach_pre(bufnr) return { file = rel_path or bufpath, gitdir = gitdir_oap or gitdir_from_bufname, toplevel = toplevel_oap, -- Stage buffers always compare against the common ancestor (':1') -- :0: index -- :1: common ancestor -- :2: target commit (HEAD) -- :3: commit which is being merged base = commit and (commit:match('^:[1-3]') and ':1' or commit) or nil, } end --- @async --- @param bufnr integer --- @param old_relpath? string local function handle_moved(bufnr, old_relpath) local bcache = assert(cache[bufnr]) local git_obj = bcache.git_obj local orig_relpath = assert(git_obj.orig_relpath or old_relpath) git_obj.orig_relpath = orig_relpath local new_name = git_obj.repo:diff_rename_status()[orig_relpath] if new_name then dprintf('File moved to %s', new_name) git_obj.relpath = new_name git_obj.file = git_obj.repo.toplevel .. '/' .. new_name elseif git_obj.orig_relpath then local orig_file = Path.join(git_obj.repo.toplevel, git_obj.orig_relpath) if not git_obj.repo:file_info(orig_file, git_obj.revision) then return end --- File was moved in the index, but then reset dprintf('Moved file reset') git_obj.relpath = git_obj.orig_relpath git_obj.orig_relpath = nil else -- File removed from index, do nothing return end git_obj.file = Path.join(git_obj.repo.toplevel, git_obj.relpath) bcache.file = git_obj.file git_obj:refresh() if not bcache:schedule() then return end local bufexists = Util.bufexists(bcache.file) local old_name = api.nvim_buf_get_name(bufnr) if not bufexists then -- Do not trigger BufFilePre/Post -- TODO(lewis6991): figure out how to avoid reattaching without -- disabling all autocommands. Util.noautocmd({ 'BufFilePre', 'BufFilePost' }, function() Util.buf_rename(bufnr, bcache.file) end) end local msg = bufexists and 'Cannot rename' or 'Renamed' dprintf('%s buffer %d from %s to %s', msg, bufnr, old_name, bcache.file) end --- @async --- @param bufnr integer local function repo_update_handler(bufnr) local bcache = cache[bufnr] if not bcache then return end dprintf('Watcher handler called for buffer %d %s', bufnr, bcache.file) local git_obj = bcache.git_obj Status.update(bufnr, { head = git_obj.repo.abbrev_head }) local was_tracked = git_obj.object_name ~= nil local old_relpath = git_obj.relpath bcache:invalidate(true) git_obj:refresh() if not bcache:schedule() then dprint('buffer invalid (1)') return end if config.watch_gitdir.follow_files and was_tracked and not git_obj.object_name then -- File was tracked but is no longer tracked. Must of been removed or -- moved. Check if it was moved and switch to it. handle_moved(bufnr, old_relpath) if not bcache:schedule() then dprint('buffer invalid (2)') return end end require('gitsigns.manager').update(bufnr) end --- @async --- Ensure attaches cannot be interleaved for the same buffer. --- Since attaches are asynchronous we need to make sure an attach isn't --- performed whilst another one is in progress. --- @param cbuf integer --- @param ctx? Gitsigns.GitContext --- @param aucmd? string M.attach = throttle_async({ hash = 1 }, function(cbuf, ctx, aucmd) local __FUNC__ = 'attach' local passed_ctx = ctx ~= nil setup() if cache[cbuf] then dprint('Already attached') return end if aucmd then dprintf('Attaching (trigger=%s)', aucmd) else dprint('Attaching') end if not api.nvim_buf_is_loaded(cbuf) then dprint('Non-loaded buffer') return end if not ctx then local err ctx, err = get_buf_context(cbuf) if err then dprint(err) return end assert(ctx) end local encoding = vim.bo[cbuf].fileencoding if encoding == '' then encoding = 'utf-8' end local file, toplevel = ctx.file, ctx.toplevel if not Path.is_abs(file) and toplevel then file = Path.join(toplevel, file) end local revision = ctx.base or config.base local git_obj = git.Obj.new(file, revision, encoding, ctx.gitdir, toplevel) if not git_obj and not passed_ctx then for _, wt in ipairs(config.worktrees) do git_obj = git.Obj.new(file, revision, encoding, wt.gitdir, wt.toplevel) if git_obj and git_obj.object_name then dprintf('Using worktree %s', vim.inspect(wt)) break end end end if not git_obj then dprint('Empty git obj') return end async.schedule() if not api.nvim_buf_is_valid(cbuf) then return end Status.update(cbuf, { head = git_obj.repo.abbrev_head, root = git_obj.repo.toplevel, gitdir = git_obj.repo.gitdir, }) if not git_obj.relpath then dprint('Cannot resolve file in repo') return end if not config.attach_to_untracked and git_obj.object_name == nil then dprint('File is untracked') return end -- On windows os.tmpname() crashes in callback threads so initialise this -- variable on the main thread. async.schedule() if not api.nvim_buf_is_valid(cbuf) then return end if config.on_attach and config.on_attach(cbuf) == false then dprint('User on_attach() returned false') return end cache[cbuf] = Cache.new(cbuf, file, git_obj) if git_obj.repo:has_watcher() then dprintf('Watching git dir %s', git_obj.repo.gitdir) --- Throttle to: --- - ensure handler is only triggered once per git operation. --- - prevent updates to the same buffer from interleaving as the handler is --- async. local throttled_repo_update_handler = throttle_async({ schedule = true }, function() repo_update_handler(cbuf) end) cache[cbuf].deregister_watcher = git_obj.repo:on_update(function() async.run(throttled_repo_update_handler):raise_on_error() end) end if not api.nvim_buf_is_loaded(cbuf) then dprint('Un-loaded buffer') return end -- Make sure to attach before the first update (which is async) so we pick up -- changes from BufReadCmd. api.nvim_buf_attach(cbuf, false, { on_lines = on_lines, on_reload = on_reload, on_detach = on_detach, }) api.nvim_create_autocmd('BufWrite', { group = 'gitsigns', buffer = cbuf, callback = function() manager.update_sync_debounced(cbuf) end, }) -- Initial update manager.update(cbuf) dprint('attach complete') if config.current_line_blame then require('gitsigns.current_line_blame').update(cbuf) end end) --- Detach Gitsigns from all buffers it is attached to. function M.detach_all() for k, _ in pairs(cache) do M.detach(k) end end --- Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not --- provided then the current buffer is used. --- --- @param bufnr integer Buffer number --- @param keep_signs? boolean function M.detach(bufnr, keep_signs) -- When this is called interactively (with no arguments) we want to remove all -- the signs, however if called via a detach event (due to nvim_buf_attach) -- then we don't want to clear the signs in case the buffer is just being -- updated due to the file externally changing. When this happens a detach and -- attach event happen in sequence and so we keep the old signs to stop the -- sign column width moving about between updates. bufnr = bufnr or api.nvim_get_current_buf() dprint('Detached') local bcache = cache[bufnr] if not bcache then dprint('Cache was nil') return end manager.detach(bufnr, keep_signs) -- Clear status variables Status.clear(bufnr) Cache.destroy(bufnr) end return M neovim-gitsigns-2.0.0/lua/gitsigns/cache.lua000066400000000000000000000251461513053142700210220ustar00rootroot00000000000000local async = require('gitsigns.async') local config = require('gitsigns.config').config local log = require('gitsigns.debug.log') local util = require('gitsigns.util') local api = vim.api local M = { CacheEntry = {}, } --- @class (exact) Gitsigns.CacheEntry.Blame --- @field entries table --- @field max_time? integer --- @field min_time? integer --- @class (exact) Gitsigns.CacheEntry --- @field bufnr integer --- @field file string --- @field compare_text? string[] --- @field hunks? Gitsigns.Hunk.Hunk[] --- @field force_next_update? boolean --- --- An update is required for the buffer next time it comes into view --- @field update_on_view? boolean --- --- @field file_mode? boolean --- --- @field compare_text_head? string[] --- @field hunks_staged? Gitsigns.Hunk.Hunk[] --- --- @field staged_diffs Gitsigns.Hunk.Hunk[] --- @field deregister_watcher? fun() --- @field git_obj Gitsigns.GitObj --- @field blame? Gitsigns.CacheEntry.Blame --- @field commits? table local CacheEntry = M.CacheEntry --- @param rev? string --- @param filename? false|string --- @return string function CacheEntry:get_rev_bufname(rev, filename) rev = rev or self.git_obj.revision or ':0' local gitdir = self.git_obj.repo.gitdir if filename == false then return ('gitsigns://%s//%s'):format(gitdir, rev) end return ('gitsigns://%s//%s:%s'):format(gitdir, rev, filename or self.git_obj.relpath) end --- Invalidate any state dependent on the buffer content. --- If 'all' is passed, then invalidate everything. --- @param all? boolean function CacheEntry:invalidate(all) self.hunks = nil self.hunks_staged = nil self.blame = nil self.commits = nil if all then -- The below doesn't need to be invalidated -- if the buffer changes self.compare_text = nil self.compare_text_head = nil end end --- @param bufnr integer --- @param file string --- @param git_obj Gitsigns.GitObj --- @return Gitsigns.CacheEntry function M.new(bufnr, file, git_obj) return setmetatable({ bufnr = bufnr, file = file, git_obj = git_obj, staged_diffs = {}, }, { __index = CacheEntry }) end local sleep = async.wrap(2, function(duration, cb) vim.defer_fn(cb, duration) end) --- @async --- @private function CacheEntry:wait_for_hunks() local loop_protect = 0 while not self.hunks and loop_protect < 10 do loop_protect = loop_protect + 1 sleep(100) end end -- If a file contains has up to this amount of lines, then -- always blame the whole file, otherwise only blame one line -- at a time. local BLAME_THRESHOLD_LEN = 10000 --- @async --- @private --- @param lnum? integer|[integer, integer] --- @param opts? Gitsigns.BlameOpts --- @return table --- @return table --- @return boolean? full function CacheEntry:run_blame(lnum, opts) local bufnr = self.bufnr -- Always send contents if buffer represents an editable file on disk. -- Otherwise do not sent contents buffer revision is from tree and git version -- is below 2.41. -- -- This avoids the error: -- "fatal: cannot use --contents with final commit object name" local send_contents = vim.bo[bufnr].buftype == '' or (not self.git_obj:from_tree() and not require('gitsigns.git.version').check(2, 41)) while true do local contents = send_contents and util.buf_lines(bufnr) or nil local tick = vim.b[bufnr].changedtick local lnum0 = api.nvim_buf_line_count(bufnr) > BLAME_THRESHOLD_LEN and lnum or nil -- TODO(lewis6991): Cancel blame on changedtick local blame, commits = self.git_obj:run_blame(contents, lnum0, self.git_obj.revision, opts) async.schedule() if not api.nvim_buf_is_valid(bufnr) then return {}, {} end if vim.b[bufnr].changedtick == tick then return blame, commits, lnum0 == nil end end error('unreachable') end --- @private --- @param lnum? integer --- @return boolean function CacheEntry:blame_valid(lnum) local blame = self.blame if not blame then return false end if lnum then return blame.entries[lnum] ~= nil end -- Need to check we have blame info for all lines for i = 1, api.nvim_buf_line_count(self.bufnr) do if not blame.entries[i] then return false end end return true end --- If lnum is nil then run blame for the entire buffer. --- @async --- @param lnum? integer|[integer, integer] --- @param opts? Gitsigns.BlameOpts --- @return Gitsigns.BlameInfo? function CacheEntry:get_blame(lnum, opts) local blame = self.blame local blame_valid = true if type(lnum) == 'table' then local curr_lnum = lnum[1] while blame_valid and curr_lnum <= lnum[2] do blame_valid = self:blame_valid(curr_lnum) curr_lnum = curr_lnum + 1 end else blame_valid = self:blame_valid(lnum) end if not blame or not blame_valid then self:wait_for_hunks() blame = blame or { entries = {} } local Hunks = require('gitsigns.hunks') local has_blameable_line = true if lnum then local start_lnum = type(lnum) == 'table' and lnum[1] or lnum local end_lnum = type(lnum) == 'table' and lnum[2] or lnum for curr_lnum = start_lnum, end_lnum do has_blameable_line = not Hunks.find_hunk(curr_lnum, self.hunks) if has_blameable_line then break end end end if lnum and not has_blameable_line then --- Bypass running blame (which can be expensive) if we know lnum is in a hunk local Blame = require('gitsigns.git.blame') local relpath = assert(self.git_obj.relpath) local start_lnum = type(lnum) == 'table' and lnum[1] or lnum local end_lnum = type(lnum) == 'table' and lnum[2] or lnum for curr_lnum = start_lnum, end_lnum do local info = Blame.get_blame_nc(relpath, curr_lnum) blame.entries[curr_lnum] = info blame.max_time = info.commit.author_time end else -- Refresh/update cache local b, commits, full = self:run_blame(lnum, opts) self.commits = vim.tbl_extend('force', self.commits or {}, commits) if lnum and not full then local start_lnum = type(lnum) == 'table' and lnum[1] or lnum local end_lnum = type(lnum) == 'table' and lnum[2] or lnum for curr_lnum = start_lnum, end_lnum do blame.entries[curr_lnum] = b[curr_lnum] end else blame.entries = b end end self.blame = blame end return blame.entries[lnum] end --- @async --- @nodiscard --- @param check_compare_text? boolean --- @return boolean function CacheEntry:schedule(check_compare_text) async.schedule() local bufnr = self.bufnr if not api.nvim_buf_is_valid(bufnr) then log.dprint('Buffer not valid, aborting') return false end if not M.cache[bufnr] then log.dprint('Has detached, aborting') return false end if check_compare_text and not M.cache[bufnr].compare_text then log.dprint('compare_text was invalid, aborting') return false end return true end --- @async --- @param greedy? boolean --- @param staged? boolean --- @return Gitsigns.Hunk.Hunk[]? function CacheEntry:get_hunks(greedy, staged) if greedy and config.diff_opts.linematch then -- Re-run the diff without linematch local buftext = util.buf_lines(self.bufnr) local text --- @type string[]? if staged then text = self.compare_text_head else text = self.compare_text end if not text then return end local run_diff = require('gitsigns.diff') local hunks = run_diff(text, buftext, false) if not self:schedule() then return end return hunks end if staged then return self.hunks_staged and vim.deepcopy(self.hunks_staged) or nil end return self.hunks and vim.deepcopy(self.hunks) or nil end --- @param hunks? Gitsigns.Hunk.Hunk[]? --- @return Gitsigns.Hunk.Hunk? hunk --- @return integer? index function CacheEntry:get_cursor_hunk(hunks) if not hunks then hunks = {} vim.list_extend(hunks, self.hunks or {}) vim.list_extend(hunks, self.hunks_staged or {}) end local lnum = api.nvim_win_get_cursor(0)[1] local Hunks = require('gitsigns.hunks') return Hunks.find_hunk(lnum, hunks) end --- @async --- @param range? [integer,integer] --- @param greedy? boolean --- @param staged? boolean --- @return Gitsigns.Hunk.Hunk? function CacheEntry:get_hunk(range, greedy, staged) local Hunks = require('gitsigns.hunks') local hunks = self:get_hunks(greedy, staged) if not range then return (self:get_cursor_hunk(hunks)) end table.sort(range) local top, bot = range[1], range[2] local hunk = Hunks.create_partial_hunk(hunks or {}, top, bot) if not hunk then return end local compare_text = assert(self.compare_text) if staged then local staged_top, staged_bot = top, bot for _, h in ipairs(assert(self.hunks)) do if top > h.vend then staged_top = staged_top - (h.added.count - h.removed.count) end if bot > h.vend then staged_bot = staged_bot - (h.added.count - h.removed.count) end end hunk.added.lines = vim.list_slice(compare_text, staged_top, staged_bot) hunk.removed.lines = vim.list_slice( assert(self.compare_text_head), hunk.removed.start, hunk.removed.start + hunk.removed.count - 1 ) else hunk.added.lines = api.nvim_buf_get_lines(self.bufnr, top - 1, bot, false) hunk.removed.lines = vim.list_slice(compare_text, hunk.removed.start, hunk.removed.start + hunk.removed.count - 1) end return hunk end function CacheEntry:get_blame_times() local blame = assert(self.blame) if blame.max_time and blame.min_time then return blame.min_time, blame.max_time end local min_time = math.huge --[[@as integer]] for _, c in pairs(assert(self.commits)) do min_time = math.min(min_time, c.author_time) end blame.min_time = min_time -- If the buffer can be edited, then always set the max time to now. -- For read-only buffers, set the max time to the latest commit time. if vim.bo[self.bufnr].modifiable then blame.max_time = os.time() else local max_time = 0 --[[@as integer]] for _, c in pairs(assert(self.commits)) do max_time = math.max(max_time, c.author_time) end blame.max_time = max_time end return blame.min_time, blame.max_time end function CacheEntry:destroy() if self.deregister_watcher then self.deregister_watcher() self.deregister_watcher = nil end end ---@type table M.cache = {} --- @param bufnr integer function M.destroy(bufnr) assert(M.cache[bufnr]):destroy() M.cache[bufnr] = nil end return M neovim-gitsigns-2.0.0/lua/gitsigns/cli.lua000066400000000000000000000054221513053142700205210ustar00rootroot00000000000000local actions = require('gitsigns.actions') local argparse = require('gitsigns.cli.argparse') local async = require('gitsigns.async') local attach = require('gitsigns.attach') local Debug = require('gitsigns.debug') local log = require('gitsigns.debug.log') local message = require('gitsigns.message') --- @type table[] local sources = { actions, attach, Debug } --- try to parse each argument as a lua boolean, nil or number, if fails then --- keep argument as a string: --- --- 'false' -> false --- 'nil' -> nil --- '100' -> 100 --- 'HEAD~300' -> 'HEAD~300' --- @param a string|boolean --- @return boolean|number|string? local function parse_to_lua(a) if tonumber(a) then return tonumber(a) elseif a == 'false' or a == 'true' then return a == 'true' elseif a == 'nil' then return nil end return a end local M = {} function M.complete(arglead, line) local words = vim.split(line, '%s+') local n = #words local matches = {} if n == 2 then for _, m in ipairs(sources) do for func, _ in pairs(m) do if not func:match('^[a-z]') then -- exclude elseif vim.startswith(func, arglead) then table.insert(matches, func) end end end elseif n > 2 then -- Subcommand completion local cmp_func = actions._get_cmp_func(assert(words[2])) if cmp_func then return cmp_func(arglead) end end return matches end --- @async --- @param params vim.api.keyset.create_user_command.command_args function M.run(params) local pos_args_raw, named_args_raw = argparse.parse_args(params.args) local func = pos_args_raw[1] if not func then func = async.await(3, function(...) -- Need to wrap vim.ui.select as Snacks version of vim.ui.select returns a -- module table with a close method which conflicts with the async lib vim.ui.select(...) end, M.complete('', 'Gitsigns '), {}) --[[@as string]] if not func then return end end local pos_args = vim.tbl_map(parse_to_lua, vim.list_slice(pos_args_raw, 2)) local named_args = vim.tbl_map(parse_to_lua, named_args_raw) local args = vim.tbl_extend('error', pos_args, named_args) log.dprintf( "Running action '%s' with arguments %s", func, vim.inspect(args, { newline = ' ', indent = '' }) ) local cmd_func = actions._get_cmd_func(func) if cmd_func then -- Action has a specialised mapping function from command form to lua -- function cmd_func(args, params) return end for _, m in ipairs(sources) do local f = m[func] if type(f) == 'function' then -- Note functions here do not have named arguments f(unpack(pos_args)) return end end message.error('%s is not a valid function or action', func) end return M neovim-gitsigns-2.0.0/lua/gitsigns/cli/000077500000000000000000000000001513053142700200135ustar00rootroot00000000000000neovim-gitsigns-2.0.0/lua/gitsigns/cli/argparse.lua000066400000000000000000000051121513053142700223210ustar00rootroot00000000000000local M = {} local function is_char(x) return x:match('[^=\'"%s]') ~= nil end -- Return positional arguments and named arguments --- @param x string --- @return string[], table function M.parse_args(x) --- @type string[], table local pos_args, named_args = {}, {} local state = 'in_arg' local cur_arg = '' local cur_val = '' local cur_quote = '' local function peek(idx) return x:sub(idx + 1, idx + 1) end local i = 1 while i <= #x do local ch = x:sub(i, i) -- dprintf('L(%d)(%s): cur_arg="%s" ch="%s"', i, state, cur_arg, ch) if state == 'in_arg' then if is_char(ch) then if ch == '-' and peek(i) == '-' then state = 'in_flag' cur_arg = '' i = i + 1 else cur_arg = cur_arg .. ch end elseif ch:match('%s') then pos_args[#pos_args + 1] = cur_arg state = 'in_ws' elseif ch == '=' then cur_val = '' local next_ch = peek(i) if next_ch == "'" or next_ch == '"' then cur_quote = next_ch i = i + 1 state = 'in_quote' else state = 'in_value' end end elseif state == 'in_flag' then if ch:match('%s') then named_args[cur_arg] = true state = 'in_ws' else cur_arg = cur_arg .. ch end elseif state == 'in_ws' then if is_char(ch) then if ch == '-' and peek(i) == '-' then state = 'in_flag' cur_arg = '' i = i + 1 else state = 'in_arg' cur_arg = ch end end elseif state == 'in_value' then if is_char(ch) then cur_val = cur_val .. ch elseif ch:match('%s') then named_args[cur_arg] = cur_val cur_arg = '' state = 'in_ws' end elseif state == 'in_quote' then local next_ch = peek(i) if ch == '\\' and next_ch == cur_quote then cur_val = cur_val .. next_ch i = i + 1 elseif ch == cur_quote then named_args[cur_arg] = cur_val state = 'in_ws' if next_ch ~= '' and not next_ch:match('%s') then error('malformed argument: ' .. next_ch) end else cur_val = cur_val .. ch end end i = i + 1 end if #cur_arg > 0 then if state == 'in_arg' then pos_args[#pos_args + 1] = cur_arg elseif state == 'in_flag' then named_args[cur_arg] = true elseif state == 'in_value' then named_args[cur_arg] = cur_val end end return pos_args, named_args end return M neovim-gitsigns-2.0.0/lua/gitsigns/color.lua000066400000000000000000000036371513053142700210760ustar00rootroot00000000000000local M = {} --- @param value integer --- @return [integer, integer, integer] function M.int_to_rgb(value) local r = bit.band(bit.rshift(value, 16), 0xFF) local g = bit.band(bit.rshift(value, 8), 0xFF) local b = bit.band(value, 0xFF) return { r, g, b } end --- @param rgb [integer, integer, integer] --- @return integer function M.rgb_to_int(rgb) return rgb[1] * 0x10000 + rgb[2] * 0x100 + rgb[3] end --- @param stops [integer,integer,integer][] --- @param t number 0-1 --- @return [integer, integer, integer] function M.gradient(stops, t) assert(t >= 0 and t <= 1, 't must be between 0 and 1') local num_stops = #stops if num_stops < 2 then error('At least two color stops are required') end local segment_length = 1 / (num_stops - 1) local segment_index = math.floor(t / segment_length) if segment_index >= num_stops - 1 then local lstop = stops[num_stops] --- @cast lstop -? return { lstop[1], lstop[2], lstop[3] } end local local_t = (t - segment_index * segment_length) / segment_length local color1 = assert(stops[segment_index + 1]) local color2 = assert(stops[segment_index + 2]) return M.blend(color1, color2, local_t) end --- @param a integer --- @param b integer --- @param alpha number --- @return integer local function lerp(a, b, alpha) return math.floor(a + (b - a) * alpha) end --- @param color1 [integer, integer, integer] --- @param color2 [integer, integer, integer] --- @param alpha number 0-1 --- @return [integer, integer, integer] function M.blend(color1, color2, alpha) return { lerp(color1[1], color2[1], alpha), lerp(color1[2], color2[2], alpha), lerp(color1[3], color2[3], alpha), } end local temp_color_stops = { { 0, 0, 255 }, -- Blue { 255, 0, 0 }, -- Red { 255, 255, 0 }, -- Yellow } --- @param value number 0-1 --- @return [integer, integer, integer] function M.temp(value) return M.gradient(temp_color_stops, value) end return M neovim-gitsigns-2.0.0/lua/gitsigns/config.lua000066400000000000000000000640641513053142700212260ustar00rootroot00000000000000--- @class (exact) Gitsigns.SchemaElem --- @field type type|type[]|fun(x:any): boolean --- @field type_help? string --- @field default_change? fun(cb: fun()) Function to refresh the config value --- @field deep_extend? boolean --- @field default? any --- @field deprecated? boolean --- @field default_help? string --- @field description string --- @class (exact) Gitsigns.DiffthisOpts --- --- Split window vertically. Default to `config.diff_opts.vertical`. If running --- via command line, then shi is taken from the command modifiers. --- @field vertical? boolean --- @field split? 'aboveleft'|'belowright'|'topleft'|'botright' --- @class (exact) Gitsigns.DiffOpts --- @field algorithm 'myers'|'minimal'|'patience'|'histogram' --- @field internal boolean --- @field indent_heuristic boolean --- @field vertical boolean --- @field linematch? integer --- @field ignore_whitespace_change? true --- @field ignore_whitespace? true --- @field ignore_whitespace_change_at_eol? true --- @field ignore_blank_lines? true --- @class (exact) Gitsigns.SignConfig --- @field show_count boolean --- @field text string --- @alias Gitsigns.SignType --- | 'add' --- | 'change' --- | 'delete' --- | 'topdelete' --- | 'changedelete' --- | 'untracked' --- @alias Gitsigns.CurrentLineBlameFmtFun fun(user: string, info: table): [string,string][] --- @class (exact) Gitsigns.CurrentLineBlameOpts : Gitsigns.BlameOpts --- @field virt_text? boolean --- @field virt_text_pos? 'eol'|'overlay'|'right_align' --- @field delay? integer --- @field virt_text_priority? integer --- @field use_focus? boolean --- @class (exact) Gitsigns.BlameOpts --- Ignore whitespace when running blame. --- @field ignore_whitespace? boolean --- Extra options passed to `git-blame`. --- @field extra_opts? string[] --- @class (exact) Gitsigns.Config --- @field package _config table config store --- @field debug_mode boolean --- @field diff_opts Gitsigns.DiffOpts --- @field diffthis Gitsigns.DiffthisOpts --- @field base? string --- @field signs table --- @field signs_staged table --- @field signs_staged_enable boolean --- @field count_chars table --- @field signcolumn boolean --- @field numhl boolean --- @field linehl boolean --- @field culhl boolean --- @field show_deleted boolean --- @field sign_priority integer --- @field _on_attach_pre? fun(bufnr: integer, callback: fun(_: table)) --- @field on_attach? fun(bufnr: integer): boolean? --- @field watch_gitdir { enable: boolean, follow_files: boolean } --- @field max_file_length integer --- @field update_debounce integer --- @field status_formatter fun(_: table): string --- @field current_line_blame boolean --- @field current_line_blame_formatter string|Gitsigns.CurrentLineBlameFmtFun --- @field current_line_blame_formatter_nc string|Gitsigns.CurrentLineBlameFmtFun --- @field current_line_blame_opts Gitsigns.CurrentLineBlameOpts --- @field preview_config vim.api.keyset.win_config --- @field auto_attach boolean --- @field attach_to_untracked boolean --- @field worktrees {toplevel: string, gitdir: string}[] --- @field word_diff boolean --- @field trouble boolean --- @field gh boolean --- -- Undocumented --- @field _refresh_staged_on_update boolean --- @field _threaded_diff boolean --- @field _git_version string --- @field _verbose boolean --- @field _test_mode boolean --- @field _new_sign_calc boolean --- @field _update_lock boolean --- @field _commit_maps boolean --- @field _statuscolumn? boolean --- @class Gitsigns.config local M = {} --- @alias Gitsigns.Config.SubscribersCb fun(old_val:any, new_val:any) --- @type table local subscribers = {} --- @param v Gitsigns.SchemaElem --- @return any local function resolve_default(v) if type(v.default) == 'function' and v.type ~= 'function' then return v.default() else return v.default end end --- @return Gitsigns.DiffOpts local function parse_diffopt() --- @type Gitsigns.DiffOpts local r = { algorithm = 'myers', internal = false, indent_heuristic = false, vertical = true, } local optmap = { ['indent-heuristic'] = 'indent_heuristic', internal = 'internal', iwhite = 'ignore_whitespace_change', iblank = 'ignore_blank_lines', iwhiteeol = 'ignore_whitespace_change_at_eol', iwhiteall = 'ignore_whitespace', } local diffopt = vim.opt.diffopt:get() --[[@as string[] ]] for _, o in ipairs(diffopt) do if optmap[o] then r[optmap[o]] = true elseif o == 'horizontal' then r.vertical = false elseif vim.startswith(o, 'algorithm:') then r.algorithm = o:sub(#'algorithm:' + 1) --[[@as 'myers'|'minimal'|'patience'|'histogram']] elseif vim.startswith(o, 'linematch:') then r.linematch = tonumber(o:sub(('linematch:'):len() + 1)) end end return r end --- @type Gitsigns.Config M.config = setmetatable({ _config = {} }, { --- @param self Gitsigns.Config --- @param k string --- @param v any __newindex = function(self, k, v) local oldv = self._config[k] local field = M.schema[k] if not field then return end if field.deep_extend then self._config[k] = vim.tbl_deep_extend('force', resolve_default(field), v) else self._config[k] = v end if field.default_change then -- Reinvoke __newindex with the original value whenever the default -- value changes. field.default_change(function() --- @diagnostic disable-next-line: no-unknown self[k] = v end) end if oldv ~= nil and not vim.deep_equal(oldv, v) then for _, cb in ipairs(subscribers[k] or {}) do cb(oldv, v) end end end, --- @param self Gitsigns.Config --- @param k string --- @return any __index = function(self, k) -- Most values in the config should have the default ( if self._config[k] == nil then local field = M.schema[k] if not field then return end self._config[k] = resolve_default(field) end return self._config[k] end, }) local function warn(s, ...) vim.notify_once(s:format(...), vim.log.levels.WARN, { title = 'gitsigns' }) end --- @param x Gitsigns.SignConfig --- @return boolean local function validate_signs(x) if type(x) ~= 'table' then return false end return true end --- @type table M.schema = { signs = { type_help = 'table', type = validate_signs, deep_extend = true, default = { add = { text = '┃' }, change = { text = '┃' }, delete = { text = '▁' }, topdelete = { text = '▔' }, changedelete = { text = '~' }, untracked = { text = '┆' }, }, default_help = [[{ add = { text = '┃' }, change = { text = '┃' }, delete = { text = '▁' }, topdelete = { text = '▔' }, changedelete = { text = '~' }, untracked = { text = '┆' }, }]], description = [[ Configuration for signs: • `text` specifies the character to use for the sign. • `show_count` to enable showing count of hunk, e.g. number of deleted lines. The highlights `GitSigns[kind][type]` is used for each kind of sign. E.g. 'add' signs uses the highlights: • `GitSignsAdd` (for normal text signs) • `GitSignsAddNr` (for signs when `config.numhl == true`) • `GitSignsAddLn `(for signs when `config.linehl == true`) • `GitSignsAddCul `(for signs when `config.culhl == true`) See |gitsigns-highlight-groups|. ]], }, signs_staged = { type = 'table', deep_extend = true, default = { add = { text = '┃' }, change = { text = '┃' }, delete = { text = '▁' }, topdelete = { text = '▔' }, changedelete = { text = '~' }, }, default_help = [[{ add = { text = '┃' }, change = { text = '┃' }, delete = { text = '▁' }, topdelete = { text = '▔' }, changedelete = { text = '~' }, }]], description = [[ Configuration for signs of staged hunks. See |gitsigns-config-signs|. ]], }, signs_staged_enable = { type = 'boolean', default = true, description = [[ Show signs for staged hunks. When enabled the signs defined in |git-config-signs_staged| are used. ]], }, worktrees = { type = 'table', default = {}, description = [[ Detached working trees. Array of tables with the keys `gitdir` and `toplevel`. If normal attaching fails, then each entry in the table is attempted with the work tree details set. Example: >lua worktrees = { { toplevel = vim.env.HOME, gitdir = vim.env.HOME .. '/projects/dotfiles/.git' } } ]], }, _on_attach_pre = { type = 'function', description = [[ Asynchronous hook called before attaching to a buffer. Mainly used to configure detached worktrees. This callback must call its callback argument. The callback argument can accept an optional table argument with the keys: 'gitdir' and 'toplevel'. Example: >lua on_attach_pre = function(bufnr, callback) ... callback { gitdir = ..., toplevel = ... } end < ]], }, on_attach = { type = 'function', description = [[ Callback called when attaching to a buffer. Mainly used to setup keymaps. The buffer number is passed as the first argument. This callback can return `false` to prevent attaching to the buffer. Example: >lua on_attach = function(bufnr) if vim.api.nvim_buf_get_name(bufnr):match() then -- Don't attach to specific buffers whose name matches a pattern return false end -- Setup keymaps vim.api.nvim_buf_set_keymap(bufnr, 'n', 'hs', 'lua require"gitsigns".stage_hunk()', {}) ... -- More keymaps end < ]], }, watch_gitdir = { type = 'table', deep_extend = true, default = { enable = true, follow_files = true, }, description = [[ When opening a file, a libuv watcher is placed on the respective `.git` directory to detect when changes happen to use as a trigger to update signs. Fields: ~ • `enable`: Whether the watcher is enabled. • `follow_files`: If a file is moved with `git mv`, switch the buffer to the new location. ]], }, sign_priority = { type = 'number', default = 6, description = [[ Priority to use for signs. ]], }, signcolumn = { type = 'boolean', default = true, description = [[ Enable/disable symbols in the sign column. When enabled the highlights defined in `signs.*.hl` and symbols defined in `signs.*.text` are used. ]], }, numhl = { type = 'boolean', default = false, description = [[ Enable/disable line number highlights. When enabled the highlights defined in `signs.*.numhl` are used. If the highlight group does not exist, then it is automatically defined and linked to the corresponding highlight group in `signs.*.hl`. ]], }, linehl = { type = 'boolean', default = false, description = [[ Enable/disable line highlights. When enabled the highlights defined in `signs.*.linehl` are used. If the highlight group does not exist, then it is automatically defined and linked to the corresponding highlight group in `signs.*.hl`. ]], }, culhl = { type = 'boolean', default = false, description = [[ Enable/disable highlights for the sign column when the cursor is on the same line. When enabled the highlights defined in `signs.*.culhl` are used. If the highlight group does not exist, then it is automatically defined and linked to the corresponding highlight group in `signs.*.hl`. ]], }, show_deleted = { type = 'boolean', deprecated = true, default = false, description = [[ Show the old version of hunks inline in the buffer (via virtual lines). Note: Virtual lines currently use the highlight `GitSignsDeleteVirtLn`. ]], }, diff_opts = { type = 'table', deep_extend = true, default_change = function(callback) vim.api.nvim_create_autocmd('OptionSet', { group = vim.api.nvim_create_augroup('gitsigns.config.diff_opts', {}), pattern = 'diffopt', callback = callback, }) end, default = parse_diffopt, default_help = "derived from 'diffopt'", description = [[ Diff options. If the default value is used, then changes to 'diffopt' are automatically applied. Fields: ~ • algorithm: string Diff algorithm to use. Values: • "myers" the default algorithm • "minimal" spend extra time to generate the smallest possible diff • "patience" patience diff algorithm • "histogram" histogram diff algorithm • internal: boolean Use Neovim's built in xdiff library for running diffs. • indent_heuristic: boolean Use the indent heuristic for the internal diff library. • vertical: boolean Start diff mode with vertical splits. • linematch: integer Enable second-stage diff on hunks to align lines. Requires `internal=true`. • ignore_blank_lines: boolean Ignore changes where lines are blank. • ignore_whitespace_change: boolean Ignore changes in amount of white space. It should ignore adding trailing white space, but not leading white space. • ignore_whitespace: boolean Ignore all white space changes. • ignore_whitespace_change_at_eol: boolean Ignore white space changes at end of line. ]], }, diffthis = { type = 'table', default = { split = 'aboveleft', }, description = [[ Options for the `:Gitsigns diffthis` command. ]], }, base = { type = 'string', default_help = 'index', description = [[ The object/revision to diff against. See |gitsigns-revision|. ]], }, count_chars = { type = 'table', default = { [1] = '1', -- '₁', [2] = '2', -- '₂', [3] = '3', -- '₃', [4] = '4', -- '₄', [5] = '5', -- '₅', [6] = '6', -- '₆', [7] = '7', -- '₇', [8] = '8', -- '₈', [9] = '9', -- '₉', ['+'] = '>', -- '₊', }, description = [[ The count characters used when `signs.*.show_count` is enabled. The `+` entry is used as a fallback. With the default, any count outside of 1-9 uses the `>` character in the sign. Possible use cases for this field: • to specify unicode characters for the counts instead of 1-9. • to define characters to be used for counts greater than 9. ]], }, status_formatter = { type = 'function', --- @param status Gitsigns.StatusObj --- @return string default = function(status) local added, changed, removed = status.added, status.changed, status.removed local status_txt = {} if added and added > 0 then table.insert(status_txt, '+' .. added) end if changed and changed > 0 then table.insert(status_txt, '~' .. changed) end if removed and removed > 0 then table.insert(status_txt, '-' .. removed) end return table.concat(status_txt, ' ') end, default_help = [[function(status) local added, changed, removed = status.added, status.changed, status.removed local status_txt = {} if added and added > 0 then table.insert(status_txt, '+'..added ) end if changed and changed > 0 then table.insert(status_txt, '~'..changed) end if removed and removed > 0 then table.insert(status_txt, '-'..removed) end return table.concat(status_txt, ' ') end]], description = [[ Function used to format `b:gitsigns_status`. ]], }, max_file_length = { type = 'number', default = 40000, description = [[ Max file length (in lines) to attach to. ]], }, preview_config = { type = 'table', deep_extend = true, default = { style = 'minimal', relative = 'cursor', row = 0, col = 1, }, description = [[ Option overrides for the Gitsigns preview window. Table is passed directly to `nvim_open_win`. ]], }, auto_attach = { type = 'boolean', default = true, description = [[ Automatically attach to files. ]], }, attach_to_untracked = { type = 'boolean', default = false, description = [[ Attach to untracked files. ]], }, update_debounce = { type = 'number', default = 100, description = [[ Debounce time for updates (in milliseconds). ]], }, current_line_blame = { type = 'boolean', default = false, description = [[ Adds an unobtrusive and customisable blame annotation at the end of the current line. The highlight group used for the text is `GitSignsCurrentLineBlame`. ]], }, current_line_blame_opts = { type = 'table', deep_extend = true, default = { virt_text = true, virt_text_pos = 'eol', virt_text_priority = 100, delay = 1000, use_focus = true, }, description = [[ Options for the current line blame annotation. Fields: ~ • virt_text: boolean Whether to show a virtual text blame annotation. • virt_text_pos: string Blame annotation position. Available values: `eol` Right after eol character. `overlay` Display over the specified column, without shifting the underlying text. `right_align` Display right aligned in the window. • delay: integer Sets the delay (in milliseconds) before blame virtual text is displayed. • ignore_whitespace: boolean Ignore whitespace when running blame. • virt_text_priority: integer Priority of virtual text. • use_focus: boolean Enable only when buffer is in focus • extra_opts: string[] Extra options passed to `git-blame`. ]], }, current_line_blame_formatter = { type = { 'string', 'function' }, default = ' , - ', description = [[ String or function used to format the virtual text of |gitsigns-config-current_line_blame|. When a string, accepts the following format specifiers: • `` • `` • `` • `` • `` • `` or `` • `` • `` • `` • `` or `` • `` • `` • `` • `` For `` and ``, `FORMAT` can be any valid date format that is accepted by `os.date()` with the addition of `%R` (defaults to `%Y-%m-%d`): • `%a` abbreviated weekday name (e.g., Wed) • `%A` full weekday name (e.g., Wednesday) • `%b` abbreviated month name (e.g., Sep) • `%B` full month name (e.g., September) • `%c` date and time (e.g., 09/16/98 23:48:10) • `%d` day of the month (16) [01-31] • `%H` hour, using a 24-hour clock (23) [00-23] • `%I` hour, using a 12-hour clock (11) [01-12] • `%M` minute (48) [00-59] • `%m` month (09) [01-12] • `%p` either "am" or "pm" (pm) • `%S` second (10) [00-61] • `%w` weekday (3) [0-6 = Sunday-Saturday] • `%x` date (e.g., 09/16/98) • `%X` time (e.g., 23:48:10) • `%Y` full year (1998) • `%y` two-digit year (98) [00-99] • `%%` the character `%` • `%R` relative (e.g., 4 months ago) When a function: Parameters: ~ {name} Git user name returned from `git config user.name` . {blame_info} Table with the following keys: • `abbrev_sha`: string • `orig_lnum`: integer • `final_lnum`: integer • `author`: string • `author_mail`: string • `author_time`: integer • `author_tz`: string • `committer`: string • `committer_mail`: string • `committer_time`: integer • `committer_tz`: string • `summary`: string • `previous`: string • `filename`: string • `boundary`: true? Note that the keys map onto the output of: `git blame --line-porcelain` Return: ~ The result of this function is passed directly to the `opts.virt_text` field of |nvim_buf_set_extmark| and thus must be a list of [text, highlight] tuples. ]], }, current_line_blame_formatter_nc = { type = { 'string', 'function' }, default = ' ', description = [[ String or function used to format the virtual text of |gitsigns-config-current_line_blame| for lines that aren't committed. See |gitsigns-config-current_line_blame_formatter| for more information. ]], }, trouble = { type = 'boolean', default = function() local has_trouble = pcall(require, 'trouble') return has_trouble end, default_help = 'true if installed', description = [[ When using setqflist() or setloclist(), open Trouble instead of the quickfix/location list window. ]], }, gh = { type = 'boolean', default = false, description = [[ Enable GitHub integration. This allows the following features: • `:Gitsigns blame_line` will show PR numbers (with a hyperlink) ]], }, _git_version = { type = 'string', default = 'auto', description = [[ Version of git available. Set to 'auto' to automatically detect. ]], }, _verbose = { type = 'boolean', default = false, description = [[ More verbose debug message. Requires debug_mode=true. ]], }, _test_mode = { description = 'Enable test mode', type = 'boolean', default = false, }, word_diff = { type = 'boolean', default = false, description = [[ Highlight intra-line word differences in the buffer. Requires `config.diff_opts.internal = true` . Uses the highlights: • For word diff in previews: • `GitSignsAddInline` • `GitSignsChangeInline` • `GitSignsDeleteInline` • For word diff in buffer: • `GitSignsAddLnInline` • `GitSignsChangeLnInline` • `GitSignsDeleteLnInline` • For word diff in virtual lines (e.g. show_deleted): • `GitSignsAddVirtLnInline` • `GitSignsChangeVirtLnInline` • `GitSignsDeleteVirtLnInline` ]], }, _refresh_staged_on_update = { type = 'boolean', default = false, description = [[ Always refresh the staged file on each update. Disabling this will cause the staged file to only be refreshed when an update to the index is detected. ]], }, _threaded_diff = { type = 'boolean', default = true, description = [[ Run diffs on a separate thread ]], }, _new_sign_calc = { type = 'boolean', default = true, description = [[ Use new sign calculation method ]], }, _update_lock = { type = 'boolean', default = false, description = [[ Acquire a lock when updating signs. ]], }, _commit_maps = { type = 'boolean', default = false, description = [[ Enable new mappings in commit buffers ]], }, _statuscolumn = { type = 'boolean', default = false, description = [[ Statuscolumn mode is enabled ]], }, debug_mode = { type = 'boolean', default = false, description = [[ Enables debug logging and makes the following functions available: `dump_cache`, `debug_messages`, `clear_debug`. ]], }, } --- Subscribe to a config change --- @param k string|string[] --- @param cb Gitsigns.Config.SubscribersCb function M.subscribe(k, cb) if type(k) == 'string' then k = { k } end for _, v in ipairs(k) do subscribers[v] = subscribers[v] or {} table.insert(subscribers[v], cb) end end local nvim011 = vim.fn.has('nvim-0.11') == 1 --- @param k string --- @param v any --- @param ty type|fun(v:any):boolean local function validate(k, v, ty) if nvim011 then --- @diagnostic disable-next-line: redundant-parameter,param-type-mismatch vim.validate(k, v, ty) else vim.validate({ [k] = { v, ty } }) end end --- @param user_config table function M.build(user_config) --- @cast user_config table local config = M.config --[[@as table]] -- Check deprecated config options for k, v in pairs(user_config) do local s = M.schema[k] if not s then warn("gitsigns: Ignoring invalid configuration field '%s'", k) else local ty = s.type if type(ty) == 'string' or type(ty) == 'function' then --- EmmyLuaLs/emmylua-analyzer-rust#696 --- @diagnostic disable-next-line: param-type-not-match, param-type-mismatch validate(k, v, ty) end if s.deprecated then warn('%s is now deprecated; ignoring', k) end config[k] = v end end end return M neovim-gitsigns-2.0.0/lua/gitsigns/current_line_blame.lua000066400000000000000000000156431513053142700236110ustar00rootroot00000000000000local async = require('gitsigns.async') local debounce = require('gitsigns.debounce') local util = require('gitsigns.util') local cache = require('gitsigns.cache').cache local Config = require('gitsigns.config') local config = Config.config local schema = require('gitsigns.config').schema local error_once = require('gitsigns.message').error_once local api = vim.api local namespace = api.nvim_create_namespace('gitsigns_blame') local M = {} --- @param bufnr integer local function reset(bufnr) if not api.nvim_buf_is_valid(bufnr) then return end api.nvim_buf_del_extmark(bufnr, namespace, 1) vim.b[bufnr].gitsigns_blame_line_dict = nil end --- @param fmt string --- @param name string --- @param info Gitsigns.BlameInfoPublic --- @return string local function expand_blame_format(fmt, name, info) if info.author == name then info.author = 'You' end return util.expand_format(fmt, info) end --- @param virt_text [string, string][] --- @return string local function flatten_virt_text(virt_text) local res = {} ---@type string[] for _, part in ipairs(virt_text) do res[#res + 1] = part[1] end return table.concat(res) end --- @param winid integer? --- @return integer local function win_width(winid) winid = winid or api.nvim_get_current_win() local wininfo = vim.fn.getwininfo(winid)[1] local textoff = wininfo and wininfo.textoff or 0 return api.nvim_win_get_width(winid) - textoff end --- @param bufnr integer --- @param lnum integer --- @return integer local function line_len(bufnr, lnum) local line = assert(api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]) return api.nvim_strwidth(line) end --- @param fmt string --- @return Gitsigns.CurrentLineBlameFmtFun local function default_formatter(fmt) return function(username, blame_info) return { { expand_blame_format(fmt, username, blame_info), 'GitSignsCurrentLineBlame', }, } end end ---@param bcache Gitsigns.CacheEntry ---@param blame_info Gitsigns.BlameInfoPublic ---@return [string, string][] local function get_blame_virt_text(bcache, blame_info) local git_obj = bcache.git_obj local use_nc = blame_info.author == 'Not Committed Yet' local clb_formatter = use_nc and config.current_line_blame_formatter_nc or config.current_line_blame_formatter if type(clb_formatter) == 'function' then local ok, res = pcall(clb_formatter, git_obj.repo.username, blame_info) if ok then --- @cast res -string return res end --- @cast res string local nc_sfx = use_nc and '_nc' or '' error_once( 'Failed running config.current_line_blame_formatter%s, using default:\n %s', nc_sfx, res ) --- @type string clb_formatter = schema.current_line_blame_formatter.default end --- @cast clb_formatter string EmmyLuaLs/emmylua-analyzer-rust#372 return default_formatter(clb_formatter)(git_obj.repo.username, blame_info) end --- @param bcache Gitsigns.CacheEntry --- @param lnum integer --- @param blame_info Gitsigns.BlameInfo --- @param opts Gitsigns.CurrentLineBlameOpts local function handle_blame_info(bcache, lnum, blame_info, opts) local bufnr = bcache.bufnr blame_info = util.convert_blame_info(blame_info) local virt_text = get_blame_virt_text(bcache, blame_info) local virt_text_str = flatten_virt_text(virt_text) vim.b[bufnr].gitsigns_blame_line_dict = blame_info vim.b[bufnr].gitsigns_blame_line = virt_text_str if opts.virt_text then local virt_text_pos = opts.virt_text_pos -- If right_align and the text is too long, move to eol so the line isn't -- obscured and the blame is truncated. if virt_text_pos == 'right_align' then local win = vim.fn.bufwinid(bufnr) if api.nvim_strwidth(virt_text_str) > (win_width(win) - line_len(bufnr, lnum)) then virt_text_pos = 'eol' end end api.nvim_buf_set_extmark(bufnr, namespace, lnum - 1, 0, { id = 1, virt_text = virt_text, virt_text_pos = virt_text_pos, priority = opts.virt_text_priority, hl_mode = 'combine', }) end end --- @param winid integer --- @return integer lnum local function get_lnum(winid) return api.nvim_win_get_cursor(winid)[1] end --- @param winid integer --- @param lnum integer --- @return boolean local function foldclosed(winid, lnum) ---@return boolean return api.nvim_win_call(winid, function() return vim.fn.foldclosed(lnum) ~= -1 end) end ---@return boolean local function insert_mode() return api.nvim_get_mode().mode == 'i' end --- Update function, must be called in async context --- @async --- @param bufnr integer local function update(bufnr) async.schedule() if not api.nvim_buf_is_valid(bufnr) then return end if insert_mode() then return end local winid = api.nvim_get_current_win() if bufnr ~= api.nvim_win_get_buf(winid) then return end local lnum = get_lnum(winid) -- Can't show extmarks on folded lines so skip if foldclosed(winid, lnum) then return end local bcache = cache[bufnr] if not bcache or not bcache.git_obj.object_name then return end local opts = config.current_line_blame_opts local blame_info = bcache:get_blame(lnum, opts) if not api.nvim_win_is_valid(winid) or bufnr ~= api.nvim_win_get_buf(winid) then return end if not blame_info then return end if lnum ~= get_lnum(winid) then -- Cursor has moved during events; abort and tr-trigger another update update(bufnr) return end handle_blame_info(bcache, lnum, blame_info, opts) end local update_throttled = debounce.throttle_async({ hash = 1 }, update) -- TODO(lewis6991): opts.delay is always defined as the schema set -- deep_extend=true M.update = debounce.debounce_trailing( function() return config.current_line_blame_opts.delay end, --- @param bufnr integer function(bufnr) async.run(update_throttled, bufnr):raise_on_error() end ) function M.setup() for k in pairs(cache) do reset(k) end local group = api.nvim_create_augroup('gitsigns_blame', {}) if not config.current_line_blame then return end -- show current buffer line blame immediately M.update(api.nvim_get_current_buf()) local update_events = { 'BufEnter', 'CursorMoved', 'CursorMovedI', 'WinResized' } local reset_events = { 'InsertEnter', 'BufLeave' } if config.current_line_blame_opts.use_focus then update_events[#update_events + 1] = 'FocusGained' reset_events[#reset_events + 1] = 'FocusLost' end api.nvim_create_autocmd(update_events, { group = group, callback = function(args) reset(args.buf) M.update(args.buf) end, }) api.nvim_create_autocmd(reset_events, { group = group, callback = function(args) reset(args.buf) end, }) api.nvim_create_autocmd('OptionSet', { group = group, pattern = { 'fileformat', 'bomb', 'eol' }, callback = function(args) reset(args.buf) end, }) end Config.subscribe('current_line_blame', function() M.setup() end) return M neovim-gitsigns-2.0.0/lua/gitsigns/debounce.lua000066400000000000000000000100201513053142700215240ustar00rootroot00000000000000local uv = vim.uv or vim.loop ---@diagnostic disable-line: deprecated local M = {} --- @class gitsigns.debounce.debounce_trailing.Opts --- @field timeout integer|fun():integer Timeout in ms --- @field hash? integer|fun(...): any Function that determines id from arguments to `fn` --- Debounces a function on the trailing edge. --- --- Example waveform --- Time: 0 1 2 3 4 5 6 7 8 9 --- Input: | | | | --- Debounced: | | --- --- In this example, the function is called at times 0, 1, 3, and 7. --- With a debounce period of 3 units, the debounced function fires at 3 and 9. --- --- @generic F: function --- @param opts gitsigns.debounce.debounce_trailing.Opts|integer|fun():integer --- @param fn F Function to debounce --- @return F Debounced function. function M.debounce_trailing(opts, fn) local timeout --- @type (integer|fun():integer)? local hash --- @type (integer|fun(...): any)? if type(opts) == 'table' then timeout = opts.timeout hash = opts.hash else timeout = opts end -- Normalize hash to a function if it's a number (argument index) if type(hash) == 'number' then local hash_i = hash --- @return any hash = function(...) return select(hash_i, ...) end elseif type(hash) ~= 'function' then hash = nil end -- Normalize ms to a function if it's a number if type(timeout) == 'number' then local ms_i = timeout timeout = function() return ms_i end end local running = {} --- @type table return function(...) local id = hash and hash(...) or true local argv, argc = { ... }, select('#', ...) local timer = running[id] if not timer or timer:is_closing() then timer = assert(uv.new_timer()) running[id] = timer end timer:start(timeout(), 0, function() timer:close() running[id] = nil fn(unpack(argv, 1, argc)) end) end end --- @class gitsigns.debounce.throttle_async.Opts --- @field hash? integer|fun(...): any Function that determines id from arguments to fn --- @field schedule? boolean If true, always schedule next call if called while running --- Throttles an async function using the first argument as an ID --- --- If function is already running then the function will be scheduled to run --- again once the running call has finished. --- --- ``` --- fn#1 _/‾\__/‾\_/‾\____________________________ --- throttled#1[schedule=false] _/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\_______________________ --- throttled#1[schedule=true] _/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\/‾‾‾‾‾‾‾‾‾‾\___________ --- --- fn#2 ______/‾\___________/‾\___________________ --- throttled#2[schedule=true] ______/‾‾‾‾‾‾‾‾‾‾\__/‾‾‾‾‾‾‾‾‾‾\__________ --- throttled#2[schedule=false] ______/‾‾‾‾‾‾‾‾‾‾\__/‾‾‾‾‾‾‾‾‾‾\__________ --- ``` --- --- @generic T --- @param opts gitsigns.debounce.throttle_async.Opts --- @param fn async fun(...: T...) Function to throttle --- @return async fun(...:T ...) # Throttled function. function M.throttle_async(opts, fn) local scheduled = {} --- @type table local running = {} --- @type table local hash = opts.hash local schedule = opts.schedule or false -- Normalize hash to a function if it's a number (argument index) if type(hash) == 'number' then local hash_i = hash hash = function(...) return select(hash_i, ...) end elseif type(hash) ~= 'function' then hash = nil end --- @async return function(...) local id = hash and hash(...) or true if scheduled[id] then -- If fn is already scheduled, then drop return end if not running[id] or schedule then scheduled[id] = true end if running[id] then return end while scheduled[id] do scheduled[id] = nil running[id] = true fn(...) running[id] = nil end end end return M neovim-gitsigns-2.0.0/lua/gitsigns/debug.lua000066400000000000000000000022431513053142700210360ustar00rootroot00000000000000local log = require('gitsigns.debug.log') --- @class gitsigns.debug local M = {} --- @param raw_item any --- @param path string[] --- @return any local function process(raw_item, path) --- @diagnostic disable-next-line:undefined-field if path[#path] == vim.inspect.METATABLE then return elseif type(raw_item) == 'function' then return elseif type(raw_item) ~= 'table' then return raw_item end --- @cast raw_item table local key = path[#path] if vim.tbl_contains({ 'compare_text', 'compare_text_head', 'hunks', 'hunks_staged', 'staged_diffs', }, key) then return { '...', length = #vim.tbl_keys(raw_item), head = raw_item[next(raw_item)] } elseif key == 'blame' then return { '...', length = #vim.tbl_keys(raw_item) } end return raw_item end --- @return any function M.dump_cache() -- TODO(lewis6991): hack: use package.loaded to avoid circular deps local cache = (require('gitsigns.cache')).cache --- @type string local text = vim.inspect(cache, { process = process }) vim.api.nvim_echo({ { text } }, false, {}) end M.debug_messages = log.show M.clear_debug = log.clear return M neovim-gitsigns-2.0.0/lua/gitsigns/debug/000077500000000000000000000000001513053142700203325ustar00rootroot00000000000000neovim-gitsigns-2.0.0/lua/gitsigns/debug/log.lua000066400000000000000000000162251513053142700216240ustar00rootroot00000000000000local uv = vim.uv or vim.loop ---@diagnostic disable-line: deprecated local start_time = uv.hrtime() --- @class Gitsigns.log --- @field package messages [number, string, string, string][] local M = { messages = {}, } function M.debug_mode() return require('gitsigns.config').config.debug_mode end function M.verbose() return require('gitsigns.config').config._verbose end --- @param name string --- @param lvl integer --- @return any local function getvarvalue(name, lvl) lvl = lvl + 1 local value --- @type any? local found --- @type boolean? -- try local variables local i = 1 while true do local n, v = debug.getlocal(lvl, i) if not n then break end if n == name then value = v found = true end i = i + 1 end if found then return value end -- try upvalues local func = debug.getinfo(lvl).func i = 1 while true do local n, v = debug.getupvalue(func, i) if not n then break end if n == name then return v end i = i + 1 end -- not found; get global return getfenv(func)[name] end --- @param info debuglib.DebugInfo --- @return string local function get_cur_module(info) local src = info.source or '???' if vim.startswith(src, '@') then src = src:sub(2) end src = src:gsub('^%./', '') local rel = src:match('^lua/(.+)$') or src:match('[/\\]lua[/\\](.+)$') local module = (rel or src):gsub('%.lua$', ''):gsub('/init$', ''):gsub('[\\/]', '.') return module end --- @param tbl table --- @param func function --- @return string? local function find_func_name(tbl, func) for k, v in pairs(tbl) do if v == func and type(k) == 'string' then return k end end end --- @param func function? --- @param lvl integer --- @return string? local function get_cur_func_name_from_self(func, lvl) if not func then return end local self_tbl = getvarvalue('self', lvl) if type(self_tbl) ~= 'table' then return end local name = find_func_name(self_tbl, func) if name then return name end local mt = getmetatable(self_tbl) local idx = mt and mt.__index if type(idx) ~= 'table' then return end local name1 = find_func_name(idx, func) if name1 then return name1 end end --- @param func function? --- @param module string? --- @return string? local function get_cur_func_name_from_loaded(func, module) if not func then return end local tbl = package.loaded[module] if type(tbl) == 'table' then return find_func_name(tbl, func) end end local func_names_cache = {} --- @type table --- @param info debuglib.DebugInfo --- @param lvl integer --- @param module string? --- @return string local function get_cur_func_name(info, lvl, module) lvl = lvl + 1 local func = info.func if func_names_cache[func] then return func_names_cache[func] end local name = getvarvalue('__FUNC__', lvl) --[[@as string?]] or info.name or get_cur_func_name_from_self(func, lvl) or get_cur_func_name_from_loaded(func, module) or (info.what == 'main') and 'main' or (''):format(info.linedefined or 0) func_names_cache[func] = name return name end --- @param lvl integer --- @return {module: string?, name:string, bufnr: integer} local function get_context(lvl) lvl = lvl + 1 local info = debug.getinfo(lvl, 'nSf') or {} --- @type any local module = get_cur_module(info) local func = get_cur_func_name(info, lvl, module) module = module:gsub('^gitsigns%.', '') func = func:gsub('(.*)%d+$', '%1') local bufnr = getvarvalue('bufnr', lvl) or getvarvalue('_bufnr', lvl) or getvarvalue('cbuf', lvl) or getvarvalue('buf', lvl) return { module = module, name = func, bufnr = bufnr } end local function tostring(obj) return type(obj) == 'string' and obj or vim.inspect(obj) end --- If called in a callback then make sure the callback defines a __FUNC__ --- variable which can be used to identify the name of the function. --- @param kind string --- @param lvl integer --- @param ... any local function cprint(kind, lvl, ...) lvl = lvl + 1 local msgs = {} --- @type string[] for i = 1, select('#', ...) do msgs[i] = tostring(select(i, ...)) end local msg = table.concat(msgs, ' ') local ctx = get_context(lvl) local time = (uv.hrtime() - start_time) / 1e6 local ctx1 = ctx.module and ctx.module .. '.' .. ctx.name or ctx.name if ctx.bufnr then ctx1 = string.format('%s(%s)', ctx1, ctx.bufnr) end table.insert(M.messages, { time, kind, ctx1, msg }) end function M.dprint(...) if not M.debug_mode() then return end cprint('debug', 2, ...) end function M.dprintf(obj, ...) if not M.debug_mode() then return end cprint('debug', 2, obj:format(...)) end function M.vprint(...) if not (M.debug_mode() and M.verbose()) then return end cprint('info', 2, ...) end function M.vprintf(obj, ...) if not (M.debug_mode() and M.verbose()) then return end cprint('info', 2, obj:format(...)) end --- @param msg string --- @param level integer local function eprint(msg, level) local info = debug.getinfo(level + 2, 'Sl') local ctx = info and string.format('%s<%d>', info.short_src, info.currentline) or '???' local time = (uv.hrtime() - start_time) / 1e6 table.insert(M.messages, { time, 'error', ctx, debug.traceback(msg) }) if M.debug_mode() then error(msg, 3) end end function M.eprint(msg) eprint(msg, 1) end function M.eprintf(fmt, ...) eprint(fmt:format(...), 1) end --- @param cond boolean --- @param msg string --- @return boolean function M.assert(cond, msg) if not cond then eprint(msg, 1) end return not cond end local sev_to_hl = { debug = 'Title', info = 'MoreMsg', warn = 'WarningMsg', error = 'ErrorMsg', } function M.clear() M.messages = {} end --- @param m [number, string, string, string] --- @param verbose? boolean --- @return [string,string?][] local function build_msg(m, verbose) local time, kind, ctx, msg = m[1], m[2], m[3], m[4] local hl = sev_to_hl[kind] -- Scrub some messages if not verbose and ctx == 'run_job' then ctx = 'git' msg = msg :gsub(vim.pesc('--no-pager --no-optional-locks --literal-pathspecs -c gc.auto=0 '), '') :gsub(vim.pesc('-c core.quotepath=off'), '') local cwd = vim.uv.cwd() if cwd then msg = msg:gsub(vim.pesc(cwd), '$CWD') end local home = vim.env.HOME if home then msg = msg:gsub(vim.pesc(home), '$HOME') end end return { { string.format('%.2f ', time), 'Comment' }, { kind:upper():sub(1, 1), hl }, { string.format(' %s:', ctx), 'Tag' }, { ' ' }, { msg }, } end function M.show() local lastm --- @type number? for _, m in ipairs(M.messages) do if lastm and m[1] - lastm > 200 then vim.api.nvim_echo({ { '|', 'NonText' } }, false, {}) end lastm = m[1] vim.api.nvim_echo(build_msg(m), false, {}) end end --- @param verbose? boolean --- @return string[]? function M.get(verbose) local r = {} --- @type string[] for _, m in ipairs(M.messages) do local e = build_msg(m, verbose) local e1 = {} --- @type string[] for _, x in ipairs(e) do e1[#e1 + 1] = x[1] end r[#r + 1] = table.concat(e1) end return r end return M neovim-gitsigns-2.0.0/lua/gitsigns/diff.lua000066400000000000000000000011351513053142700206570ustar00rootroot00000000000000local config = require('gitsigns.config').config --- @async --- @param a string[] --- @param b string[] --- @param linematch? boolean --- @return Gitsigns.Hunk.Hunk[] hunks return function(a, b, linematch) -- -- Short circuit optimization -- if not a or #a == 0 then -- local Hunks = require('gitsigns.hunks') -- local hunk = Hunks.create_hunk(0, 0, 1, #b) -- hunk.added.lines = b -- return { hunk } -- end if config.diff_opts.internal then return require('gitsigns.diff_int').run_diff(a, b, linematch) else return require('gitsigns.diff_ext').run_diff(a, b) end end neovim-gitsigns-2.0.0/lua/gitsigns/diff_ext.lua000066400000000000000000000047501513053142700215450ustar00rootroot00000000000000local Hunks = require('gitsigns.hunks') local scheduler = require('gitsigns.async').schedule local config = require('gitsigns.config').config local git_command = require('gitsigns.git.cmd') local M = {} --- @param path string --- @param text string[] local function write_to_file(path, text) local f, err = io.open(path, 'wb') if f == nil then error(err) end for _, l in ipairs(text) do f:write(l) f:write('\n') end f:close() end --- @async --- @param text_cmp string[] --- @param text_buf string[] --- @return Gitsigns.Hunk.Hunk[] function M.run_diff(text_cmp, text_buf) local results = {} --- @type Gitsigns.Hunk.Hunk[] -- tmpname must not be called in a callback if vim.in_fast_event() then scheduler() end local file_buf = vim.fn.tempname() local file_cmp = vim.fn.tempname() write_to_file(file_buf, text_buf) write_to_file(file_cmp, text_cmp) -- Taken from gitgutter, diff.vim: -- -- If a file has CRLF line endings and git's core.autocrlf is true, the file -- in git's object store will have LF line endings. Writing it out via -- git-show will produce a file with LF line endings. -- -- If this last file is one of the files passed to git-diff, git-diff will -- convert its line endings to CRLF before diffing -- which is what we want -- but also by default outputs a warning on stderr. -- -- warning: LF will be replace by CRLF in . -- The file will have its original line endings in your working directory. -- -- We can safely ignore the warning, we turn it off by passing the '-c -- "core.safecrlf=false"' argument to git-diff. local opts = config.diff_opts local out = git_command({ '-c', 'core.safecrlf=false', 'diff', '--color=never', '--' .. (opts.indent_heuristic and '' or 'no-') .. 'indent-heuristic', '--diff-algorithm=' .. opts.algorithm, '--patch-with-raw', '--unified=0', file_cmp, file_buf, }, { -- git-diff implies --exit-code ignore_error = true, }) for _, line in ipairs(out) do if vim.startswith(line, '@@') then results[#results + 1] = Hunks.parse_diff_line(line) elseif #results > 0 then local r = results[#results] --- @cast r -? if line:sub(1, 1) == '-' then r.removed.lines[#r.removed.lines + 1] = line:sub(2) elseif line:sub(1, 1) == '+' then r.added.lines[#r.added.lines + 1] = line:sub(2) end end end os.remove(file_buf) os.remove(file_cmp) return results end return M neovim-gitsigns-2.0.0/lua/gitsigns/diff_int.lua000066400000000000000000000132571513053142700215410ustar00rootroot00000000000000local async = require('gitsigns.async') local uv = vim.uv or vim.loop ---@diagnostic disable-line: deprecated local create_hunk = require('gitsigns.hunks').create_hunk local config = require('gitsigns.config').config --- @return fun(v:any): string encode --- @return fun(v:string): any decode local function getencdec() local m = jit and package.preload['string.buffer'] and require('string.buffer') or vim.mpack --- @diagnostic disable-next-line: need-check-nil, undefined-field, return-type-mismatch --- EmmyLuaLs/emmylua-analyzer-rust#697 return m.encode, m.decode end --- @async --- @generic T, R --- @param f fun(...:T...): R... --- @param ... T... --- @return R... local function new_thread(f, ...) local args = { ... } --- @type T[] return async.await(1, function(cb) local encode, decode = getencdec() local worker = uv.new_work(function(getencdec_bc, f_bc, argse) local getencdec0 = getencdec or assert(loadstring(getencdec_bc --[[@as string]])) local encode0, decode0 = getencdec0() local args0 = decode0(argse) --[[@as any[] ]] local f0 = assert(loadstring(f_bc)) return encode0(f0(unpack(args0))) end, function(r) cb(decode(r --[[@as string]])) end) local getencdec_bc = string.dump(getencdec) local f_bc = string.dump(f) worker:queue(getencdec_bc, f_bc, encode(args)) end) end local M = {} --- @alias Gitsigns.Region [integer, string, integer, integer] --- @alias Gitsigns.RawHunk [integer, integer, integer, integer] ---@param a string ---@param b string ---@param opts Gitsigns.DiffOpts ---@param linematch? boolean ---@return Gitsigns.RawHunk[] local function run_diff(a, b, opts, linematch) local linematch0 --- @type integer? if linematch ~= false then linematch0 = opts.linematch end --- @diagnostic disable-next-line: deprecated return (vim.text and vim.text.diff or vim.diff)(a, b, { result_type = 'indices', algorithm = opts.algorithm, indent_heuristic = opts.indent_heuristic, ignore_whitespace = opts.ignore_whitespace, ignore_whitespace_change = opts.ignore_whitespace_change, ignore_whitespace_change_at_eol = opts.ignore_whitespace_change_at_eol, ignore_blank_lines = opts.ignore_blank_lines, linematch = linematch0, }) --[[@as Gitsigns.RawHunk[] ]] end --- @async --- @param a string --- @param b string --- @param opts Gitsigns.DiffOpts --- @param linematch? boolean --- @return Gitsigns.RawHunk[] local function run_diff_async(a, b, opts, linematch) return new_thread(run_diff, a, b, opts, linematch) end --- @param fa string[] --- @param fb string[] --- @param rawhunks Gitsigns.RawHunk[] --- @return Gitsigns.Hunk.Hunk[] local function tohunks(fa, fb, rawhunks) local hunks = {} --- @type Gitsigns.Hunk.Hunk[] for _, r in ipairs(rawhunks) do local rs, rc, as, ac = r[1], r[2], r[3], r[4] local hunk = create_hunk(rs, rc, as, ac) if rc > 0 then for i = rs, rs + rc - 1 do hunk.removed.lines[#hunk.removed.lines + 1] = fa[i] or '' end if rs + rc >= #fa and fa[#fa] ~= '' then hunk.removed.no_nl_at_eof = true end end if ac > 0 then for i = as, as + ac - 1 do hunk.added.lines[#hunk.added.lines + 1] = fb[i] or '' end if as + ac >= #fb and fb[#fb] ~= '' then hunk.added.no_nl_at_eof = true end end hunks[#hunks + 1] = hunk end return hunks end --- @async --- @param fa string[] --- @param fb string[] --- @param linematch? boolean --- @return Gitsigns.Hunk.Hunk[] function M.run_diff(fa, fb, linematch) local run_diff0 = config._threaded_diff and vim.is_thread and run_diff_async or run_diff local a = table.concat(fa, '\n') local b = table.concat(fb, '\n') return tohunks(fa, fb, run_diff0(a, b, config.diff_opts, linematch)) end local gaps_between_regions = 5 --- @param hunks Gitsigns.Hunk.Hunk[] --- @return Gitsigns.Hunk.Hunk[] local function denoise_hunks(hunks) ---@diagnostic disable-next-line: assign-type-mismatch -- Denoise the hunks local ret = { hunks[1] } for j = 2, #hunks do local h, n = ret[#ret], hunks[j] if not h or not n then break end if n.added.start - h.added.start - h.added.count < gaps_between_regions then h.added.count = n.added.start + n.added.count - h.added.start h.removed.count = n.removed.start + n.removed.count - h.removed.start if h.added.count > 0 or h.removed.count > 0 then h.type = 'change' end else ret[#ret + 1] = n end end return ret end --- @param removed string[] --- @param added string[] --- @return Gitsigns.Region[] removed --- @return Gitsigns.Region[] added function M.run_word_diff(removed, added) local adds = {} --- @type Gitsigns.Region[] local rems = {} --- @type Gitsigns.Region[] if #removed ~= #added then return rems, adds end for i = 1, #removed do local rmd = removed[i] --- @cast rmd -? local add = added[i] --- @cast add -? -- pair lines by position local a = table.concat(vim.split(rmd, ''), '\n') local b = table.concat(vim.split(add, ''), '\n') local hunks = {} --- @type Gitsigns.Hunk.Hunk[] for _, r in ipairs(run_diff(a, b, config.diff_opts)) do local rs, rc, as, ac = r[1], r[2], r[3], r[4] -- Balance of the unknown offset done in hunk_func if rc == 0 then rs = rs + 1 end if ac == 0 then as = as + 1 end hunks[#hunks + 1] = create_hunk(rs, rc, as, ac) end hunks = denoise_hunks(hunks) for _, h in ipairs(hunks) do adds[#adds + 1] = { i, h.type, h.added.start, h.added.start + h.added.count } rems[#rems + 1] = { i, h.type, h.removed.start, h.removed.start + h.removed.count } end end return rems, adds end return M neovim-gitsigns-2.0.0/lua/gitsigns/gh.lua000066400000000000000000000045671513053142700203610ustar00rootroot00000000000000local async = require('gitsigns.async') local log = require('gitsigns.debug.log') local system = require('gitsigns.system').system --- @type async fun(cmd: string[], opts?: vim.SystemOpts): vim.SystemCompleted local asystem = async.wrap(3, system) --- @class gitsigns.gh.PrInfo --- @field url string --- @field number string local M = {} --- @async --- @param args string[] --- @param cwd? string --- @return table? json local function gh_cmd(args, cwd) if vim.fn.executable('gh') == 0 then log.eprintf('Could not find gh command') return end --- @diagnostic disable-next-line: param-type-not-match EmmyLuaLs/emmylua-analyzer-rust#594 local obj = asystem({ 'gh', unpack(args) }, { cwd = cwd }) --- @cast obj.stderr -? if obj.code ~= 0 then if obj.stderr:match( 'none of the git remotes configured for this repository point to a known GitHub host' ) then return end log.eprintf( "Error running 'gh %s', code=%d: %s", table.concat(args, ' '), obj.code, obj.stderr or '[no stderr]' ) return end return vim.json.decode(assert(obj.stdout)) end --- @async --- @param cwd? string --- @return string? : The URL of the current repository local function repo_url(cwd) local res = gh_cmd({ 'repo', 'view', '--json', 'url' }, cwd) if res then return res.url end end --- @async function M.commit_url(sha, cwd) local url = repo_url(cwd) if url then return ('%s/commit/%s'):format(url, sha) end end --- Requests a list of GitHub PRs associated with the given commit SHA --- @async --- @param sha string --- @param cwd string --- @return gitsigns.gh.PrInfo[]? : Array of PR object local function associated_prs(sha, cwd) return gh_cmd({ 'pr', 'list', '--search', sha, '--state', 'merged', '--json', 'url,number', }, cwd) end --- @async --- @param sha string --- @param toplevel string --- @return Gitsigns.LineSpec function M.create_pr_linespec(sha, toplevel) local ret = {} --- @type Gitsigns.LineSpec local prs = associated_prs(sha, toplevel) if prs and next(prs) then ret[#ret + 1] = { '(', 'Title' } for i, pr in ipairs(prs) do ret[#ret + 1] = { ('#%s'):format(pr.number), 'Title', pr.url } if i < #prs then ret[#ret + 1] = { ', ', 'NormalFloat' } end end ret[#ret + 1] = { ') ', 'Title' } end return ret end return M neovim-gitsigns-2.0.0/lua/gitsigns/git.lua000066400000000000000000000167101513053142700205370ustar00rootroot00000000000000local log = require('gitsigns.debug.log') local async = require('gitsigns.async') local util = require('gitsigns.util') local Repo = require('gitsigns.git.repo') local errors = require('gitsigns.git.errors') local M = {} M.Repo = Repo --- @class Gitsigns.GitObj --- @field file string --- @field encoding string --- @field i_crlf? boolean Object has crlf --- @field w_crlf? boolean Working copy has crlf --- @field mode_bits string --- --- Revision the object is tracking against. Nil for index --- @field revision? string --- --- The fixed object name to use. Nil for untracked. --- @field object_name? string --- --- The path of the file relative to toplevel. Used to --- perform git operations. Nil if file does not exist --- @field relpath? string --- --- Used for tracking moved files --- @field orig_relpath? string --- --- @field repo Gitsigns.Repo --- @field has_conflicts? boolean --- --- @field _lock Gitsigns.async.Semaphore local Obj = {} Obj.__index = Obj M.Obj = Obj --- @async --- @param revision? string --- @return string? err function Obj:change_revision(revision) self.revision = util.norm_base(revision) return self:refresh() end --- @async --- @param fn async fun() function Obj:lock(fn) local timer = vim.defer_fn(function() log.eprint('Lock was not released') self._lock:release() end, 2000) self._lock:with(function() timer:stop() timer:close() return fn() end) end --- @async --- @return string? err function Obj:refresh() local info, err = self.repo:file_info(self.file, self.revision) if err then log.eprint(err) end if not info then return err end self.relpath = info.relpath self.object_name = info.object_name self.mode_bits = info.mode_bits self.has_conflicts = info.has_conflicts self.i_crlf = info.i_crlf self.w_crlf = info.w_crlf end function Obj:from_tree() return Repo.from_tree(self.revision) end --- @async --- @param revision? string --- @param relpath? string --- @return string[] stdout, string? stderr function Obj:get_show_text(revision, relpath) relpath = relpath or self.relpath if revision and not relpath then log.dprint('no relpath') return {} end local object = revision and (revision .. ':' .. relpath) or self.object_name if not object then log.dprint('no revision or object_name') return { '' } end local stdout, stderr = self.repo:get_show_text(object, self.encoding) -- detect renames if revision and stderr and ( stderr:match(errors.e.path_does_not_exist) or stderr:match(errors.e.path_exist_on_disk_but_not_in) ) then --- @cast relpath -? log.dprintf('%s not found in %s looking for renames', relpath, revision) local old_path = self.repo:log_rename_status(revision, relpath) if old_path then log.dprintf('found rename %s -> %s', old_path, relpath) stdout, stderr = self.repo:get_show_text(revision .. ':' .. old_path, self.encoding) end end if not self.i_crlf and self.w_crlf then -- Add cr -- Do not add cr to the newline at the end of file for i = 1, #stdout - 1 do stdout[i] = stdout[i] .. '\r' end end return stdout, stderr end --- @param file string local function autocmd_changed(file) vim.schedule(function() vim.api.nvim_exec_autocmds('User', { pattern = 'GitSignsChanged', modeline = false, data = { file = file }, }) end) end --- @async function Obj:unstage_file() self.repo:command({ 'reset', self.file }) autocmd_changed(self.file) end --- @async --- @param contents? string[] --- @param lnum? integer|[integer, integer] --- @param revision? string --- @param opts? Gitsigns.BlameOpts --- @return table --- @return table function Obj:run_blame(contents, lnum, revision, opts) return require('gitsigns.git.blame').run_blame(self, contents, lnum, revision, opts) end --- @async --- @private function Obj:ensure_file_in_index() if self.object_name and not self.has_conflicts then return end if not self.object_name then -- If there is no object_name then it is not yet in the index so add it self.repo:command({ 'add', '--intent-to-add', self.file }) else -- Update the index with the common ancestor (stage 1) which is what bcache -- stores self.repo:update_index(self.mode_bits, self.object_name, assert(self.relpath), true) end self:refresh() end --- @async --- Stage 'lines' as the entire contents of the file --- @param lines string[] function Obj:stage_lines(lines) local relpath = assert(self.relpath) local new_object = self.repo:hash_object(relpath, lines) self.repo:update_index(self.mode_bits, new_object, relpath) autocmd_changed(self.file) end local sleep = async.wrap(2, function(duration, cb) vim.defer_fn(cb, duration) end) --- @async --- @param hunks Gitsigns.Hunk.Hunk[] --- @param invert? boolean --- @return string? err function Obj:stage_hunks(hunks, invert) self:ensure_file_in_index() local relpath = assert(self.relpath) local patch = require('gitsigns.hunks').create_patch(relpath, hunks, self.mode_bits, invert) if not self.i_crlf and self.w_crlf then -- Remove cr for i, p in ipairs(patch) do patch[i] = p:gsub('\r$', '') end end local stat, err = pcall(function() self.repo:command({ 'apply', '--whitespace=nowarn', '--cached', '--unidiff-zero', '-', }, { stdin = patch, }) end) if not stat then return err end -- Staging operations cause IO of the git directory so wait some time -- for the changes to settle. sleep(100) autocmd_changed(self.file) end --- @async --- @param file string Absolute path or relative to toplevel --- @param revision string? --- @param encoding string --- @param gitdir string? --- @param toplevel string? --- @return Gitsigns.GitObj? function Obj.new(file, revision, encoding, gitdir, toplevel) local cwd = toplevel if not cwd and util.Path.is_abs(file) then cwd = vim.fn.fnamemodify(file, ':h') end local repo, err = Repo.get(cwd, gitdir, toplevel) if not repo then log.dprint('Not in git repo') if err and not err:match(errors.e.not_in_git) and not err:match(errors.e.worktree) then log.eprint(err) end return end if vim.startswith(vim.fn.fnamemodify(file, ':p'), vim.fn.fnamemodify(repo.gitdir, ':p')) then -- Normally this check would be caught (unintended) in the above -- block, as gitdir resolution will fail if `file` is inside a gitdir. -- If gitdir is explicitly passed (or set in the env with GIT_DIR) -- then resolution will succeed, but we still don't want to -- attach if `file` is inside the gitdir. log.dprint('In gitdir') return end -- When passing gitdir and toplevel, suppress stderr when resolving the file local silent = gitdir ~= nil and toplevel ~= nil revision = util.norm_base(revision) local info, err2 = repo:file_info(file, revision) if err2 and not silent then log.eprint(err2) end if not info then return end if info.relpath then file = util.Path.join(repo.toplevel, info.relpath) end local self = setmetatable({}, Obj) self.repo = repo self.file = util.cygpath(file, 'unix') self.revision = revision self.encoding = encoding self.relpath = info.relpath self.object_name = info.object_name self.mode_bits = info.mode_bits self.has_conflicts = info.has_conflicts self.i_crlf = info.i_crlf self.w_crlf = info.w_crlf self._lock = async.semaphore(1) return self end return M neovim-gitsigns-2.0.0/lua/gitsigns/git/000077500000000000000000000000001513053142700200275ustar00rootroot00000000000000neovim-gitsigns-2.0.0/lua/gitsigns/git/blame.lua000066400000000000000000000170511513053142700216160ustar00rootroot00000000000000local uv = vim.uv or vim.loop ---@diagnostic disable-line: deprecated local error_once = require('gitsigns.message').error_once local log = require('gitsigns.debug.log') local util = require('gitsigns.util') --- @class Gitsigns.CommitInfo --- @field author string --- @field author_mail string --- @field author_time integer --- @field author_tz string --- @field committer string --- @field committer_mail string --- @field committer_time integer --- @field committer_tz string --- @field summary string --- @field sha string --- @field abbrev_sha string --- @field boundary? true --- @class Gitsigns.BlameInfoPublic: Gitsigns.BlameInfo, Gitsigns.CommitInfo --- @field body? string[] --- @field hunk_no? integer --- @field num_hunks? integer --- @field hunk? string[] --- @field hunk_head? string --- @class Gitsigns.BlameInfo --- @field orig_lnum integer --- @field final_lnum integer --- @field commit Gitsigns.CommitInfo --- @field filename string --- @field previous_filename? string --- @field previous_sha? string local NOT_COMMITTED = { author = 'Not Committed Yet', author_mail = '', committer = 'Not Committed Yet', committer_mail = '', } local M = {} --- @param file string --- @return Gitsigns.CommitInfo local function not_committed(file) local time = os.time() return { sha = string.rep('0', 40), abbrev_sha = string.rep('0', 8), author = 'Not Committed Yet', author_mail = '', author_tz = '+0000', author_time = time, committer = 'Not Committed Yet', committer_time = time, committer_mail = '', committer_tz = '+0000', summary = 'Version of ' .. file, } end --- @param file string --- @param lnum integer --- @return Gitsigns.BlameInfo function M.get_blame_nc(file, lnum) return { orig_lnum = 0, final_lnum = lnum, commit = not_committed(file), filename = file, } end ---@param x any ---@return integer local function asinteger(x) return assert(util.tointeger(x)) end --- @param readline fun(): string? --- @param commits table --- @param result table local function incremental_iter(readline, commits, result) local line = assert(readline()) local sha, orig_lnum_str, final_lnum_str, size_str = line:match('(%x+) (%d+) (%d+) (%d+)') if not sha then error(("Could not parse sha from line: '%s'"):format(line)) end local orig_lnum = asinteger(orig_lnum_str) local final_lnum = asinteger(final_lnum_str) local size = asinteger(size_str) local commit = commits[sha] or { sha = sha, abbrev_sha = sha:sub(1, 8) --[[@as string]], } --- @type string?, string? local previous_sha, previous_filename line = assert(readline()) -- filename terminates the entry while not line:match('^filename ') do local key, value = line:match('^([^%s]+) (.*)') if key == 'previous' then previous_sha, previous_filename = line:match('^previous (%x+) (.*)') elseif key then key = key:gsub('%-', '_') --- @type string if vim.endswith(key, '_time') then commit[key] = asinteger(value) else commit[key] = value end else commit[line] = true if line ~= 'boundary' then log.dprintf("Unknown tag: '%s'", line) end end line = assert(readline()) end local filename = assert(line:match('^filename (.*)')) -- New in git 2.41: -- The output given by "git blame" that attributes a line to contents -- taken from the file specified by the "--contents" option shows it -- differently from a line attributed to the working tree file. if commit.author_mail == '' or commit.author_mail == 'External file (--contents)' then commit = vim.tbl_extend('force', commit, NOT_COMMITTED) end commits[sha] = commit --[[@as Gitsigns.CommitInfo]] for j = 0, size - 1 do result[final_lnum + j] = { final_lnum = final_lnum + j, orig_lnum = orig_lnum + j, commit = commits[sha], filename = filename, previous_filename = previous_filename, previous_sha = previous_sha, } end end --- @param data string --- @param partial? string --- @return string[] lines --- @return string? partial local function data_to_lines(data, partial) local lines = vim.split(data, '\n') if partial then lines[1] = partial .. lines[1] partial = nil end -- if data doesn't end with a newline, then the last line is partial if lines[#lines] ~= '' then partial = lines[#lines] end -- Clear the last line as it will be empty of the partial line lines[#lines] = nil return lines, partial end --- @param f fun(readline: fun(): string?)) --- @return fun(data: string?) local function buffered_line_reader(f) --- @param data string? return coroutine.wrap(function(data) if not data then return end local data_lines, partial_line = data_to_lines(data) local i = 0 --- @async local function readline(peek) if not data_lines[i + 1] then -- No more data, wait for more data = coroutine.yield() if not data then -- No more data, return the partial line if there is one return partial_line end data_lines, partial_line = data_to_lines(data, partial_line) i = 0 end if peek then return data_lines[i + 1] end i = i + 1 return data_lines[i] end while readline(true) do f(readline) end end) end --- @async --- @param obj Gitsigns.GitObj --- @param contents? string[] --- @param lnum? integer|[integer, integer] --- @param revision? string --- @param opts? Gitsigns.BlameOpts --- @return table --- @return table function M.run_blame(obj, contents, lnum, revision, opts) local ret = {} --- @type table if not obj.object_name or obj.repo.abbrev_head == '' then assert(contents, 'contents must be provided for untracked files') -- As we support attaching to untracked files we need to return something if -- the file isn't isn't tracked in git. -- If abbrev_head is empty, then assume the repo has no commits local commit = not_committed(obj.file) for i in ipairs(contents) do ret[i] = { orig_lnum = 0, final_lnum = i, commit = commit, filename = obj.file, } end return ret, {} end --- @type Gitsigns.BlameOpts --- EmmyLuaLs/emmylua-analyzer-rust#921 opts = opts or {} local ignore_file = obj.repo.toplevel .. '/.git-blame-ignore-revs' local commits = {} --- @type table local reader = buffered_line_reader(function(readline) incremental_iter(readline, commits, ret) end) --- @param data string? local function on_stdout(_, data) reader(data) end local contents_str = contents and table.concat(contents, '\n') or nil local _, stderr = obj.repo:command( util.flatten({ 'blame', '--incremental', contents and { '--contents', '-' }, opts.ignore_whitespace and '-w' or nil, lnum and { '-L', type(lnum) == 'table' and (lnum[1] .. ',' .. lnum[2]) or (lnum .. ',+1') }, opts.extra_opts, uv.fs_stat(ignore_file) and { '--ignore-revs-file', ignore_file }, revision, '--', obj.file, }), { stdin = contents_str, stdout = on_stdout, ignore_error = true, } ) if stderr then local msg = 'Error running git-blame: ' .. stderr error_once(msg) log.eprint(msg) end return ret, commits end return M neovim-gitsigns-2.0.0/lua/gitsigns/git/cmd.lua000066400000000000000000000032531513053142700213000ustar00rootroot00000000000000local async = require('gitsigns.async') local log = require('gitsigns.debug.log') local util = require('gitsigns.util') local asystem = async.wrap(3, require('gitsigns.system').system) --- @class Gitsigns.Git.JobSpec : vim.SystemOpts --- @field ignore_error? boolean --- @async --- @param args string[] --- @param spec? Gitsigns.Git.JobSpec --- @return string[] stdout, string? stderr, integer code local function git_command(args, spec) spec = spec or {} if spec.cwd then -- cwd must be a windows path and not a unix path spec.cwd = util.cygpath(spec.cwd) end local cmd = { 'git', '--no-pager', '--no-optional-locks', '--literal-pathspecs', '-c', 'gc.auto=0', -- Disable auto-packing which emits messages to stderr } vim.list_extend(cmd, args) if spec.text == nil then spec.text = true end --- @type vim.SystemCompleted local obj = asystem(cmd, spec) async.schedule() if not spec.ignore_error and obj.code > 0 then log.eprintf( "Received exit code %d when running command\n'%s':\n%s", obj.code, table.concat(cmd, ' '), obj.stderr ) end local stdout_lines = vim.split(obj.stdout or '', '\n') if spec.text then -- If stdout ends with a newline, then remove the final empty string after -- the split if stdout_lines[#stdout_lines] == '' then stdout_lines[#stdout_lines] = nil end end if log.verbose then log.vprintf('%d lines:', #stdout_lines) for i = 1, math.min(10, #stdout_lines) do log.vprintf('\t%s', stdout_lines[i]) end end if obj.stderr == '' then obj.stderr = nil end return stdout_lines, obj.stderr, obj.code end return git_command neovim-gitsigns-2.0.0/lua/gitsigns/git/errors.lua000066400000000000000000000013171513053142700220500ustar00rootroot00000000000000return { e = { worktree = vim.pesc('fatal: this operation must be run in a work tree'), -- Match both: -- 'fatal: not a git repository (or any of the parent directories)' -- 'fatal: not a git repository (or any parent up to mount point /)' not_in_git = 'fatal: not a git repository', path_does_not_exist = "fatal: path .* does not exist in '.*'", path_exist_on_disk_but_not_in = "fatal: path .* exists on disk, but not in '.*'", path_is_outside_worktree = "fatal: .*: '.*' is outside repository at '.*'", ambiguous_head = "fatal: ambiguous argument 'HEAD'", }, w = { could_not_open_directory = '^warning: could not open directory .*: No such file or directory', }, } neovim-gitsigns-2.0.0/lua/gitsigns/git/repo.lua000066400000000000000000000363611513053142700215100ustar00rootroot00000000000000local async = require('gitsigns.async') local git_command = require('gitsigns.git.cmd') local config = require('gitsigns.config').config local log = require('gitsigns.debug.log') local util = require('gitsigns.util') local errors = require('gitsigns.git.errors') local Watcher = require('gitsigns.git.repo.watcher') local check_version = require('gitsigns.git.version').check local uv = vim.uv or vim.loop ---@diagnostic disable-line: deprecated --- @class Gitsigns.RepoInfo --- @field gitdir string --- @field toplevel string --- @field detached boolean --- @field abbrev_head string --- @class Gitsigns.Repo : Gitsigns.RepoInfo --- --- Username configured for the repo. --- Needed for to determine "You" in current line blame. --- @field username string --- @field private _watcher? Gitsigns.Repo.Watcher local M = {} --- @param gitdir string --- @return boolean local function is_rebasing(gitdir) return util.Path.exists(util.Path.join(gitdir, 'rebase-merge')) or util.Path.exists(util.Path.join(gitdir, 'rebase-apply')) end --- Return the abbreviated ref for HEAD (or short SHA if detached). --- Equivalent to `git rev-parse --abbrev-ref HEAD` --- @param gitdir string Must be an absolute path to the .git directory --- @return string abbrev_head local function abbrev_head(gitdir) local head_path = util.Path.join(gitdir, 'HEAD') -- TODO(lewis6991): should this be async? vim.wait(1000, function() return not util.Path.exists(head_path .. '.lock') end, 10, true) local f = assert(io.open(head_path, 'r')) local head = f:read('*l') f:close() -- HEAD content is either: -- "ref: refs/heads/" -- "" (detached HEAD) local refpath = head:match('^ref:%s*(.+)$') if refpath then -- Extract last path component (branch name) return refpath:match('([^/]+)$') or refpath end assert(head:find('^[%x]+$'), 'Invalid HEAD content: ' .. head) -- Detached HEAD -> like `git rev-parse --abbrev-ref HEAD`, return literal "HEAD" local short_sha = log.debug_mode() and 'HEAD' or head:sub(1, 7) if is_rebasing(gitdir) then short_sha = short_sha .. '(rebasing)' end return short_sha end --- Registers a callback to be invoked on update events. --- --- The provided callback function `cb` will be stored and called when an update --- occurs. Returns a deregister function that, when called, will remove the --- callback from the watcher. --- --- @param callback fun() Callback function to be invoked on update. --- @return fun() deregister Function to remove the callback from the watcher. function M:on_update(callback) assert(self._watcher, 'Watcher not initialized') return self._watcher:on_update(callback) end --- Run git command the with the objects gitdir and toplevel --- @async --- @param args table --- @param spec? Gitsigns.Git.JobSpec --- @return string[] stdout --- @return string? stderr --- @return integer code function M:command(args, spec) spec = spec or {} spec.cwd = self.toplevel local args0 = { '--git-dir', self.gitdir } if self.detached then -- If detached, we need to set the work tree to the toplevel so that git -- commands work correctly. args0 = vim.list_extend(args0, { '--work-tree', self.toplevel }) end vim.list_extend(args0, args) return git_command(args0, spec) end --- @async --- @param base string? --- @return string[] function M:files_changed(base) if base and base ~= ':0' then local results = self:command({ 'diff', '--name-status', base }) for i, result in ipairs(results) do results[i] = vim.split(result:gsub('\t', ' '), ' ', { plain = true })[2] end return results end local results = self:command({ 'status', '--porcelain', '--ignore-submodules' }) local ret = {} --- @type string[] for _, line in ipairs(results) do if line:sub(1, 2):match('^.M') then ret[#ret + 1] = line:sub(4, -1) end end return ret end --- @param encoding string --- @return boolean local function iconv_supported(encoding) -- TODO(lewis6991): needs https://github.com/neovim/neovim/pull/21924 if vim.startswith(encoding, 'utf-16') or vim.startswith(encoding, 'utf-32') then return false end return true end --- @async --- Get version of file in the index, return array lines --- @param object string --- @param encoding? string --- @return string[] stdout, string? stderr function M:get_show_text(object, encoding) local stdout, stderr = self:command({ 'show', object }, { text = false, ignore_error = true }) if encoding and encoding ~= 'utf-8' and iconv_supported(encoding) then for i, l in ipairs(stdout) do stdout[i] = vim.iconv(l, encoding, 'utf-8') end end return stdout, stderr end --- @type table local repo_cache = setmetatable({}, { __mode = 'v' }) --- @async --- @private --- @param info Gitsigns.RepoInfo --- @return Gitsigns.Repo function M._new(info) --- @type Gitsigns.Repo local self = setmetatable(info, { __index = M }) self.username = self:command({ 'config', 'user.name' }, { ignore_error = true })[1] if config.watch_gitdir.enable then self._watcher = Watcher.new(self.gitdir) self._watcher:on_head_update(function() self.abbrev_head = abbrev_head(self.gitdir) log.dprintf('HEAD changed, updating abbrev_head to %s', self.abbrev_head) end) end return self end function M:has_watcher() return self._watcher ~= nil end local sem = async.semaphore(1) --- @async --- @param cwd? string --- @param gitdir? string --- @param toplevel? string --- @return Gitsigns.Repo? repo --- @return string? err function M.get(cwd, gitdir, toplevel) --- EmmyLuaLs/emmylua-analyzer-rust#659 --- @return Gitsigns.Repo? repo --- @return string? err return sem:with(function() local info, err = M.get_info(cwd, gitdir, toplevel) if not info then return nil, err end repo_cache[info.gitdir] = repo_cache[info.gitdir] or M._new(info) return repo_cache[info.gitdir] end) end --- @async --- @param gitdir string --- @param head_str string --- @param cwd string --- @return string local function process_abbrev_head(gitdir, head_str, cwd) if head_str ~= 'HEAD' then return head_str end local short_sha = git_command({ 'rev-parse', '--short', 'HEAD' }, { ignore_error = true, cwd = cwd, })[1] or '' -- Make tests easier if short_sha ~= '' and log.debug_mode() then short_sha = 'HEAD' end if is_rebasing(gitdir) then return short_sha .. '(rebasing)' end return short_sha end --- @async --- @param dir? string --- @param gitdir? string --- @param worktree? string --- @return Gitsigns.RepoInfo? info, string? err function M.get_info(dir, gitdir, worktree) -- Does git rev-parse have --absolute-git-dir, added in 2.13: -- https://public-inbox.org/git/20170203024829.8071-16-szeder.dev@gmail.com/ local has_abs_gd = check_version(2, 13) -- Wait for internal scheduler to settle before running command (#215) async.schedule() if dir and not uv.fs_stat(dir) then -- Cwd can be deleted externally, so check if it exists (see #1331) log.dprintf("dir '%s' does not exist", dir) return end -- Explicitly fallback to env vars for better debug gitdir = gitdir or vim.env.GIT_DIR worktree = worktree or vim.env.GIT_WORK_TREE or vim.fs.dirname(gitdir) -- gitdir and worktree must be provided together from `man git`: -- > Specifying the location of the ".git" directory using this option (or GIT_DIR environment -- > variable) turns off the repository discovery that tries to find a directory with ".git" -- > subdirectory (which is how the repository and the top-level of the working tree are -- > discovered), and tells Git that you are at the top level of the working tree. If you are -- > not at the top-level directory of the working tree, you should tell Git where the -- > top-level of the working tree is, with the --work-tree= option (or GIT_WORK_TREE -- > environment variable) local stdout, stderr, code = git_command( util.flatten({ gitdir and { '--git-dir', gitdir }, worktree and { '--work-tree', worktree }, 'rev-parse', '--show-toplevel', has_abs_gd and '--absolute-git-dir' or '--git-dir', '--abbrev-ref', 'HEAD', }), { ignore_error = true, -- Worktree may be a relative path, so don't set cwd when it is provided. cwd = not worktree and dir or nil, } ) -- If the repo has no commits yet, rev-parse will fail. Ignore this error. if code > 0 and stderr and stderr:match(errors.e.ambiguous_head) then code = 0 end if code > 0 then return nil, string.format('got stderr: %s', stderr or '') end if #stdout < 3 then return nil, string.format('incomplete stdout: %s', table.concat(stdout, '\n')) end --- @cast stdout [string, string, string] local toplevel_r = stdout[1] local gitdir_r = stdout[2] -- On windows, git will emit paths with `/` but dir may contain `\` so need to -- normalize. if dir and not vim.startswith(vim.fs.normalize(dir), toplevel_r) then log.dprintf("'%s' is outside worktree '%s'", dir, toplevel_r) -- outside of worktree return end if not has_abs_gd then gitdir_r = assert(uv.fs_realpath(gitdir_r)) end if gitdir and not worktree and gitdir ~= gitdir_r then log.eprintf('expected gitdir to be %s, got %s', gitdir, gitdir_r) end return { toplevel = toplevel_r, gitdir = gitdir_r, abbrev_head = process_abbrev_head(gitdir_r, stdout[3], toplevel_r), detached = toplevel_r and gitdir_r ~= toplevel_r .. '/.git', } end --- @class (exact) Gitsigns.Repo.LsTree.Result --- @field relpath string --- @field mode_bits? string --- @field object_name? string --- @field object_type? 'blob'|'tree'|'commit' --- @async --- @param path string --- @param revision string --- @return Gitsigns.Repo.LsTree.Result? info --- @return string? err function M:ls_tree(path, revision) local results, stderr, code = self:command({ '-c', 'core.quotepath=off', 'ls-tree', revision, path, }, { ignore_error = true }) if code > 0 then return nil, stderr or tostring(code) end local res = results[1] if not res then -- Not found, see if it was renamed log.dprintf('%s not found in %s looking for renames', path, revision) local old_path = self:diff_rename_status(revision, true)[path] if old_path then log.dprintf('found rename %s -> %s', old_path, path) return self:ls_tree(old_path, revision) end return nil, ('%s not found in %s'):format(path, revision) end local info, relpath = unpack(vim.split(res, '\t')) assert(info and relpath) local mode_bits, object_type, object_name = unpack(vim.split(info, '%s+')) --- @cast object_type 'blob'|'tree'|'commit' return { relpath = relpath, mode_bits = mode_bits, object_name = object_name, object_type = object_type, } end --- @class (exact) Gitsigns.Repo.LsFiles.Result --- @field relpath? string nil if file is not in working tree --- @field mode_bits? string --- @field object_name? string nil if file is untracked --- @field i_crlf? boolean (requires git version >= 2.9) --- @field w_crlf? boolean (requires git version >= 2.9) --- @field has_conflicts? true --- @async --- Get information about files in the index and the working tree --- @param file string --- @return Gitsigns.Repo.LsFiles.Result? info --- @return string? err function M:ls_files(file) local has_eol = check_version(2, 9) -- --others + --exclude-standard means ignored files won't return info, but -- untracked files will. Unlike file_info_tree which won't return untracked -- files. local results, stderr, code = self:command( util.flatten({ '-c', 'core.quotepath=off', 'ls-files', '--stage', '--others', '--exclude-standard', has_eol and '--eol', file, }), { ignore_error = true } ) -- ignore_error for the cases when we run: -- git ls-files --others exists/nonexist if code > 0 and (not stderr or not stderr:match(errors.e.path_does_not_exist)) then return nil, stderr or tostring(code) end local relpath_idx = has_eol and 2 or 1 local result = {} for _, line in ipairs(results) do local parts = vim.split(line, '\t') if #parts > relpath_idx then -- tracked file local attrs = vim.split(assert(parts[1]), '%s+') local stage = tonumber(attrs[3]) if stage <= 1 then result.mode_bits = attrs[1] result.object_name = attrs[2] else result.has_conflicts = true end if has_eol then result.relpath = parts[3] local eol = vim.split(assert(parts[2]), '%s+') result.i_crlf = eol[1] == 'i/crlf' result.w_crlf = eol[2] == 'w/crlf' else result.relpath = parts[2] end else -- untracked file result.relpath = parts[relpath_idx] end end return result end --- @param revision? string --- @return boolean function M.from_tree(revision) return revision ~= nil and not vim.startswith(revision, ':') end --- @async --- @param file string --- @param revision? string --- @return Gitsigns.Repo.LsFiles.Result? info --- @return string? err function M:file_info(file, revision) if M.from_tree(revision) then local info, err = self:ls_tree(file, assert(revision)) if err then return nil, err end if info and info.object_type == 'blob' then return { relpath = info.relpath, mode_bits = info.mode_bits, object_name = info.object_name, } end else local info, err = self:ls_files(file) if err then return nil, err end return info end end --- @async --- @param mode_bits string --- @param object string --- @param path string --- @param add? boolean function M:update_index(mode_bits, object, path, add) self:command(util.flatten({ 'update-index', add and '--add', '--cacheinfo', ('%s,%s,%s'):format(mode_bits, object, path), })) end --- @async --- @param path string --- @param lines string[] --- @return string function M:hash_object(path, lines) -- Concatenate the lines into a single string to ensure EOL -- is respected local text = table.concat(lines, '\n') local res = self:command({ 'hash-object', '-w', '--path', path, '--stdin' }, { stdin = text })[1] return assert(res) end --- @async --- @param revision string --- @param path string --- @return string? function M:log_rename_status(revision, path) local out = self:command({ 'log', '--follow', '--name-status', '--diff-filter=R', '--format=', revision .. '..HEAD', '--', path, }) local line = out[#out] if not line then return end return vim.split(line, '%s+')[2] end --- @async --- @param revision? string --- @param invert? boolean --- @return table function M:diff_rename_status(revision, invert) local out = self:command({ 'diff', '--name-status', '--find-renames', '--find-copies', '--cached', revision, }) local ret = {} --- @type table for _, l in ipairs(out) do local parts = vim.split(l, '%s+') if #parts == 3 then --- @cast parts [string, string, string] local stat, orig_file, new_file = parts[1], parts[2], parts[3] if vim.startswith(stat, 'R') then if invert then ret[new_file] = orig_file else ret[orig_file] = new_file end end end end return ret end return M neovim-gitsigns-2.0.0/lua/gitsigns/git/repo/000077500000000000000000000000001513053142700207745ustar00rootroot00000000000000neovim-gitsigns-2.0.0/lua/gitsigns/git/repo/watcher.lua000066400000000000000000000067741513053142700231520ustar00rootroot00000000000000local debounce_trailing = require('gitsigns.debounce').debounce_trailing local util = require('gitsigns.util') local log = require('gitsigns.debug.log') --- vim.inspect but on one line --- @param x any --- @return string local function inspect(x) return vim.inspect(x, { indent = '', newline = ' ' }) end --- @class Gitsigns.Repo.Watcher --- @field private update_callbacks table --- @field private head_update_callbacks table --- @field private handle uv.uv_fs_event_t --- @field private handler_debounced fun(weak_self:{ref:Gitsigns.Repo.Watcher}) --- @field private changed_files table --- @field private gitdir string --- @field private weak_repo {ref:Gitsigns.Repo} Weak reference to repo --- @field private _gc userdata? Used for garbage collection local Watcher = {} Watcher.__index = Watcher --- @param gitdir string --- @return Gitsigns.Repo.Watcher function Watcher.new(gitdir) local handle = assert(vim.uv.new_fs_event()) --- @type Gitsigns.Repo.Watcher local self = setmetatable({}, Watcher) self.update_callbacks = {} self.head_update_callbacks = {} self.changed_files = {} self.handle = handle self.handler_debounced = debounce_trailing(200, function(weak_self) vim.schedule(function() Watcher.handler2(weak_self) end) end) self._gc = util.gc_proxy(function() handle:stop() handle:close() end) log.dprintf('Starting git dir watcher on %s', gitdir) self.handle:start(gitdir, {}, Watcher.handler1(util.weak_ref(self))) return self end --- @param callback fun() Callback function to be invoked on update. function Watcher:on_head_update(callback) self.head_update_callbacks[callback] = true end --- @param callback fun() Callback function to be invoked on update. --- @return fun() deregister Function to remove the callback from the watcher. function Watcher:on_update(callback) self.update_callbacks[callback] = true return function() self.update_callbacks[callback] = nil end end --- @private --- @param weak_self {ref:Gitsigns.Repo.Watcher} function Watcher.handler2(weak_self) local self = weak_self.ref if not self then return -- garbage collected end local head_changed = self.changed_files.HEAD or false self.changed_files = {} if head_changed then for cb in pairs(self.head_update_callbacks) do cb() end end for cb in pairs(self.update_callbacks) do vim.schedule(cb) end end function Watcher.handler1(weak_self) --- @param err string? --- @param filename string --- @param events { change: boolean?, rename: boolean? } return function(err, filename, events) local __FUNC__ = 'watcher.handler1' local watcher = weak_self.ref if not watcher then log.dprint('watcher was garbage collected') return end if err then log.dprintf('Git dir update error: %s', err) return end -- The luv docs say filename is passed as a string but it has been observed -- to sometimes be nil. -- https://github.com/lewis6991/gitsigns.nvim/issues/848 if not filename then log.eprint('No filename') return end for _, ex in ipairs({ '.watchman-cookie', 'index.lock', }) do if vim.startswith(filename, ex) then log.dprintf("Git dir update: '%s' %s (ignoring)", filename, inspect(events)) return end end log.dprintf("Git dir update: '%s' %s", filename, inspect(events)) watcher.changed_files[filename] = true watcher.handler_debounced(weak_self) end end return Watcher neovim-gitsigns-2.0.0/lua/gitsigns/git/version.lua000066400000000000000000000047431513053142700222270ustar00rootroot00000000000000local async = require('gitsigns.async') local gs_config = require('gitsigns.config') local log = require('gitsigns.debug.log') local err = require('gitsigns.message').error local system = require('gitsigns.system').system local tointeger = require('gitsigns.util').tointeger local M = {} --- @type fun(cmd: string[], opts?: vim.SystemOpts): vim.SystemCompleted local asystem = async.wrap(3, system) --- @class (exact) Gitsigns.Version --- @field major integer --- @field minor integer --- @field patch integer --- @param version string --- @return Gitsigns.Version local function parse_version(version) assert(version:match('%d+%.%d+%.%w+'), 'Invalid git version: ' .. version) local parts = vim.split(version, '%.') --- @cast parts [string, string, string] local patch --- @type integer if parts[3] == 'GIT' then patch = 0 else local patch_ver = vim.split(parts[3], '-') patch = assert(tointeger(patch_ver[1])) end return { patch = patch, major = assert(tointeger(parts[1])), minor = assert(tointeger(parts[2])), } end --- @async local function set_version() local version = gs_config.config._git_version if version ~= 'auto' then local ok, ret = pcall(parse_version, version) if ok then M.version = ret else err(ret --[[@as string]]) end return end --- @type vim.SystemCompleted local obj = asystem({ 'git', '--version' }) async.schedule() local line = vim.split(obj.stdout or '', '\n')[1] if not line then err("Unable to detect git version as 'git --version' failed to return anything") log.eprint(obj.stderr) return end -- Sometime 'git --version' returns an empty string (#948) if log.assert(type(line) == 'string', 'Unexpected output: ' .. line) then return end if log.assert(vim.startswith(line, 'git version'), 'Unexpected output: ' .. line) then return end local parts = vim.split(line, '%s+') M.version = parse_version(assert(parts[3])) end --- @async --- Usage: check_version{2,3} --- @param major? integer --- @param minor? integer --- @param patch? integer --- @return boolean function M.check(major, minor, patch) if not M.version then set_version() end if not M.version then return false elseif not major or not minor then return false elseif M.version.major < major then return false elseif minor and M.version.minor < minor then return false elseif patch and M.version.patch < patch then return false end return true end return M neovim-gitsigns-2.0.0/lua/gitsigns/highlight.lua000066400000000000000000000220171513053142700217200ustar00rootroot00000000000000local api = vim.api --- @class Gitsigns.Hldef --- @field [integer] string --- @field desc string --- @field hidden? boolean --- @field fg_factor? number local nvim10 = vim.fn.has('nvim-0.10') == 1 local M = {} --- Use array of dict so we can iterate deterministically --- Export for docgen --- @type table[] M.hls = {} --- @param s string --- @return string local function capitalise(s) return s:sub(1, 1):upper() .. s:sub(2) end ---@param staged boolean ---@param kind ''|'Nr'|'Ln'|'Cul' ---@param ty 'add'|'change'|'delete'|'changedelete'|'topdelete'|'untracked' ---@return string? highlight ---@return Gitsigns.Hldef? spec local function gen_hl(staged, kind, ty) local cty = capitalise(ty) local hl = ('GitSigns%s%s%s'):format(staged and 'Staged' or '', cty, kind) if kind == 'Ln' and (ty == 'delete' or 'ty' == 'topdelete') then return end local what --- @type string if kind == 'Nr' then what = 'number column (when `config.numhl == true`)' elseif kind == 'Ln' then what = 'buffer line (when `config.linehl == true`)' elseif kind == 'Cul' then what = 'the text (when the cursor is on the same line as the sign)' else what = 'the text' end local fallbacks --- @type string[] if staged then fallbacks = { ('GitSigns%s%s'):format(cty, kind) } elseif ty == 'changedelete' then fallbacks = { 'GitSignsChange' .. kind } elseif ty == 'topdelete' then fallbacks = { 'GitSignsDelete' .. kind } elseif ty == 'untracked' then fallbacks = { 'GitSignsAdd' .. kind } elseif kind == 'Nr' then fallbacks = { ('GitGutter%sLineNr'):format(cty), ('GitSigns%s'):format(cty), } elseif kind == 'Ln' then fallbacks = { ('GitGutter%sLine'):format(cty), ('SignifyLine%s'):format(cty), ('Diff%s'):format(cty), } elseif kind == 'Cul' then fallbacks = { ('GitSigns%s'):format(cty) } else fallbacks = { ('GitGutter%s'):format(cty), ('SignifySign%s'):format(cty), ty == 'add' and 'DiffAddedGutter' or ty == 'delete' and 'DiffRemovedGutter' or ty == 'change' and 'DiffModifiedGutter' or '???', ty == 'add' and (nvim10 and 'Added' or 'diffAdded') or ty == 'delete' and (nvim10 and 'Removed' or 'diffRemoved') or ty == 'change' and (nvim10 and 'Changed' or 'diffChanged') or '???', ('Diff%s'):format(cty), } end local sty = (staged and 'staged ' or '') --- @type Gitsigns.Hldef local spec = { desc = ("Used for %s of '%s' %ssigns."):format(what, ty, sty), fg_factor = staged and 0.5 or nil, unpack(fallbacks), } return hl, spec end for _, staged in ipairs({ false, true }) do for _, kind in ipairs({ '', 'Nr', 'Ln', 'Cul' }) do for _, ty in ipairs({ 'add', 'change', 'delete', 'changedelete', 'topdelete', 'untracked' }) do local hl, spec = gen_hl(staged, kind, ty) if hl then table.insert(M.hls, { [hl] = spec }) end end end end vim.list_extend(M.hls, { { GitSignsAddPreview = { 'GitGutterAddLine', 'SignifyLineAdd', 'DiffAdd', desc = 'Used for added lines in previews.', }, }, { GitSignsDeletePreview = { 'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete', desc = 'Used for deleted lines in previews.', }, }, { GitSignsNoEOLPreview = { 'DiffNoEOL', 'Constant', desc = 'Used for "No newline at end of file".', }, }, { GitSignsCurrentLineBlame = { 'NonText', desc = 'Used for current line blame.' } }, { GitSignsAddInline = { 'TermCursor', desc = 'Used for added word diff regions in inline previews.', }, }, { GitSignsDeleteInline = { 'TermCursor', desc = 'Used for deleted word diff regions in inline previews.', }, }, { GitSignsChangeInline = { 'TermCursor', desc = 'Used for changed word diff regions in inline previews.', }, }, { GitSignsAddLnInline = { 'GitSignsAddInline', desc = 'Used for added word diff regions when `config.word_diff == true`.', }, }, { GitSignsChangeLnInline = { 'GitSignsChangeInline', desc = 'Used for changed word diff regions when `config.word_diff == true`.', }, }, { GitSignsDeleteLnInline = { 'GitSignsDeleteInline', desc = 'Used for deleted word diff regions when `config.word_diff == true`.', }, }, -- Currently unused -- {GitSignsAddLnVirtLn = {'GitSignsAddLn'}}, -- {GitSignsChangeVirtLn = {'GitSignsChangeLn'}}, -- {GitSignsAddLnVirtLnInLine = {'GitSignsAddLnInline', }}, -- {GitSignsChangeVirtLnInLine = {'GitSignsChangeLnInline', }}, { GitSignsDeleteVirtLn = { 'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete', desc = 'Used for deleted lines shown by inline `preview_hunk_inline()` or `show_deleted()`.', }, }, { GitSignsDeleteVirtLnInLine = { 'GitSignsDeleteLnInline', desc = 'Used for word diff regions in lines shown by inline `preview_hunk_inline()` or `show_deleted()`.', }, }, { GitSignsVirtLnum = { 'GitSignsDeleteVirtLn', desc = 'Used for line numbers in inline hunks previews.', }, }, }) ---@param name string ---@return vim.api.keyset.get_hl_info local function get_hl(name) return api.nvim_get_hl(0, { name = name, link = false }) end --- @param hl_name string --- @return boolean local function is_hl_set(hl_name) local hl = get_hl(hl_name) local color = hl.fg or hl.bg or hl.reverse or hl.ctermfg or hl.ctermbg or hl.cterm and hl.cterm.reverse return color ~= nil end --- @param x? integer --- @param factor number --- @return integer? local function cmix(x, factor) if not x or factor == 0 then return x end local r = math.floor(x / 2 ^ 16) local x1 = x - (r * 2 ^ 16) local g = math.floor(x1 / 2 ^ 8) local b = math.floor(x1 - (g * 2 ^ 8)) local function mix(c, target, f) return math.floor(c + (target - c) * f) end -- If positive, lighten by mixing with 255 (white) -- If negative, darken by mixing with 0 (black) local target = factor > 0 and 255 or 0 factor = math.abs(factor) r = mix(r, target, factor) g = mix(g, target, factor) b = mix(b, target, factor) return math.floor(r * 2 ^ 16 + g * 2 ^ 8 + b) end local function dprintf(fmt, ...) dprintf = require('gitsigns.debug.log').dprintf dprintf(fmt, ...) end --- @param hl string --- @param hldef Gitsigns.Hldef --- @param is_bg_light boolean local function derive(hl, hldef, is_bg_light) for _, d in ipairs(hldef) do if is_hl_set(d) then dprintf('Deriving %s from %s', hl, d) if hldef.fg_factor then local dh = get_hl(d) api.nvim_set_hl(0, hl, { default = true, fg = cmix(dh.fg, hldef.fg_factor * (is_bg_light and 1 or -1)), bg = dh.bg, }) else api.nvim_set_hl(0, hl, { default = true, link = d }) end return end end if hldef[1] and not hldef.fg_factor then -- No fallback found which is set. Just link to the first fallback -- if there are no modifiers dprintf('Deriving %s from %s', hl, hldef[1]) api.nvim_set_hl(0, hl, { default = true, link = hldef[1] }) else dprintf('Could not derive %s', hl) end end --- Setup a GitSign* highlight by deriving it from other potentially present --- highlights. function M.setup_highlights() local is_bg_light = vim.o.background == 'light' for _, hlg in ipairs(M.hls) do for hl, hldef in pairs(hlg) do if is_hl_set(hl) then -- Already defined dprintf('Highlight %s is already defined', hl) else derive(hl, hldef, is_bg_light) end end end end function M.setup() M.setup_highlights() api.nvim_create_autocmd('ColorScheme', { group = 'gitsigns', callback = M.setup_highlights, }) end do --- temperature highlight local temp_colors = {} --- @type table local normal_bg --- @type [integer,integer,integer]? --- @param min integer --- @param max integer --- @param t integer --- @param alpha number 0-1 --- @param fg? boolean --- @return string function M.get_temp_hl(min, max, t, alpha, fg) local Color = require('gitsigns.color') local denom = math.max(max, t) - min local normalized_t = denom ~= 0 and (t - min) / denom or 0 local raw_temp_color = Color.temp(normalized_t) if normal_bg == nil then local normal_hl = api.nvim_get_hl(0, { name = 'Normal' }) if normal_hl.bg then normal_bg = Color.int_to_rgb(normal_hl.bg) elseif vim.o.background == 'light' then normal_bg = { 255, 255, 255 } -- white else normal_bg = { 0, 0, 0 } -- black end end local color = Color.rgb_to_int(Color.blend(raw_temp_color, normal_bg, alpha)) if temp_colors[color] then return temp_colors[color] end local fgs = fg and 'fg' or 'bg' local hl_name = ('GitSignsColorTemp.%s.%d'):format(fgs, color) api.nvim_set_hl(0, hl_name, { [fgs] = color }) temp_colors[color] = hl_name return hl_name end end return M neovim-gitsigns-2.0.0/lua/gitsigns/hunks.lua000066400000000000000000000405251513053142700211050ustar00rootroot00000000000000local log = require('gitsigns.debug.log') local util = require('gitsigns.util') local config = require('gitsigns.config').config local min, max = math.min, math.max --- @alias Gitsigns.Hunk.Type --- | "add" --- | "change" --- | "delete" --- @class (exact) Gitsigns.Hunk.Node --- @field start integer --- @field count integer --- @field lines string[] --- @field no_nl_at_eof? true --- @class (exact) Gitsigns.Hunk.Hunk --- @field type Gitsigns.Hunk.Type --- @field head string --- @field added Gitsigns.Hunk.Node --- @field removed Gitsigns.Hunk.Node --- @field vend integer --- @class (exact) Gitsigns.Hunk.Hunk_Public --- @field type Gitsigns.Hunk.Type --- @field head string --- @field lines string[] --- @field added Gitsigns.Hunk.Node --- @field removed Gitsigns.Hunk.Node --- @class gitsigns.hunks local M = {} --- @param old_start integer --- @param old_count integer --- @param new_start integer --- @param new_count integer --- @return Gitsigns.Hunk.Hunk function M.create_hunk(old_start, old_count, new_start, new_count) return { removed = { start = old_start, count = old_count, lines = {} }, added = { start = new_start, count = new_count, lines = {} }, head = ('@@ -%d%s +%d%s @@'):format( old_start, old_count > 0 and ',' .. old_count or '', new_start, new_count > 0 and ',' .. new_count or '' ), vend = new_start + max(new_count - 1, 0), type = new_count == 0 and 'delete' or old_count == 0 and 'add' or 'change', } end --- @param hunks Gitsigns.Hunk.Hunk[] --- @param top integer --- @param bot integer --- @return Gitsigns.Hunk.Hunk? function M.create_partial_hunk(hunks, top, bot) local pretop, precount = top, bot - top + 1 local unused = 0 for _, h in ipairs(hunks) do local added_in_hunk = h.added.count - h.removed.count local added_in_range = 0 if h.added.start >= top and h.vend <= bot then -- Range contains hunk added_in_range = added_in_hunk else local added_above_bot = max(0, bot + 1 - (h.added.start + h.removed.count)) local added_above_top = max(0, top - (h.added.start + h.removed.count)) if h.added.start >= top and h.added.start <= bot then -- Range top intersects hunk added_in_range = added_above_bot elseif h.vend >= top and h.vend <= bot then -- Range bottom intersects hunk added_in_range = added_in_hunk - added_above_top pretop = pretop - added_above_top elseif h.added.start <= top and h.vend >= bot then -- Range within hunk added_in_range = added_above_bot - added_above_top pretop = pretop - added_above_top else -- No intersection unused = unused + 1 end if top > h.vend then pretop = pretop - added_in_hunk end end precount = precount - added_in_range end if unused == #hunks then -- top and bot are not in any hunk return end if precount == 0 then pretop = pretop - 1 end return M.create_hunk(pretop, precount, top, bot - top + 1) end --- @param hunk Gitsigns.Hunk.Hunk --- @param fileformat string --- @return string[] function M.patch_lines(hunk, fileformat) local lines = {} --- @type string[] for _, l in ipairs(hunk.removed.lines) do lines[#lines + 1] = '-' .. l end for _, l in ipairs(hunk.added.lines) do lines[#lines + 1] = '+' .. l end if fileformat == 'dos' then lines = util.strip_cr(lines) end return lines end local function tointeger(x) return tonumber(x) --[[@as integer]] end --- @param line string --- @return Gitsigns.Hunk.Hunk function M.parse_diff_line(line) local diffkey = vim.trim(assert(vim.split(line, '@@', { plain = true })[2])) -- diffKey: "-xx,n +yy" -- pre: {xx, n}, now: {yy} local p = vim.tbl_map( --- @param s string --- @return [string, string] function(s) return vim.split(s:sub(2), ',') end, vim.split(diffkey, ' ') ) --- @type [string,string?], [string,string?] local pre, now = p[1], p[2] local hunk = M.create_hunk( tointeger(pre[1]), (tointeger(pre[2]) or 1), tointeger(now[1]), (tointeger(now[2]) or 1) ) hunk.head = line return hunk end --- @param hunk Gitsigns.Hunk.Hunk --- @return integer local function change_end(hunk) if hunk.added.count == 0 then -- delete return hunk.added.start elseif hunk.removed.count == 0 then -- add return hunk.added.start + hunk.added.count - 1 else -- change return hunk.added.start + min(hunk.added.count, hunk.removed.count) - 1 end end --- Calculate signs needed to be applied from a hunk for a specified line range. --- @param hunk Gitsigns.Hunk.Hunk --- @param next_hunk Gitsigns.Hunk.Hunk? --- @param min_lnum integer --- @param max_lnum integer --- @param untracked? boolean --- @return Gitsigns.Sign[] local function calc_signs(hunk, next_hunk, min_lnum, max_lnum, untracked) local start, added, removed = hunk.added.start, hunk.added.count, hunk.removed.count if hunk.type == 'delete' and start == 0 then if min_lnum <= 1 then -- topdelete signs get placed one row lower return { { type = 'topdelete', count = removed, lnum = 1 } } else return {} end end --- @type Gitsigns.Sign[] local signs = {} local cend = change_end(hunk) -- if this is a change hunk, mark changedelete if lines were removed or if the -- next hunk removes on this hunks last line local changedelete = hunk.type == 'change' and ( removed > added or ((next_hunk and next_hunk.type == 'delete') and start + added - 1 == next_hunk.added.start) ) for lnum = max(start, min_lnum), min(cend, max_lnum) do signs[#signs + 1] = { type = (changedelete and lnum == cend) and 'changedelete' or untracked and 'untracked' or hunk.type, count = lnum == start and (hunk.type == 'add' and added or removed) or nil, lnum = lnum, } end if hunk.type == 'change' and added > removed and hunk.vend >= min_lnum and cend <= max_lnum then for lnum = max(cend, min_lnum), min(hunk.vend, max_lnum) do signs[#signs + 1] = { type = 'add', count = lnum == hunk.vend and (added - removed) or nil, lnum = lnum, } end end return signs end --- Calculate signs needed to be applied from a hunk for a specified line range. --- @param prev_hunk Gitsigns.Hunk.Hunk? --- @param hunk Gitsigns.Hunk.Hunk --- @param next_hunk Gitsigns.Hunk.Hunk? --- @param min_lnum? integer --- @param max_lnum? integer --- @param untracked? boolean --- @return Gitsigns.Sign[] function M.calc_signs(prev_hunk, hunk, next_hunk, min_lnum, max_lnum, untracked) if not (not untracked or hunk.type == 'add') then log.eprintf('Invalid hunk with untracked=%s hunk="%s"', untracked, hunk.head) return {} end min_lnum = max(1, min_lnum or 1) max_lnum = max_lnum or math.huge --[[@as integer]] if not config._new_sign_calc then return calc_signs(hunk, next_hunk, min_lnum, max_lnum, untracked) end local start, added, removed = hunk.added.start, hunk.added.count, hunk.removed.count local cend = change_end(hunk) local topdelete = hunk.type == 'delete' and (start == 0 or prev_hunk and change_end(prev_hunk) == start) and (not next_hunk or next_hunk.added.start ~= start + 1) if topdelete and min_lnum == 1 then min_lnum = 0 end --- @type Gitsigns.Sign[] local signs = {} for lnum = max(start, min_lnum), min(cend, max_lnum) do local changedelete = hunk.type == 'change' and (removed > added and lnum == cend or prev_hunk and prev_hunk.added.start == 0) signs[#signs + 1] = { type = topdelete and 'topdelete' or changedelete and 'changedelete' or untracked and 'untracked' or hunk.type, count = lnum == start and (hunk.type == 'add' and added or removed) or nil, lnum = lnum + (topdelete and 1 or 0), } end if hunk.type == 'change' and added > removed and hunk.vend >= min_lnum and cend <= max_lnum then for lnum = max(cend, min_lnum), min(hunk.vend, max_lnum) do signs[#signs + 1] = { type = 'add', count = lnum == hunk.vend and (added - removed) or nil, lnum = lnum, } end end return signs end --- @param relpath string --- @param hunks Gitsigns.Hunk.Hunk[] --- @param mode_bits string --- @param invert? boolean --- @return string[] function M.create_patch(relpath, hunks, mode_bits, invert) invert = invert or false local results = { string.format('diff --git a/%s b/%s', relpath, relpath), 'index 000000..000000 ' .. mode_bits, '--- a/' .. relpath, '+++ b/' .. relpath, } local offset = 0 for _, process_hunk in ipairs(hunks) do local start, pre_count, now_count = process_hunk.removed.start, process_hunk.removed.count, process_hunk.added.count if process_hunk.type == 'add' then start = start + 1 end local pre_lines = process_hunk.removed.lines local now_lines = process_hunk.added.lines if invert then pre_count, now_count = now_count, pre_count --- @type integer, integer pre_lines, now_lines = now_lines, pre_lines --- @type string[], string[] end table.insert( results, ('@@ -%s,%s +%s,%s @@'):format(start, pre_count, start + offset, now_count) ) for _, l in ipairs(pre_lines) do results[#results + 1] = '-' .. l end if process_hunk.removed.no_nl_at_eof then results[#results + 1] = '\\ No newline at end of file' end for _, l in ipairs(now_lines) do results[#results + 1] = '+' .. l end if process_hunk.added.no_nl_at_eof then results[#results + 1] = '\\ No newline at end of file' end process_hunk.removed.start = start + offset offset = offset + (now_count - pre_count) end return results end --- @param hunks Gitsigns.Hunk.Hunk[] --- @return Gitsigns.StatusObj function M.get_summary(hunks) local status = { added = 0, changed = 0, removed = 0 } for _, hunk in ipairs(hunks or {}) do if hunk.type == 'add' then status.added = status.added + hunk.added.count elseif hunk.type == 'delete' then status.removed = status.removed + hunk.removed.count elseif hunk.type == 'change' then local add, remove = hunk.added.count, hunk.removed.count local delta = min(add, remove) status.changed = status.changed + delta status.added = status.added + add - delta status.removed = status.removed + remove - delta end end return status end --- @param lnum integer --- @param hunks Gitsigns.Hunk.Hunk[]? --- @return Gitsigns.Hunk.Hunk?, integer? function M.find_hunk(lnum, hunks) for i, hunk in ipairs(hunks or {}) do if lnum == 1 and hunk.added.start == 0 and hunk.vend == 0 then return hunk, i end if hunk.added.start <= lnum and hunk.vend >= lnum then return hunk, i end end end --- @param lnum integer --- @param hunks Gitsigns.Hunk.Hunk[] --- @param direction 'first'|'last'|'next'|'prev' --- @param wrap? boolean --- @return integer? function M.find_nearest_hunk(lnum, hunks, direction, wrap) if #hunks == 0 then return elseif direction == 'first' then return 1 elseif direction == 'last' then return #hunks elseif direction == 'next' then if assert(hunks[1]).added.start > lnum then return 1 end for i = #hunks, 1, -1 do if hunks[i].added.start <= lnum then if i + 1 <= #hunks and assert(hunks[i + 1]).added.start > lnum then return i + 1 elseif wrap then return 1 end end end elseif direction == 'prev' then if max(assert(hunks[#hunks]).vend) < lnum then return #hunks end for i = 1, #hunks do if lnum <= max(hunks[i].vend, 1) then if i > 1 and max(assert(hunks[i - 1]).vend, 1) < lnum then return i - 1 elseif wrap then return #hunks end end end end end --- @param a Gitsigns.Hunk.Hunk[]? --- @param b Gitsigns.Hunk.Hunk[]? --- @return boolean function M.compare_heads(a, b) if (a == nil) ~= (b == nil) then return true elseif a and #a ~= #b then return true end for i, ah in ipairs(a or {}) do --- @diagnostic disable-next-line:need-check-nil if b[i].head ~= ah.head then return true end end return false end --- @param a Gitsigns.Hunk.Hunk --- @param b Gitsigns.Hunk.Hunk --- @return boolean local function compare_new(a, b) if a.added.start ~= b.added.start then return false end if a.added.count ~= b.added.count then return false end for i = 1, a.added.count do if a.added.lines[i] ~= b.added.lines[i] then return false end end return true end --- Return hunks in a using b's hunks as a filter. Only compare the 'new' section --- of the hunk. --- --- Eg. Given: --- --- a = { --- 1 = '@@ -24 +25,1 @@', --- 2 = '@@ -32 +34,1 @@', --- 3 = '@@ -37 +40,1 @@' --- } --- --- b = { --- 1 = '@@ -26 +25,1 @@' --- } --- --- Since a[1] and b[1] introduce the same changes to the buffer (both have --- +25,1), we exclude this hunk in the output so we return: --- --- { --- 1 = '@@ -32 +34,1 @@', --- 2 = '@@ -37 +40,1 @@' --- } --- --- @param a Gitsigns.Hunk.Hunk[] --- @param b Gitsigns.Hunk.Hunk[] --- @return Gitsigns.Hunk.Hunk[]? function M.filter_common(a, b) if not a and not b then return end a, b = a or {}, b or {} local a_i = 1 local b_i = 1 --- @type Gitsigns.Hunk.Hunk[] local ret = {} -- Need an offset of 1 in order to process when we hit the end of either -- a or b for _ = 1, max(#a, #b) + 1 do local a_h, b_h = a[a_i], b[b_i] if not a_h then -- Reached the end of a break end if not b_h then -- Reached the end of b, add remainder of a for i = a_i, #a do ret[#ret + 1] = a[i] end break end if a_h.added.start > b_h.added.start then -- a pointer is ahead of b; increment b pointer b_i = b_i + 1 elseif a_h.added.start < b_h.added.start then -- b pointer is ahead of a; add a_h to ret and increment a pointer ret[#ret + 1] = a_h a_i = a_i + 1 else -- a_h.start == b_h.start -- a_h and b_h start on the same line, if hunks have the same changes then -- skip (filtered) otherwise add a_h to ret. Increment both hunk -- pointers -- TODO(lewis6991): Be smarter; if bh intercepts then break down ah. if not compare_new(a_h, b_h) then ret[#ret + 1] = a_h end a_i = a_i + 1 b_i = b_i + 1 end end return ret end --- @param hunk Gitsigns.Hunk.Hunk --- @param fileformat string --- @return Gitsigns.LineSpec[] function M.linespec_for_hunk(hunk, fileformat) local hls = {} --- @type [string, string|Gitsigns.HlMark[], string?][][] local removed, added = hunk.removed.lines, hunk.added.lines if fileformat == 'dos' then removed = util.strip_cr(removed) added = util.strip_cr(added) end for _, spec in ipairs({ { sym = '-', lines = removed, hl = 'GitSignsDeletePreview' }, { sym = '+', lines = added, hl = 'GitSignsAddPreview' }, }) do for _, l in ipairs(spec.lines) do --- @type Gitsigns.HlMark local mark = { start_row = 0, hl_group = spec.hl, end_row = 1, -- Highlight whole line } hls[#hls + 1] = { { spec.sym .. l, { mark } } } end if config.diff_opts.internal then if spec.lines == removed and hunk.removed.no_nl_at_eof or spec.lines == added and hunk.added.no_nl_at_eof then local mark = { start_row = 0, end_row = 1, hl_group = 'GitSignsNoEOLPreview' } hls[#hls + 1] = { { spec.sym .. '\\ No newline at end of file', { mark } } } end end end if config.diff_opts.internal then local removed_regions, added_regions = require('gitsigns.diff_int').run_word_diff(removed, added) for _, region in ipairs(removed_regions) do local i = region[1] local hlm = assert(assert(hls[i])[1])[2] hlm[#hlm + 1] = { start_row = 0, hl_group = 'GitSignsDeleteInline', start_col = region[3], end_col = region[4], } end for _, region in ipairs(added_regions) do local i = hunk.removed.count + region[1] local hlm = assert(assert(hls[i])[1])[2] hlm[#hlm + 1] = { start_row = 0, hl_group = 'GitSignsAddInline', start_col = region[3], end_col = region[4], } end end return hls end return M neovim-gitsigns-2.0.0/lua/gitsigns/manager.lua000066400000000000000000000367261513053142700213770ustar00rootroot00000000000000local async = require('gitsigns.async') local log = require('gitsigns.debug.log') local util = require('gitsigns.util') local run_diff = require('gitsigns.diff') local Hunks = require('gitsigns.hunks') local Signs = require('gitsigns.signs') local Status = require('gitsigns.status') local debounce_trailing = require('gitsigns.debounce').debounce_trailing local throttle_async = require('gitsigns.debounce').throttle_async local cache = require('gitsigns.cache').cache local Config = require('gitsigns.config') local config = Config.config local api = vim.api local signs_normal = Signs.new() local signs_staged = Signs.new(true) --- @class gitsigns.manager local M = {} local statuscolumn_active = false --- @param bufnr? integer --- @param top? integer --- @param bot? integer local function redraw_statuscol(bufnr, top, bot) if statuscolumn_active then api.nvim__redraw({ buf = bufnr, range = { top, bot }, statuscolumn = true, }) end end --- @param bufnr? integer --- @param lnum? integer --- @return string function M.statuscolumn(bufnr, lnum) bufnr = bufnr or 0 lnum = lnum or vim.v.lnum if not config._statuscolumn then config.signcolumn = false config._statuscolumn = true end local res = {} --- @type string[] local res_len = 0 for _, signs in pairs({ signs_normal, signs_staged }) do local marks = api.nvim_buf_get_extmarks(bufnr, signs.ns, { lnum - 1, 0 }, { lnum - 1, -1 }, {}) for _, mark in pairs(marks) do local id = mark[1] local s = signs.signs[id] if s then vim.list_extend(res, { '%#' .. s[2] .. '#', s[1], '%*' }) --- @diagnostic disable-next-line: missing-parameter res_len = res_len + vim.str_utfindex(s[1]) end end end local pad = math.max(0, 2 - res_len) return table.concat(res) .. string.rep(' ', pad) end --- @param bufnr integer --- @param signs Gitsigns.Signs --- @param hunks? Gitsigns.Hunk.Hunk[] --- @param top integer --- @param bot integer --- @param clear? boolean --- @param untracked boolean --- @param filter? fun(line: integer):boolean local function apply_win_signs0(bufnr, signs, hunks, top, bot, clear, untracked, filter) if clear then signs:remove(bufnr) -- Remove all signs end hunks = hunks or {} for i, hunk in ipairs(hunks) do --- @type Gitsigns.Hunk.Hunk?, Gitsigns.Hunk.Hunk? local prev_hunk, next_hunk = hunks[i - 1], hunks[i + 1] -- To stop the sign column width changing too much, if there are signs to be -- added but none of them are visible in the window, then make sure to add at -- least one sign. Only do this on the first call after an update when we all -- the signs have been cleared. if clear and i == 1 then signs:add( bufnr, Hunks.calc_signs(prev_hunk, hunk, next_hunk, hunk.added.start, hunk.added.start, untracked), filter ) end signs:add(bufnr, Hunks.calc_signs(prev_hunk, hunk, next_hunk, top, bot, untracked), filter) if hunk.added.start > bot then break end end end --- @param bufnr integer --- @param top integer --- @param bot integer --- @param clear? boolean local function apply_win_signs(bufnr, top, bot, clear) local bcache = assert(cache[bufnr]) local untracked = bcache.git_obj.object_name == nil apply_win_signs0(bufnr, signs_normal, bcache.hunks, top, bot, clear, untracked) if signs_staged then apply_win_signs0( bufnr, signs_staged, bcache.hunks_staged, top, bot, clear, false, function(lnum) return not signs_normal:contains(bufnr, lnum) end ) end if clear then redraw_statuscol(bufnr, top, bot) end end --- @param blame table? --- @param first integer --- @param last_orig integer --- @param last_new integer local function on_lines_blame(blame, first, last_orig, last_new) if not blame then return end if last_new < last_orig then util.list_remove(blame, last_new + 1, last_orig) elseif last_new > last_orig then util.list_insert(blame, last_orig + 1, last_new) end for i = first + 1, last_new do blame[i] = nil end end --- @param buf integer --- @param first integer --- @param last_orig integer --- @param last_new integer --- @return true? function M.on_lines(buf, first, last_orig, last_new) local bcache = cache[buf] if not bcache then log.dprint('Cache for buffer was nil. Detaching') return true end if bcache.blame then on_lines_blame(bcache.blame.entries, first, last_orig, last_new) end signs_normal:on_lines(buf, first, last_orig, last_new) if signs_staged then signs_staged:on_lines(buf, first, last_orig, last_new) end -- Signs in changed regions get invalidated so we need to force a redraw if -- any signs get removed. if bcache.hunks and signs_normal:contains(buf, first, last_new) then -- Force a sign redraw on the next update (fixes #521) bcache.force_next_update = true end if signs_staged then if bcache.hunks_staged and signs_staged:contains(buf, first, last_new) then -- Force a sign redraw on the next update (fixes #521) bcache.force_next_update = true end end M.update_sync_debounced(buf) end local ns = api.nvim_create_namespace('gitsigns') --- @param bufnr integer --- @param row integer local function apply_word_diff(bufnr, row) -- Don't run on folded lines if vim.fn.foldclosed(row + 1) ~= -1 then return end local bcache = cache[bufnr] if not bcache or not bcache.hunks then return end local line = api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] if not line then -- Invalid line return end local lnum = row + 1 local hunk = Hunks.find_hunk(lnum, bcache.hunks) if not hunk then -- No hunk at line return end if hunk.added.count ~= hunk.removed.count then -- Only word diff if added count == removed return end local pos = lnum - hunk.added.start + 1 local added_line = assert(hunk.added.lines[pos]) local removed_line = assert(hunk.removed.lines[pos]) local _, added_regions = require('gitsigns.diff_int').run_word_diff( { removed_line }, { added_line } ) local cols = #line for _, region in ipairs(added_regions) do local rtype, scol, ecol = region[2], region[3] - 1, region[4] - 1 if ecol == scol then -- Make sure region is at least 1 column wide so deletes can be shown ecol = scol + 1 end local hl_group = rtype == 'add' and 'GitSignsAddLnInline' or rtype == 'change' and 'GitSignsChangeLnInline' or 'GitSignsDeleteLnInline' local opts = { ephemeral = true, priority = 1000, } if ecol > cols and ecol == scol + 1 then -- delete on last column, use virtual text instead opts.virt_text = { { ' ', hl_group } } opts.virt_text_pos = 'overlay' else opts.end_col = ecol opts.hl_group = hl_group end api.nvim_buf_set_extmark(bufnr, ns, row, scol, opts) util.redraw({ buf = bufnr, range = { row, row + 1 } }) end end local ns_rm = api.nvim_create_namespace('gitsigns_removed') local VIRT_LINE_LEN = 300 --- @param bufnr integer local function clear_deleted(bufnr) local marks = api.nvim_buf_get_extmarks(bufnr, ns_rm, 0, -1, {}) for _, mark in ipairs(marks) do api.nvim_buf_del_extmark(bufnr, ns_rm, mark[1]) end end --- @param bufnr integer --- @param nsd integer --- @param hunk Gitsigns.Hunk.Hunk local function show_deleted(bufnr, nsd, hunk) local virt_lines = {} --- @type [string, string][][] for i, line in ipairs(hunk.removed.lines) do local vline = {} --- @type [string, string][] local last_ecol = 1 if config.word_diff then local regions = require('gitsigns.diff_int').run_word_diff( { hunk.removed.lines[i] }, { hunk.added.lines[i] } ) for _, region in ipairs(regions) do local rline, scol, ecol = region[1], region[3], region[4] if rline > 1 then break end vline[#vline + 1] = { line:sub(last_ecol, scol - 1), 'GitSignsDeleteVirtLn' } vline[#vline + 1] = { line:sub(scol, ecol - 1), 'GitSignsDeleteVirtLnInline' } last_ecol = ecol end end if #line > 0 then vline[#vline + 1] = { line:sub(last_ecol, -1), 'GitSignsDeleteVirtLn' } end -- Add extra padding so the entire line is highlighted local padding = string.rep(' ', VIRT_LINE_LEN - #line) vline[#vline + 1] = { padding, 'GitSignsDeleteVirtLn' } virt_lines[i] = vline end local topdelete = hunk.added.start == 0 and hunk.type == 'delete' local row = topdelete and 0 or hunk.added.start - 1 api.nvim_buf_set_extmark(bufnr, nsd, row, -1, { virt_lines = virt_lines, -- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166 virt_lines_above = hunk.type ~= 'delete' or topdelete, }) end --- @param bufnr integer --- @param hunks? Gitsigns.Hunk.Hunk[] local function update_show_deleted(bufnr, hunks) clear_deleted(bufnr) if config.show_deleted then for _, hunk in ipairs(hunks or {}) do show_deleted(bufnr, ns_rm, hunk) end end end --- @param bufnr integer --- @return boolean local function buf_in_view(bufnr) for _, win in ipairs(api.nvim_tabpage_list_wins(0)) do if api.nvim_win_get_buf(win) == bufnr then return true end end return false end --- @async --- @param bcache Gitsigns.CacheEntry --- @param fn async fun() local function update_lock(bcache, fn) if not config._update_lock then fn() else bcache.git_obj:lock(fn) end end --- @async --- Ensure updates cannot be interleaved. --- Since updates are asynchronous we need to make sure an update isn't performed --- whilst another one is in progress. If this happens then schedule another --- update after the current one has completed. --- @param bufnr integer M.update = throttle_async({ hash = 1, schedule = true }, function(bufnr) local bcache = cache[bufnr] if not bcache or not bcache:schedule() then return end if not buf_in_view(bufnr) then log.dprint('Buffer not in view, deferring update') bcache.update_on_view = true return end bcache.update_on_view = nil update_lock(bcache, function() local old_hunks, old_hunks_staged = bcache.hunks, bcache.hunks_staged bcache.hunks, bcache.hunks_staged = nil, nil local git_obj = bcache.git_obj local file_mode = bcache.file_mode if not bcache.compare_text or config._refresh_staged_on_update or file_mode then if file_mode then bcache.compare_text = util.file_lines(git_obj.file) else bcache.compare_text = git_obj:get_show_text() end if not bcache:schedule(true) then return end end local buftext = util.buf_lines(bufnr) bcache.hunks = run_diff(bcache.compare_text, buftext) if not bcache:schedule() then return end local bufname = api.nvim_buf_get_name(bufnr) local rev_is_index = not git_obj:from_tree() if config.signs_staged_enable and not file_mode and (rev_is_index or bufname:match('^fugitive://') or bufname:match('^gitsigns://')) then if not bcache.compare_text_head or config._refresh_staged_on_update then -- When the revision is from the index, we compare against HEAD to -- show the staged changes. -- -- When showing a revision buffer (a buffer that represents the revision -- of a specific file and does not have a corresponding file on disk), we -- utilize the staged signs to represent the changes introduced in that -- revision. Therefore we compare against the previous commit. Note there -- should not be any normal signs for these buffers. local staged_rev = rev_is_index and 'HEAD' or git_obj.revision .. '^' bcache.compare_text_head = git_obj:get_show_text(staged_rev) if not bcache:schedule(true) then return end end local hunks_head = run_diff(bcache.compare_text_head, buftext) if not bcache:schedule() then return end bcache.hunks_staged = Hunks.filter_common(hunks_head, bcache.hunks) end -- Note the decoration provider may have invalidated bcache.hunks at this -- point if bcache.force_next_update or Hunks.compare_heads(bcache.hunks, old_hunks) or Hunks.compare_heads(bcache.hunks_staged, old_hunks_staged) then -- Apply signs to the window. Other signs will be added by the decoration -- provider as they are drawn. apply_win_signs(bufnr, vim.fn.line('w0'), vim.fn.line('w$'), true) update_show_deleted(bufnr, bcache.hunks) bcache.force_next_update = false local summary = Hunks.get_summary(bcache.hunks) summary.head = git_obj.repo.abbrev_head Status.update(bufnr, summary) end end) end) M.update_sync_debounced = debounce_trailing({ timeout = function() return config.update_debounce end, hash = 1, }, function(bufnr) async.run(M.update, bufnr):raise_on_error() end) --- @param bufnr integer --- @param keep_signs? boolean function M.detach(bufnr, keep_signs) if not keep_signs then -- Remove all signs signs_normal:remove(bufnr) if signs_staged then signs_staged:remove(bufnr) end redraw_statuscol(bufnr) end end function M.reset_signs() -- Remove all signs signs_normal:reset() signs_staged:reset() end --- @param bufnr integer --- @param topline integer --- @param botline_guess integer --- @return false? local function on_win(bufnr, topline, botline_guess) local bcache = cache[bufnr] if not bcache or not bcache.hunks then return false end local botline = math.min(botline_guess, api.nvim_buf_line_count(bufnr)) apply_win_signs(bufnr, topline + 1, botline + 1) if not (config.word_diff and config.diff_opts.internal) then return false end end function M.setup() -- Calling this before any await calls will stop nvim's intro messages being -- displayed api.nvim_set_decoration_provider(ns, { on_win = function(_, _winid, bufnr, topline, botline) return on_win(bufnr, topline, botline) end, on_line = function(_, _winid, bufnr, row) apply_word_diff(bufnr, row) end, }) Config.subscribe({ 'signcolumn', 'numhl', 'linehl', 'show_deleted' }, function() -- Remove all signs M.reset_signs() for k, v in pairs(cache) do v:invalidate(true) M.update_sync_debounced(k) end end) api.nvim_create_autocmd('OptionSet', { group = 'gitsigns', pattern = { 'fileformat', 'bomb', 'eol' }, callback = function(args) local buf = args.buf local bcache = cache[buf] if not bcache then return end bcache:invalidate(true) M.update_sync_debounced(buf) end, }) do -- deferred updates from file watcher api.nvim_create_autocmd('TabEnter', { group = 'gitsigns', desc = 'Gitsigns: deferred updates', callback = function() for _, win in ipairs(api.nvim_tabpage_list_wins(0)) do local bufnr = api.nvim_win_get_buf(win) if cache[bufnr] and cache[bufnr].update_on_view then log.dprint('TabEnter update') async.run(M.update, bufnr):raise_on_error() end end end, }) api.nvim_create_autocmd('BufEnter', { group = 'gitsigns', desc = 'Gitsigns: deferred updates', callback = function(args) local bufnr = args.buf if cache[bufnr] and cache[bufnr].update_on_view then log.dprint('BufEnter update') async.run(M.update, bufnr):raise_on_error() end end, }) end end return M neovim-gitsigns-2.0.0/lua/gitsigns/message.lua000066400000000000000000000010411513053142700213670ustar00rootroot00000000000000local levels = vim.log.levels local M = {} --- @type fun(fmt: string, ...: string) M.warn = vim.schedule_wrap(function(fmt, ...) vim.notify(fmt:format(...), levels.WARN, { title = 'gitsigns' }) end) --- @type fun(fmt: string, ...: string) M.error = vim.schedule_wrap(function(fmt, ...) vim.notify(fmt:format(...), levels.ERROR, { title = 'gitsigns' }) end) --- @type fun(fmt: string, ...: string) M.error_once = vim.schedule_wrap(function(fmt, ...) vim.notify_once(fmt:format(...), levels.ERROR, { title = 'gitsigns' }) end) return M neovim-gitsigns-2.0.0/lua/gitsigns/popup.lua000066400000000000000000000206121513053142700211130ustar00rootroot00000000000000local api = vim.api local M = {} --- @param bufnr integer --- @param lines string[] --- @return integer local function bufnr_calc_width(bufnr, lines) return api.nvim_buf_call(bufnr, function() local width = 0 for _, l in ipairs(lines) do if vim.fn.type(l) == vim.v.t_string then local len = vim.fn.strdisplaywidth(l) if len > width then width = len end end end return width + 1 -- Add 1 for some miinor padding end) end -- Expand height until all lines are visible to account for wrapped lines. --- @param winid integer --- @param nlines integer --- @param border? any local function expand_height(winid, nlines, border) border = border or (vim.fn.exists('&winborder') == 1 and vim.o.winborder or '') local newheight = 0 local maxheight = vim.o.lines - vim.o.cmdheight - (border ~= '' and 2 or 0) for _ = 0, 50 do local winheight = api.nvim_win_get_height(winid) if newheight > winheight then -- Window must be max height break end --- @type integer local wd = api.nvim_win_call(winid, function() return vim.fn.line('w$') end) if wd >= nlines then break end newheight = winheight + nlines - wd if newheight > maxheight then api.nvim_win_set_height(winid, maxheight) break end api.nvim_win_set_height(winid, newheight) end end --- @class (exact) Gitsigns.HlMark --- @field hl_group string --- @field start_row integer --- @field start_col? integer --- @field end_row? integer --- @field end_col? integer --- @field url? string --- Each element represents a multi-line segment --- @alias Gitsigns.LineSpec [string, string|Gitsigns.HlMark[], string?][] --- @param hlmarks Gitsigns.HlMark[] --- @param row_offset integer local function offset_hlmarks(hlmarks, row_offset) for _, h in ipairs(hlmarks) do h.start_row = h.start_row + row_offset if h.end_row then h.end_row = h.end_row + row_offset end end end --- Partition the text and Gitsigns.HlMarks from a Gitsigns.LineSpec[] --- @param fmt Gitsigns.LineSpec[] --- @return string[] --- @return Gitsigns.HlMark[] local function partition_linesspec(fmt) local lines = {} --- @type string[] local ret = {} --- @type Gitsigns.HlMark[] local row = 0 for _, section in ipairs(fmt) do local section_text = {} --- @type string[] local col = 0 for _, part in ipairs(section) do local text, hls, url = part[1], part[2], part[3] section_text[#section_text + 1] = text local _, no_lines = text:gsub('\n', '') local end_row = row + no_lines --- @type integer local end_col = no_lines > 0 and 0 or col + #text --- @type integer if type(hls) == 'string' then ret[#ret + 1] = { url = url, hl_group = hls, start_row = row, end_row = end_row, start_col = col, end_col = end_col, } else -- hl is Gitsigns.HlMark[] offset_hlmarks(hls, row) vim.list_extend(ret, hls) end row = end_row col = end_col end local section_lines = vim.split(table.concat(section_text), '\n', { plain = true }) vim.list_extend(lines, section_lines) row = row + 1 end return lines, ret end --- @param id string|true local function close_all_but(id) for _, winid in ipairs(api.nvim_list_wins()) do if vim.w[winid].gitsigns_preview ~= nil and vim.w[winid].gitsigns_preview ~= id then pcall(api.nvim_win_close, winid, true) end end end --- @param id string function M.close(id) for _, winid in ipairs(api.nvim_list_wins()) do if vim.w[winid].gitsigns_preview == id then pcall(api.nvim_win_close, winid, true) end end end local ns = api.nvim_create_namespace('gitsigns_popup') --- @param bufnr integer --- @param lines_spec Gitsigns.LineSpec[] local function update_buf(bufnr, lines_spec) local lines, highlights = partition_linesspec(lines_spec) -- In case nvim was opened with '-M' vim.bo[bufnr].modifiable = true api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) vim.bo[bufnr].modifiable = false for _, hl in ipairs(highlights) do local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, ns, hl.start_row, hl.start_col or 0, { hl_group = hl.hl_group, url = hl.url, end_row = hl.end_row, end_col = hl.end_col, hl_eol = true, }) if not ok then error(vim.inspect(hl) .. '\n' .. err) end end end --- @param lines_spec Gitsigns.LineSpec[] --- @return integer bufnr local function create_buf(lines_spec) local ts = vim.bo.tabstop local bufnr = api.nvim_create_buf(false, true) assert(bufnr ~= 0, 'Failed to create buffer') vim.bo[bufnr].bufhidden = 'wipe' -- Set tabstop before calculating the buffer width so that the correct width -- is calculated vim.bo[bufnr].tabstop = ts update_buf(bufnr, lines_spec) return bufnr end --- @param winid integer --- @param bufnr integer --- @param opts vim.api.keyset.win_config local function update_win(winid, bufnr, opts) local lines = api.nvim_buf_get_lines(bufnr, 0, -1, true) local width = opts.width or bufnr_calc_width(bufnr, lines) local height = opts.height or #lines api.nvim_win_set_width(winid, width) api.nvim_win_set_height(winid, height) if not opts.height then expand_height(winid, #lines, opts.border) end end --- @param bufnr integer --- @param opts vim.api.keyset.win_config --- @param id? string|true --- @return integer winid local function create_win(bufnr, opts, id) id = id or true -- Close any popups not matching id close_all_but(id) local lines = api.nvim_buf_get_lines(bufnr, 0, -1, true) local opts1 = vim.deepcopy(opts or {}) opts1.height = opts1.height or #lines -- Guess, adjust later opts1.width = opts1.width or bufnr_calc_width(bufnr, lines) local winid = api.nvim_open_win(bufnr, false, opts1) vim.w[winid].gitsigns_preview = id if not opts.height then expand_height(winid, #lines, opts.border) end if opts1.style == 'minimal' then -- If 'signcolumn' = auto:1-2, then a empty signcolumn will appear and cause -- line wrapping. vim.wo[winid].signcolumn = 'no' end -- Close the popup when navigating to any window which is not the preview -- itself. local group = 'gitsigns_popup' local group_id = api.nvim_create_augroup(group, {}) local old_cursor = api.nvim_win_get_cursor(0) vim.keymap.set('n', 'q', 'quit!', { silent = true, buffer = bufnr }) api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, { group = group_id, callback = function() local cursor = api.nvim_win_get_cursor(0) -- Did the cursor REALLY change (neovim/neovim#12923) if (old_cursor[1] ~= cursor[1] or old_cursor[2] ~= cursor[2]) and api.nvim_get_current_win() ~= winid then -- Clear the augroup api.nvim_create_augroup(group, {}) pcall(api.nvim_win_close, winid, true) return end old_cursor = cursor end, }) api.nvim_create_autocmd('WinClosed', { pattern = tostring(winid), group = group_id, callback = function() -- Clear the augroup api.nvim_create_augroup(group, {}) end, }) -- update window position to follow the cursor when scrolling api.nvim_create_autocmd('WinScrolled', { buffer = api.nvim_get_current_buf(), group = group_id, callback = function() if api.nvim_win_is_valid(winid) and api.nvim_get_current_win() ~= winid then api.nvim_win_set_config(winid, opts1) end end, }) return winid end --- @param lines_spec Gitsigns.LineSpec[] --- @param opts vim.api.keyset.win_config --- @param id? string --- @return integer winid, integer bufnr function M.create(lines_spec, opts, id) local bufnr = create_buf(lines_spec) local winid = create_win(bufnr, opts, id) return winid, bufnr end --- @param winid integer --- @param bufnr integer --- @param lines_spec Gitsigns.LineSpec[] --- @param opts vim.api.keyset.win_config function M.update(winid, bufnr, lines_spec, opts) update_buf(bufnr, lines_spec) update_win(winid, bufnr, opts) end --- @param id string --- @return integer? winid function M.is_open(id) for _, winid in ipairs(api.nvim_list_wins()) do if vim.w[winid].gitsigns_preview == id then return winid end end end --- @param id string --- @return integer? winid function M.focus_open(id) local winid = M.is_open(id) if winid then api.nvim_set_current_win(winid) end return winid end return M neovim-gitsigns-2.0.0/lua/gitsigns/repeat.lua000066400000000000000000000011311513053142700212230ustar00rootroot00000000000000local M = {} function M.mk_repeatable(fn) return function(...) local args = { ... } local nargs = select('#', ...) vim.go.operatorfunc = "v:lua.require'gitsigns.repeat'.repeat_action" M.repeat_action = function() fn(unpack(args, 1, nargs)) if vim.fn.exists('*repeat#set') == 1 then local action = vim.api.nvim_replace_termcodes( string.format('call %s()', vim.go.operatorfunc), true, true, true ) vim.fn['repeat#set'](action, -1) end end vim.cmd('normal! g@l') end end return M neovim-gitsigns-2.0.0/lua/gitsigns/signs.lua000066400000000000000000000105341513053142700210750ustar00rootroot00000000000000local api = vim.api local Config = require('gitsigns.config') local config = Config.config --- @param s string --- @return string local function capitalise(s) return s:sub(1, 1):upper() .. s:sub(2) end --- @class Gitsigns.Sign --- @field type Gitsigns.SignType --- @field count? integer --- @field lnum integer --- @class Gitsigns.Signs --- @field name string --- @field group string --- @field config table --- @field staged boolean --- @field ns integer --- @field signs table --- @field private _hl_cache table> local M = {} local km = { culhl = 'Cul', linehl = 'Ln', numhl = 'Nr', hl = '', } --- @param ty Gitsigns.SignType --- @param kind 'hl'|'numhl'|'linehl'|'culhl' --- @return string? function M:hl(ty, kind) if kind ~= 'hl' and not config[kind] then return end self._hl_cache = self._hl_cache or {} self._hl_cache[ty] = self._hl_cache[ty] or {} if self._hl_cache[ty][kind] then return self._hl_cache[ty][kind] end local result = ('GitSigns%s%s%s'):format(self.staged and 'Staged' or '', capitalise(ty), km[kind]) self._hl_cache[ty][kind] = result return result end --- @param buf integer --- @param last_orig integer --- @param last_new integer function M:on_lines(buf, _, last_orig, last_new) -- Remove extmarks on line deletions to mimic -- the behaviour of vim signs. if last_orig > last_new then self:remove(buf, last_new + 1, last_orig) end end --- @param bufnr integer --- @param start_lnum? integer --- @param end_lnum? integer function M:remove(bufnr, start_lnum, end_lnum) if start_lnum then api.nvim_buf_clear_namespace(bufnr, self.ns, start_lnum - 1, end_lnum or start_lnum) for i = start_lnum - 1, (end_lnum or start_lnum) - 1 do self.signs[i] = nil end else self.signs = {} api.nvim_buf_clear_namespace(bufnr, self.ns, 0, -1) end end ---@param bufnr integer ---@param signs Gitsigns.Sign[] --- @param filter? fun(line: integer):boolean function M:add(bufnr, signs, filter) if not config.signcolumn and not config.numhl and not config.linehl and not config._statuscolumn then -- Don't place signs if it won't show anything return end for _, sign in ipairs(signs) do if (not filter or filter(sign.lnum)) and not self:contains(bufnr, sign.lnum) then local lnum, ty = sign.lnum, sign.type local cs = self.config[ty] local text = cs.text if config.signcolumn and cs.show_count and sign.count then local cc = config.count_chars local count_char = cc[sign.count] or cc['+'] or '' text = text .. count_char end local sign_hl_group = self:hl(ty, 'hl') local ok, id_or_err = pcall(api.nvim_buf_set_extmark, bufnr, self.ns, lnum - 1, 0, { id = lnum, sign_text = config.signcolumn and text or '', priority = config.sign_priority, sign_hl_group = self:hl(ty, 'hl'), number_hl_group = self:hl(ty, 'numhl'), line_hl_group = self:hl(ty, 'linehl'), cursorline_hl_group = self:hl(ty, 'culhl'), }) if ok then --- @cast id_or_err integer self.signs[id_or_err] = { text, sign_hl_group } elseif config.debug_mode then vim.schedule(function() error(table.concat({ string.format('Error placing extmark on line %d', lnum), id_or_err, }, '\n')) end) end end end end ---@param bufnr integer ---@param start integer ---@param last? integer ---@return boolean function M:contains(bufnr, start, last) local marks = api.nvim_buf_get_extmarks( bufnr, self.ns, { start - 1, 0 }, { last or start - 1, 0 }, { limit = 1 } ) return #marks > 0 end function M:reset() for _, buf in ipairs(api.nvim_list_bufs()) do self:remove(buf) end end --- @param staged? boolean --- @return Gitsigns.Signs function M.new(staged) local self = setmetatable({}, { __index = M }) self.config = staged and config.signs_staged or config.signs Config.subscribe(staged and 'signs_staged' or 'signs', function() self.config = staged and config.signs_staged or config.signs end) self.staged = staged == true self.group = 'gitsigns_signs_' .. (staged and 'staged' or '') self.ns = api.nvim_create_namespace(self.group) self.signs = {} return self end return M neovim-gitsigns-2.0.0/lua/gitsigns/status.lua000066400000000000000000000022431513053142700212730ustar00rootroot00000000000000local api = vim.api --- @class (exact) Gitsigns.StatusObj --- @field added? integer --- @field removed? integer --- @field changed? integer --- @field head? string --- @field root? string --- @field gitdir? string local M = {} --- @param bufnr integer local function autocmd_update(bufnr) api.nvim_exec_autocmds('User', { pattern = 'GitSignsUpdate', modeline = false, data = { buffer = bufnr }, }) end --- @param bufnr integer --- @param status Gitsigns.StatusObj function M.update(bufnr, status) if not api.nvim_buf_is_loaded(bufnr) then return end local bstatus = vim.b[bufnr].gitsigns_status_dict if bstatus then status = vim.tbl_extend('force', bstatus, status) end vim.b[bufnr].gitsigns_head = status.head or '' vim.b[bufnr].gitsigns_status_dict = status local config = require('gitsigns.config').config vim.b[bufnr].gitsigns_status = config.status_formatter(status) autocmd_update(bufnr) end function M.clear(bufnr) if not api.nvim_buf_is_loaded(bufnr) then return end vim.b[bufnr].gitsigns_head = nil vim.b[bufnr].gitsigns_status_dict = nil vim.b[bufnr].gitsigns_status = nil autocmd_update(bufnr) end return M neovim-gitsigns-2.0.0/lua/gitsigns/system.lua000066400000000000000000000006661513053142700213030ustar00rootroot00000000000000local log = require('gitsigns.debug.log') local M = {} -- compat module contains 0.11 fixes. local system = vim.fn.has('nvim-0.11.2') == 1 and vim.system or require('gitsigns.system.compat') --- @param cmd string[] --- @param opts vim.SystemOpts --- @param on_exit fun(obj: vim.SystemCompleted) --- @return vim.SystemObj function M.system(cmd, opts, on_exit) log.dprint(unpack(cmd)) return system(cmd, opts, on_exit) end return M neovim-gitsigns-2.0.0/lua/gitsigns/system/000077500000000000000000000000001513053142700205705ustar00rootroot00000000000000neovim-gitsigns-2.0.0/lua/gitsigns/system/compat.lua000066400000000000000000000214461513053142700225650ustar00rootroot00000000000000local uv = vim.uv or vim.loop ---@diagnostic disable-line: deprecated --- @type vim.SystemSig local SIG = { HUP = 1, -- Hangup INT = 2, -- Interrupt from keyboard KILL = 9, -- Kill signal TERM = 15, -- Termination signal -- STOP = 17,19,23 -- Stop the process } --- @param handle uv.uv_handle_t? local function close_handle(handle) if handle and not handle:is_closing() then handle:close() end end --- @class Gitsigns.SystemObj : vim.SystemObj --- @field private _state vim.SystemState local SystemObj = {} --- @param state vim.SystemState --- @return vim.SystemObj local function new_systemobj(state) return setmetatable({ pid = state.pid, _state = state, }, { __index = SystemObj }) end --- @param signal integer|string function SystemObj:kill(signal) assert(self._state.handle):kill(signal) end --- @package --- @param signal? vim.SystemSig function SystemObj:_timeout(signal) self._state.done = 'timeout' self:kill(signal or SIG.TERM) end -- Use max 32-bit signed int value to avoid overflow on 32-bit systems. #31633 local MAX_TIMEOUT = 2 ^ 31 - 1 --- @param timeout? integer --- @return vim.SystemCompleted function SystemObj:wait(timeout) local state = self._state local done = vim.wait(timeout or state.timeout or MAX_TIMEOUT, function() return state.result ~= nil end, nil, true) if not done then -- Send sigkill since this cannot be caught self:_timeout(SIG.KILL) vim.wait(timeout or state.timeout or MAX_TIMEOUT, function() return state.result ~= nil end, nil, true) end return assert(state.result) end --- @param data string[]|string|nil function SystemObj:write(data) local stdin = self._state.stdin if not stdin then error('stdin has not been opened on this object') end if type(data) == 'table' then for _, v in ipairs(data) do stdin:write(v) stdin:write('\n') end elseif type(data) == 'string' then stdin:write(data) elseif data == nil then -- Shutdown the write side of the duplex stream and then close the pipe. -- Note shutdown will wait for all the pending write requests to complete -- TODO(lewis6991): apparently shutdown doesn't behave this way. -- (https://github.com/neovim/neovim/pull/17620#discussion_r820775616) stdin:write('', function() stdin:shutdown(function() close_handle(stdin) end) end) end end --- @return boolean function SystemObj:is_closing() local handle = self._state.handle return handle == nil or handle:is_closing() or false end --- @param output? fun(err: string?, data: string?)|false --- @param text? boolean --- @return uv.uv_stream_t? pipe --- @return fun(err: string?, data: string?)? handler --- @return string[]? data local function setup_output(output, text) if output == false then return end local bucket --- @type string[]? local handler --- @type fun(err: string?, data: string?) if type(output) == 'function' then handler = output else bucket = {} handler = function(err, data) if err then error(err) end if text and data then bucket[#bucket + 1] = data:gsub('\r\n', '\n') else bucket[#bucket + 1] = data end end end local pipe = assert(uv.new_pipe(false)) --- @param err? string --- @param data? string local function handler_with_close(err, data) handler(err, data) if data == nil then pipe:read_stop() pipe:close() end end return pipe, handler_with_close, bucket end --- @param input? string|string[]|boolean --- @return uv.uv_stream_t? --- @return string|string[]? local function setup_input(input) if not input then return end local towrite --- @type string|string[]? if type(input) == 'string' or type(input) == 'table' then towrite = input end return assert(uv.new_pipe(false)), towrite end --- @return table local function base_env() local env = vim.fn.environ() --- @type table env['NVIM'] = vim.v.servername env['NVIM_LISTEN_ADDRESS'] = nil return env end --- uv.spawn will completely overwrite the environment --- when we just want to modify the existing one, so --- make sure to prepopulate it with the current env. --- @param env? table --- @param clear_env? boolean --- @return string[]? local function setup_env(env, clear_env) if not env and clear_env then return end env = env or {} if not clear_env then --- @type table env = vim.tbl_extend('force', base_env(), env) end local renv = {} --- @type string[] for k, v in pairs(env) do renv[#renv + 1] = string.format('%s=%s', k, tostring(v)) end return renv end local is_win = vim.fn.has('win32') == 1 --- @param cmd string --- @param opts uv.spawn.options --- @param on_exit fun(code: integer, signal: integer) --- @param on_error fun() --- @return uv.uv_process_t, integer local function spawn(cmd, opts, on_exit, on_error) if is_win then local cmd1 = vim.fn.exepath(cmd) if cmd1 ~= '' then cmd = cmd1 end end local handle, pid_or_err = uv.spawn(cmd, opts, on_exit) if not handle then on_error() if opts.cwd and not uv.fs_stat(opts.cwd) then error(("%s (cwd): '%s'"):format(pid_or_err, opts.cwd)) elseif vim.fn.executable(cmd) == 0 then error(("%s (cmd): '%s'"):format(pid_or_err, cmd)) else error(pid_or_err) end end return handle, pid_or_err --[[@as integer]] end --- @param timeout integer --- @param cb fun() --- @return uv.uv_timer_t local function timer_oneshot(timeout, cb) local timer = assert(uv.new_timer()) timer:start(timeout, 0, function() timer:stop() timer:close() cb() end) return timer end --- @param state vim.SystemState --- @param code integer --- @param signal integer --- @param on_exit fun(result: vim.SystemCompleted)? local function _on_exit(state, code, signal, on_exit) close_handle(state.handle) close_handle(state.stdin) close_handle(state.timer) local check = uv.new_check() check:start(function() for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do if not pipe:is_closing() then return end end check:stop() check:close() if state.done == nil then state.done = true end if (code == 0 or code == 1) and state.done == 'timeout' then -- Unix: code == 0 -- Windows: code == 1 code = 124 end local stdout_data = state.stdout_data local stderr_data = state.stderr_data state.result = { code = code, signal = signal, stdout = stdout_data and table.concat(stdout_data) or nil, stderr = stderr_data and table.concat(stderr_data) or nil, } if on_exit then on_exit(state.result) end end) end --- @param state vim.SystemState local function _on_error(state) close_handle(state.handle) close_handle(state.stdin) close_handle(state.stdout) close_handle(state.stderr) close_handle(state.timer) end --- Run a system command --- --- @param cmd string[] --- @param opts? vim.SystemOpts --- @param on_exit? fun(out: vim.SystemCompleted) --- @return vim.SystemObj local function system(cmd, opts, on_exit) ---@diagnostic disable-next-line vim.validate({ cmd = { cmd, 'table' }, opts = { opts, 'table', true }, on_exit = { on_exit, 'function', true }, }) ---@diagnostic disable-line opts = opts or {} local stdout, stdout_handler, stdout_data = setup_output(opts.stdout, opts.text) local stderr, stderr_handler, stderr_data = setup_output(opts.stderr, opts.text) local stdin, towrite = setup_input(opts.stdin) --- @type vim.SystemState local state = { done = false, cmd = cmd, timeout = opts.timeout, stdin = stdin, stdout = stdout, stdout_data = stdout_data, stderr = stderr, stderr_data = stderr_data, } --- @diagnostic disable-next-line:missing-fields, param-type-not-match state.handle, state.pid = spawn(assert(cmd[1]), { args = vim.list_slice(cmd, 2), stdio = { stdin, stdout, stderr }, cwd = opts.cwd, --- @diagnostic disable-next-line: assign-type-mismatch luvit/luv#777 env = setup_env(opts.env, opts.clear_env), detached = opts.detach, hide = true, }, function(code, signal) _on_exit(state, code, signal, on_exit) end, function() _on_error(state) end) if stdout and stdout_handler then stdout:read_start(stdout_handler) end if stderr and stderr_handler then stderr:read_start(stderr_handler) end local obj = new_systemobj(state) if towrite then obj:write(towrite) obj:write(nil) -- close the stream end if opts.timeout then state.timer = timer_oneshot(opts.timeout, function() if state.handle and state.handle:is_active() then --- @diagnostic disable-next-line: access-invisible obj:_timeout() end end) end return obj end return system neovim-gitsigns-2.0.0/lua/gitsigns/test.lua000066400000000000000000000014341513053142700207300ustar00rootroot00000000000000local M = {} local function eq(act, exp) assert(act == exp, string.format('%s != %s', act, exp)) end M._tests = {} M._tests.expand_format = function() local util = require('gitsigns.util') assert('hello % world % 2021' == util.expand_format(' % % ', { var1 = 'hello', var2 = 'world', var_time = 1616838297, })) end M._tests.test_args = function() local parse_args = require('gitsigns.cli.argparse').parse_args local pos_args, named_args = parse_args('hello there key=value, key1="a b c"') eq(pos_args[1], 'hello') eq(pos_args[2], 'there') eq(named_args.key, 'value,') eq(named_args.key1, 'a b c') pos_args, named_args = parse_args('base=HEAD~1 posarg') eq(named_args.base, 'HEAD~1') eq(pos_args[1], 'posarg') end return M neovim-gitsigns-2.0.0/lua/gitsigns/util.lua000066400000000000000000000265221513053142700207330ustar00rootroot00000000000000local uv = vim.uv or vim.loop ---@diagnostic disable-line: deprecated local is_win = vim.fn.has('win32') == 1 --- @class Gitsigns.Util.Path local Path = {} --- @class Gitsigns.Util local M = {} --- @param path? string --- @return boolean function Path.exists(path) return path ~= nil and uv.fs_stat(path) ~= nil end --- @param path string --- @return boolean function Path.is_dir(path) ---@diagnostic disable-next-line:param-type-mismatch local stat = uv.fs_lstat(path) if stat then return stat.type == 'directory' end return false end --- @async --- @param path string --- @return boolean function Path.is_abs(path) -- Check if the path is absolute on Windows if is_win and M.cygpath(path):match('^%a:[/\\]') then return true end -- Check if the path is absolute on Unix-like systems return vim.startswith(path, '/') end function Path.join(...) if vim.fs.joinpath then return vim.fs.joinpath(...) end local path = table.concat({ ... }, '/') if is_win then path = path:gsub('\\', '/') end return (path:gsub('//+', '/')) end M.Path = Path --- @param path string --- @return string[] function M.file_lines(path) local file = assert(io.open(path, 'rb')) local contents = file:read('*a') file:close() return vim.split(contents, '\n') end M.path_sep = package.config:sub(1, 1) --- @param ... integer --- @return string local function make_bom(...) local r = {} ---@diagnostic disable-next-line:no-unknown for i, a in ipairs({ ... }) do ---@diagnostic disable-next-line:no-unknown r[i] = string.char(a) end return table.concat(r) end local BOM_TABLE = { ['utf-8'] = make_bom(0xef, 0xbb, 0xbf), ['utf-16le'] = make_bom(0xff, 0xfe), ['utf-16'] = make_bom(0xfe, 0xff), ['utf-16be'] = make_bom(0xfe, 0xff), ['utf-32le'] = make_bom(0xff, 0xfe, 0x00, 0x00), ['utf-32'] = make_bom(0xff, 0xfe, 0x00, 0x00), ['utf-32be'] = make_bom(0x00, 0x00, 0xfe, 0xff), ['utf-7'] = make_bom(0x2b, 0x2f, 0x76), ['utf-1'] = make_bom(0xf7, 0x54, 0x4c), } ---@param x string? ---@param encoding string ---@return string? local function add_bom(x, encoding) local bom = BOM_TABLE[encoding] if bom then return x and bom .. x or bom end return x end --- @param bufnr integer --- @return string[] function M.buf_lines(bufnr) -- nvim_buf_get_lines strips carriage returns if fileformat==dos local buftext = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local dos = vim.bo[bufnr].fileformat == 'dos' if dos then for i = 1, #buftext - 1 do buftext[i] = buftext[i] .. '\r' end end if vim.bo[bufnr].endofline then -- Add CR to the last line if dos then buftext[#buftext] = buftext[#buftext] .. '\r' end buftext[#buftext + 1] = '' end if vim.bo[bufnr].bomb then buftext[1] = add_bom(buftext[1], vim.bo[bufnr].fileencoding) end return buftext end --- @param buf integer local function delete_alt(buf) local alt = vim.api.nvim_buf_call(buf, function() return vim.fn.bufnr('#') end) if alt ~= buf and alt ~= -1 then pcall(vim.api.nvim_buf_delete, alt, { force = true }) end end --- @param bufnr integer --- @param name string function M.buf_rename(bufnr, name) vim.api.nvim_buf_set_name(bufnr, name) delete_alt(bufnr) end --- @param events string[] --- @param f fun() function M.noautocmd(events, f) local ei = vim.o.eventignore vim.o.eventignore = table.concat(events, ',') f() vim.o.eventignore = ei end --- @param bufnr integer --- @param start_row integer --- @param end_row integer --- @param lines string[] function M.set_lines(bufnr, start_row, end_row, lines) if vim.bo[bufnr].fileformat == 'dos' then lines = M.strip_cr(lines) end if start_row == 0 and end_row == -1 then if lines[#lines] == '' then lines = vim.deepcopy(lines) --[[@as string[] ]] lines[#lines] = nil else vim.bo[bufnr].eol = false end end vim.api.nvim_buf_set_lines(bufnr, start_row, end_row, false, lines) end --- @param time number --- @param divisor integer --- @param time_word string --- @return string local function to_relative_string(time, divisor, time_word) local num = math.floor(time / divisor) if num > 1 then time_word = time_word .. 's' end return num .. ' ' .. time_word .. ' ago' end --- @param timestamp number --- @return string function M.get_relative_time(timestamp) local current_timestamp = os.time() local elapsed = current_timestamp - timestamp if elapsed == 0 then return 'a while ago' end local minute_seconds = 60 local hour_seconds = minute_seconds * 60 local day_seconds = hour_seconds * 24 local month_seconds = day_seconds * 30 local year_seconds = month_seconds * 12 if elapsed < minute_seconds then return to_relative_string(elapsed, 1, 'second') elseif elapsed < hour_seconds then return to_relative_string(elapsed, minute_seconds, 'minute') elseif elapsed < day_seconds then return to_relative_string(elapsed, hour_seconds, 'hour') elseif elapsed < month_seconds then return to_relative_string(elapsed, day_seconds, 'day') elseif elapsed < year_seconds then return to_relative_string(elapsed, month_seconds, 'month') else return to_relative_string(elapsed, year_seconds, 'year') end end --- @param opts vim.api.keyset.redraw function M.redraw(opts) if vim.fn.has('nvim-0.10') == 1 then vim.api.nvim__redraw(opts) elseif opts.range then vim.api.nvim__buf_redraw_range(opts.buf or 0, opts.range[1], opts.range[2]) end end --- @param xs string[] --- @return boolean local function is_dos(xs) -- Do not check CR at EOF for i = 1, #xs - 1 do local x = xs[i] --[[@as string]] if x:sub(-1) ~= '\r' then return false end end return true end --- Strip '\r' from the EOL of each line only if all lines end with '\r' --- @param xs0 string[] --- @return string[] function M.strip_cr(xs0) if not is_dos(xs0) then -- don't strip, return early return xs0 end -- all lines end with '\r', need to strip local xs = vim.deepcopy(xs0) for i = 1, #xs do local x = xs[i] --[[@as string]] xs[i] = x:sub(1, -2) end return xs end --- @param base? string --- @return string? function M.norm_base(base) if base == ':0' then return end if base and base:sub(1, 1):match('[~\\^]') then base = 'HEAD' .. base end return base end function M.emptytable() return setmetatable({}, { ---@param t table ---@param k any ---@return any __index = function(t, k) t[k] = {} return t[k] end, }) end local function expand_date(fmt, time) if fmt == '%R' then return M.get_relative_time(time) end return os.date(fmt, time) end ---@param fmt string ---@param info table ---@return string function M.expand_format(fmt, info) local ret = {} --- @type string[] for _ = 1, 20 do -- loop protection -- Capture or local scol, ecol, match, key, time_fmt = fmt:find('(<([^:>]+):?([^>]*)>)') if not match then break end --- @cast scol -? --- @cast ecol -? --- @cast key string ret[#ret + 1], fmt = fmt:sub(1, scol - 1), fmt:sub(ecol + 1) local v = info[key] if v then if type(v) == 'table' then v = table.concat(v, '\n') end if vim.endswith(key, '_time') then if time_fmt == '' then time_fmt = '%Y-%m-%d' end v = expand_date(time_fmt, v) end match = tostring(v) end ret[#ret + 1] = match end ret[#ret + 1] = fmt return table.concat(ret, '') end --- @param buf string --- @return boolean function M.bufexists(buf) return vim.fn.bufexists(buf) == 1 end --- @param x Gitsigns.BlameInfo --- @return Gitsigns.BlameInfoPublic function M.convert_blame_info(x) --- @type Gitsigns.BlameInfoPublic local ret = vim.tbl_extend('error', x, x.commit) ret.commit = nil return ret end --- Efficiently remove items from middle of a list. --- --- Calling table.remove() in a loop will re-index the tail of the table on --- every iteration, instead this function will re-index the table exactly --- once. --- --- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524 --- ---@param t any[] ---@param first integer ---@param last integer function M.list_remove(t, first, last) local n = table.maxn(t) for i = 0, n - first do t[first + i] = t[last + 1 + i] t[last + 1 + i] = nil end end --- Efficiently insert items into the middle of a list. --- --- Calling table.insert() in a loop will re-index the tail of the table on --- every iteration, instead this function will re-index the table exactly --- once. --- --- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524 --- ---@param t any[] ---@param first integer ---@param last integer ---@param v any function M.list_insert(t, first, last, v) local n = table.maxn(t) -- Shift table forward for i = n - first, 0, -1 do t[last + 1 + i] = t[first + i] end -- Fill in new values for i = first, last do t[i] = v end end --- Run a function once and ignore subsequent calls --- @generic F: function --- @param fn F --- @return F function M.once(fn) local called = false return function(...) if called then return end called = true return fn(...) end end --- @param x any --- @return integer? function M.tointeger(x) local nx = tonumber(x) if nx and nx == math.floor(nx) then --- @cast nx integer return nx end end local has_cygpath --- @type boolean? --- @async --- @param path string --- @param mode? 'unix'|'windows' (default: 'windows') --- @return string function M.cygpath(path, mode) local async = require('gitsigns.async') local system = require('gitsigns.system').system if has_cygpath == nil then has_cygpath = is_win and vim.fn.executable('cygpath') == 1 end if not has_cygpath or uv.fs_stat(path) then return path end -- If on windows and path isn't recognizable as a file, try passing it -- through cygpath --- @type string local stdout = async.await(3, system, { 'cygpath', '--absolute', '--' .. (mode or 'windows'), path, }, { text = true }).stdout async.schedule() local result = vim.split(stdout, '\n')[1] -- Strip trailing newline/carriage return that may be present in cygpath output on MSYS2 if result then result = result:match('^(.-)[\r\n]*$') end return assert(result) end --- Flattens a nested table structure into a flat array of strings. Only --- traverses numeric keys, recursively flattening tables and collecting --- strings. --- @param x table The input table to flatten. --- @return string[] # A flat array of strings extracted from the nested table. function M.flatten(x) local ret = {} --- @type string[] for i = 1, table.maxn(x) do local v = x[i] if type(v) == 'table' then vim.list_extend(ret, M.flatten(v)) elseif type(v) == 'string' then ret[#ret + 1] = v elseif not v then -- skip else error('Expected string, table, false or nil, got ' .. type(v)) end end return ret end --- @param fn fun() --- @return userdata function M.gc_proxy(fn) local proxy = newproxy(true) getmetatable(proxy).__gc = fn return proxy end --- @generic T --- @param x T --- @return {ref: T} function M.weak_ref(x) return setmetatable({ ref = x }, { __mode = 'v' }) end return M neovim-gitsigns-2.0.0/plugin/000077500000000000000000000000001513053142700161325ustar00rootroot00000000000000neovim-gitsigns-2.0.0/plugin/gitsigns.lua000066400000000000000000000000341513053142700204610ustar00rootroot00000000000000require('gitsigns').setup() neovim-gitsigns-2.0.0/release-please-config.json000066400000000000000000000004451513053142700216640ustar00rootroot00000000000000{ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "release-type": "simple", "include-component-in-tag": false, "bump-minor-pre-major": true, "packages": { ".": { "extra-files": [ "gen_help.lua" ] } } } neovim-gitsigns-2.0.0/test/000077500000000000000000000000001513053142700156135ustar00rootroot00000000000000neovim-gitsigns-2.0.0/test/actions_spec.lua000066400000000000000000000220561513053142700207750ustar00rootroot00000000000000local helpers = require('test.gs_helpers') local setup_gitsigns = helpers.setup_gitsigns local feed = helpers.feed local test_file = helpers.test_file local edit = helpers.edit local check = helpers.check local exec_lua = helpers.exec_lua local fn = helpers.fn local system = fn.system local test_config = helpers.test_config local clear = helpers.clear local setup_test_repo = helpers.setup_test_repo local eq = helpers.eq local expectf = helpers.expectf helpers.env() --- @param exp_hunks string[] local function expect_hunks(exp_hunks) expectf(function() --- @type table[] local hunks = exec_lua("return require('gitsigns').get_hunks()") if #exp_hunks ~= #hunks then local msg = {} --- @type string[] msg[#msg + 1] = '' msg[#msg + 1] = string.format( 'Number of hunks do not match. Expected: %d, passed in: %d', #exp_hunks, #hunks ) msg[#msg + 1] = '\nExpected hunks:' for _, h in ipairs(exp_hunks) do msg[#msg + 1] = h end msg[#msg + 1] = '\nPassed in hunks:' for _, h in ipairs(hunks) do msg[#msg + 1] = h.head end error(table.concat(msg, '\n')) end for i, hunk in ipairs(hunks) do eq(exp_hunks[i], hunk.head) end end) end local delay = 10 --- @param cmd string local function command(cmd) helpers.sleep(delay) helpers.api.nvim_command(cmd) -- Flaky tests, add a large delay between commands. -- Flakiness is due to actions being async and problems occur when an action -- is run while another action or update is running. -- Must wait for actions and updates to finish. helpers.sleep(delay) end local function retry(f) local orig_delay = delay local ok, err --- @type boolean, string? for _ = 1, 20 do --- @type boolean, string? ok, err = pcall(f) if ok then return end delay = math.ceil(delay * 1.6) print('failed, retrying with delay', delay) end if err then delay = orig_delay error(err) end end describe('actions', function() local orig_it = it local function it(desc, f) orig_it(desc, function() retry(f) end) end before_each(function() clear() command('cd ' .. system({ 'dirname', os.tmpname() })) setup_gitsigns(test_config) end) it('works with commands', function() setup_test_repo() edit(test_file) feed('jjjccEDIT') check({ status = { head = 'main', added = 0, changed = 1, removed = 0 }, signs = { changed = 1 }, }) command('Gitsigns stage_hunk') check({ status = { head = 'main', added = 0, changed = 0, removed = 0 }, signs = {}, }) command('Gitsigns undo_stage_hunk') check({ status = { head = 'main', added = 0, changed = 1, removed = 0 }, signs = { changed = 1 }, }) command('Gitsigns stage_hunk') check({ status = { head = 'main', added = 0, changed = 0, removed = 0 }, signs = {}, }) command('Gitsigns stage_hunk') check({ status = { head = 'main', added = 0, changed = 1, removed = 0 }, signs = { changed = 1 }, }) -- Add multiple edits feed('ggccThat') check({ status = { head = 'main', added = 0, changed = 2, removed = 0 }, signs = { changed = 2 }, }) command('Gitsigns stage_buffer') check({ status = { head = 'main', added = 0, changed = 0, removed = 0 }, signs = {}, }) command('Gitsigns reset_buffer_index') check({ status = { head = 'main', added = 0, changed = 2, removed = 0 }, signs = { changed = 2 }, }) command('Gitsigns reset_hunk') check({ status = { head = 'main', added = 0, changed = 1, removed = 0 }, signs = { changed = 1 }, }) end) describe('staging partial hunks', function() setup(function() clear() setup_test_repo({ test_file_text = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H' } }) end) before_each(function() helpers.git('reset', '--hard') edit(test_file) end) local function set_lines(start, dend, lines) helpers.api.nvim_buf_set_lines(0, start, dend, false, lines) end describe('can stage add hunks', function() before_each(function() set_lines(2, 2, { 'c1', 'c2', 'c3', 'c4' }) expect_hunks({ '@@ -2 +3,4 @@' }) end) it('contained in range', function() command([[1,7 Gitsigns stage_hunk]]) expect_hunks({}) end) it('containing range', function() command([[4,5 Gitsigns stage_hunk]]) expect_hunks({ '@@ -2 +3,1 @@', '@@ -4 +6,1 @@', }) end) it('from top range', function() command([[1,4 Gitsigns stage_hunk]]) expect_hunks({ '@@ -4 +5,2 @@' }) end) it('from bottom range', function() command([[4,7 Gitsigns stage_hunk]]) expect_hunks({ '@@ -2 +3,1 @@' }) command([[Gitsigns reset_buffer_index]]) expect_hunks({ '@@ -2 +3,4 @@' }) command([[4,10 Gitsigns stage_hunk]]) expect_hunks({ '@@ -2 +3,1 @@' }) end) end) describe('can stage modified-add hunks', function() before_each(function() set_lines(2, 4, { 'c1', 'c2', 'c3', 'c4', 'c5' }) expect_hunks({ '@@ -3,2 +3,5 @@' }) end) it('from top range containing mod', function() command([[2,3 Gitsigns stage_hunk]]) expect_hunks({ '@@ -4,1 +4,4 @@' }) end) it('from top range containing mod-add', function() command([[2,5 Gitsigns stage_hunk]]) expect_hunks({ '@@ -5 +6,2 @@' }) end) it('from bottom range containing add', function() command([[6,8 Gitsigns stage_hunk]]) expect_hunks({ '@@ -3,2 +3,3 @@' }) end) it('containing range containing add', function() command('write') command([[5,6 Gitsigns stage_hunk]]) expect_hunks({ '@@ -3,2 +3,2 @@', '@@ -6 +7,1 @@', }) end) end) describe('can stage modified-remove hunks', function() before_each(function() set_lines(2, 7, { 'c1', 'c2', 'c3' }) command('write') expect_hunks({ '@@ -3,5 +3,3 @@' }) end) it('from top range', function() expect_hunks({ '@@ -3,5 +3,3 @@' }) command([[2,3 Gitsigns stage_hunk]]) expect_hunks({ '@@ -4,4 +4,2 @@' }) command([[2,3 Gitsigns reset_buffer_index]]) expect_hunks({ '@@ -3,5 +3,3 @@' }) command([[2,4 Gitsigns stage_hunk]]) expect_hunks({ '@@ -5,3 +5,1 @@' }) end) it('from bottom range', function() expect_hunks({ '@@ -3,5 +3,3 @@' }) command([[4,6 Gitsigns stage_hunk]]) expect_hunks({ '@@ -3,1 +3,1 @@' }) command([[2,3 Gitsigns reset_buffer_index]]) expect_hunks({ '@@ -3,5 +3,3 @@' }) command([[5,6 Gitsigns stage_hunk]]) expect_hunks({ '@@ -3,2 +3,2 @@' }) end) end) it('can stage remove hunks', function() set_lines(2, 5, {}) expect_hunks({ '@@ -3,3 +2 @@' }) command([[2 Gitsigns stage_hunk]]) expect_hunks({}) end) end) local function check_cursor(pos) eq(pos, helpers.api.nvim_win_get_cursor(0)) end it('can navigate hunks', function() setup_test_repo() edit(test_file) feed('dd') feed('4Gx') feed('6Gx') expect_hunks({ '@@ -1,1 +0 @@', '@@ -5,1 +4,1 @@', '@@ -7,1 +6,1 @@', }) check_cursor({ 6, 0 }) command('Gitsigns next_hunk') -- Wrap check_cursor({ 1, 0 }) command('Gitsigns next_hunk') check_cursor({ 4, 0 }) command('Gitsigns next_hunk') check_cursor({ 6, 0 }) command('Gitsigns prev_hunk') check_cursor({ 4, 0 }) command('Gitsigns prev_hunk') check_cursor({ 1, 0 }) command('Gitsigns prev_hunk') -- Wrap check_cursor({ 6, 0 }) end) it('can navigate hunks (nowrap)', function() setup_test_repo() edit(test_file) feed('4Gx') feed('6Gx') feed('gg') expect_hunks({ '@@ -4,1 +4,1 @@', '@@ -6,1 +6,1 @@', }) command('set nowrapscan') check_cursor({ 1, 0 }) command('Gitsigns next_hunk') check_cursor({ 4, 0 }) command('Gitsigns next_hunk') check_cursor({ 6, 0 }) command('Gitsigns next_hunk') check_cursor({ 6, 0 }) feed('G') check_cursor({ 18, 0 }) command('Gitsigns prev_hunk') check_cursor({ 6, 0 }) command('Gitsigns prev_hunk') check_cursor({ 4, 0 }) command('Gitsigns prev_hunk') check_cursor({ 4, 0 }) end) it('can stage hunks with no NL at EOF', function() setup_test_repo() local newfile = helpers.newfile exec_lua([[vim.g.editorconfig = false]]) system("printf 'This is a file with no nl at eof' > " .. newfile) helpers.git('add', newfile) helpers.git('commit', '-m', 'commit on main') edit(newfile) check({ status = { head = 'main', added = 0, changed = 0, removed = 0 } }) feed('x') check({ status = { head = 'main', added = 0, changed = 1, removed = 0 } }) command('Gitsigns stage_hunk') check({ status = { head = 'main', added = 0, changed = 0, removed = 0 } }) end) end) neovim-gitsigns-2.0.0/test/debounce_spec.lua000066400000000000000000000066211513053142700211210ustar00rootroot00000000000000--- @diagnostic disable: global-in-non-module, redundant-parameter local helpers = require('test.gs_helpers') local clear = helpers.clear local eq = helpers.eq local exec_lua = helpers.exec_lua helpers.env() describe('debounce', function() before_each(function() clear() helpers.setup_path() end) it('closes the timer even if the function errors', function() exec_lua(function() local uv = vim.uv or vim.loop ---@diagnostic disable-line: deprecated _G._debounce_close_called = 0 local orig_new_timer = uv.new_timer uv.new_timer = function(...) local t = assert(orig_new_timer(...)) local proxy = { _t = t } function proxy:start(...) return self._t:start(...) end function proxy:close(...) _G._debounce_close_called = _G._debounce_close_called + 1 return self._t:close(...) end return proxy end local debounce_trailing = require('gitsigns.debounce').debounce_trailing local debounced = debounce_trailing(1, function() error('GS_DEBOUNCE_TEST_CLOSE') end) debounced() end) helpers.expectf(function() eq(1, exec_lua('return _G._debounce_close_called')) end) end) it('prints a full stacktrace if the function errors', function() exec_lua(function() local debounce_trailing = require('gitsigns.debounce').debounce_trailing local debounced = debounce_trailing(1, function() error('GS_DEBOUNCE_TEST_STACK') end) debounced() end) helpers.expectf(function() local messages = exec_lua(function() return vim.api.nvim_exec2('messages', { output = true }).output end) ---@type string assert(messages:match('debounce_spec.lua:%d+: GS_DEBOUNCE_TEST_STACK'), messages) assert(messages:match('stack traceback'), messages) assert(messages:match('lua/gitsigns/debounce.lua'), messages) end) end) it('debounces independently by hash key', function() exec_lua(function() local debounce_trailing = require('gitsigns.debounce').debounce_trailing _G._debounce_hash_calls = {} local debounced = debounce_trailing({ timeout = 10, hash = 1, }, function(id, value) local t = _G._debounce_hash_calls[id] or { count = 0, value = nil } t.count = t.count + 1 t.value = value _G._debounce_hash_calls[id] = t end) debounced('a', 1) debounced('a', 2) debounced('b', 3) debounced('b', 4) end) helpers.expectf(function() eq({ a = { count = 1, value = 2 }, b = { count = 1, value = 4 }, }, exec_lua('return _G._debounce_hash_calls')) end) end) it('accepts a hash function for ids', function() exec_lua(function() local debounce_trailing = require('gitsigns.debounce').debounce_trailing _G._debounce_hash_fn_calls = {} local debounced = debounce_trailing({ timeout = 10, hash = function(id) return id end, }, function(id, value) _G._debounce_hash_fn_calls[id] = (_G._debounce_hash_fn_calls[id] or 0) + 1 _G._debounce_hash_fn_value = value end) debounced('a', 1) debounced('a', 2) end) helpers.expectf(function() eq(1, exec_lua("return _G._debounce_hash_fn_calls['a']")) eq(2, exec_lua('return _G._debounce_hash_fn_value')) end) end) end) neovim-gitsigns-2.0.0/test/gitdir_watcher_spec.lua000066400000000000000000000140671513053142700223370ustar00rootroot00000000000000--- @diagnostic disable: access-invisible local helpers = require('test.gs_helpers') local clear = helpers.clear local system = helpers.fn.system local edit = helpers.edit local eq = helpers.eq local setup_test_repo = helpers.setup_test_repo local cleanup = helpers.cleanup local command = helpers.api.nvim_command local test_config = helpers.test_config local match_debug_messages = helpers.match_debug_messages local n, p, np = helpers.n, helpers.p, helpers.np local setup_gitsigns = helpers.setup_gitsigns local test_file = helpers.test_file local git = helpers.git helpers.env() local function get_bufs() local bufs = {} --- @type table for _, b in ipairs(helpers.api.nvim_list_bufs()) do bufs[b] = helpers.api.nvim_buf_get_name(b) end return bufs end describe('gitdir_watcher', function() before_each(function() clear() command('cd ' .. system({ 'dirname', os.tmpname() })) end) after_each(function() cleanup() end) it('can follow moved files', function() setup_test_repo() setup_gitsigns(test_config) command('Gitsigns clear_debug') edit(test_file) local revparse_pat = ('system.system: git .* rev-parse --show-toplevel --absolute-git-dir --abbrev-ref HEAD'):gsub( '%-', '%%-' ) match_debug_messages({ 'attach.attach(1): Attaching (trigger=BufReadPost)', np(revparse_pat), np('system.system: git .* config user.name'), np('system.system: git .* ls%-files .* ' .. vim.pesc(test_file)), np('attach%.attach%(1%): Watching git dir .*'), np('system.system: git .* show .*'), }) eq({ [1] = test_file }, get_bufs()) command('Gitsigns clear_debug') local test_file2 = test_file .. '2' git('mv', test_file, test_file2) match_debug_messages({ p('git.repo.watcher.watcher.handler: Git dir update: .*'), np('system.system: git .* ls%-files .* ' .. vim.pesc(test_file)), np('system.system: git .* diff %-%-name%-status .* %-%-cached'), n('attach.handle_moved(1): File moved to dummy.txt2'), np('system.system: git .* ls%-files .* ' .. vim.pesc(test_file2)), np('attach%.handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt to .*/dummy.txt2'), np('system.system: git .* show .*'), }) eq({ [1] = test_file2 }, get_bufs()) command('Gitsigns clear_debug') local test_file3 = test_file .. '3' git('mv', test_file2, test_file3) match_debug_messages({ p('git.repo.watcher.watcher.handler: Git dir update: .*'), np('system.system: git .* ls%-files .* ' .. vim.pesc(test_file2)), np('system.system: git .* diff %-%-name%-status .* %-%-cached'), n('attach.handle_moved(1): File moved to dummy.txt3'), np('system.system: git .* ls%-files .* ' .. vim.pesc(test_file3)), np('attach%.handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt2 to .*/dummy.txt3'), np('system.system: git .* show .*'), }) eq({ [1] = test_file3 }, get_bufs()) command('Gitsigns clear_debug') git('mv', test_file3, test_file) match_debug_messages({ p('git.repo.watcher.watcher.handler: Git dir update: .*'), np('system.system: git .* ls%-files .* ' .. vim.pesc(test_file3)), np('system.system: git .* diff %-%-name%-status .* %-%-cached'), np('system.system: git .* ls%-files .* ' .. vim.pesc(test_file)), n('attach.handle_moved(1): Moved file reset'), np('system.system: git .* ls%-files .* ' .. vim.pesc(test_file)), np('attach%.handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt3 to .*/dummy.txt'), np('system.system: git .* show .*'), }) eq({ [1] = test_file }, get_bufs()) end) it('can debounce and throttle updates per buffer', function() helpers.git_init_scratch() local f1 = vim.fs.joinpath(helpers.scratch, 'file1') local f2 = vim.fs.joinpath(helpers.scratch, 'file2') helpers.write_to_file(f1, { '1', '2', '3' }) helpers.write_to_file(f2, { '1', '2', '3' }) git('add', f1, f2) git('commit', '-m', 'init commit') setup_gitsigns(test_config) command('edit ' .. f1) helpers.feed('Aa') command('write') local b1 = helpers.api.nvim_get_current_buf() command('split ' .. f2) helpers.feed('Ab') command('write') local b2 = helpers.api.nvim_get_current_buf() helpers.check({ signs = { changed = 1 } }, b1) helpers.check({ signs = { changed = 1 } }, b2) git('add', f1, f2) helpers.check({ signs = {} }, b1) helpers.check({ signs = {} }, b2) end) it('garbage collects repo and watcher', function() setup_test_repo() helpers.setup_path() local result = helpers.exec_lua(function(scratch) local async = require('gitsigns.async') local Repo = require('gitsigns.git.repo') local repo, err = async.run(Repo.get, scratch):wait(5000) assert(repo, err) local gitdir = repo.gitdir local watcher = repo._watcher local handle = watcher.handle local function get_upvalue(fn, key) for i = 1, 50 do local name, value = debug.getupvalue(fn, i) if not name then break end if name == key then return value end end end local repo_cache = get_upvalue(Repo.get, 'repo_cache') assert(repo_cache, 'repo_cache not found') local weak = setmetatable({ repo, watcher }, { __mode = 'v' }) --- @diagnostic disable-next-line: unused, assign-type-mismatch --- assign to nil to allow gc watcher, repo = nil, nil vim.wait(2000, function() collectgarbage('collect') return weak[1] == nil and weak[2] == nil and repo_cache[gitdir] == nil and handle:is_closing() end, 20, false) return { repo_gced = weak[1] == nil, watcher_gced = weak[2] == nil, cache_cleared = repo_cache[gitdir] == nil, handle_closed = handle:is_closing(), } end, helpers.scratch) eq(true, result.repo_gced) eq(true, result.watcher_gced) eq(true, result.cache_cleared) eq(true, result.handle_closed) end) end) neovim-gitsigns-2.0.0/test/gitsigns_spec.lua000066400000000000000000000761471513053142700211760ustar00rootroot00000000000000local Screen = require('nvim-test.screen') local helpers = require('test.gs_helpers') local api = helpers.api local check = helpers.check local cleanup = helpers.cleanup local clear = helpers.clear local command = api.nvim_command local edit = helpers.edit local eq = helpers.eq local exec_lua = helpers.exec_lua local expectf = helpers.expectf local feed = helpers.feed local fn = helpers.fn local get_buf_var = api.nvim_buf_get_var local git = helpers.git local insert = helpers.insert local match_dag = helpers.match_dag local match_debug_messages = helpers.match_debug_messages local match_lines = helpers.match_lines local n, p, np = helpers.n, helpers.p, helpers.np local newfile = helpers.newfile local scratch = helpers.scratch local setup_gitsigns = helpers.setup_gitsigns local setup_test_repo = helpers.setup_test_repo local split = vim.split local system = fn.system local test_config = helpers.test_config local test_file = helpers.test_file local write_to_file = helpers.write_to_file helpers.env() ---@param bufnr? integer local function wait_for_attach(bufnr) expectf(function() return exec_lua(function(bufnr0) return vim.b[bufnr0 or 0].gitsigns_status_dict.gitdir ~= nil end, bufnr) end) match_debug_messages({ ('attach.attach(1): attach complete'):format(bufnr or api.nvim_get_current_buf()), }) end local revparse_pat = ('system.system: git .* rev-parse --show-toplevel --absolute-git-dir --abbrev-ref HEAD'):gsub( '%-', '%%-' ) describe('gitsigns (with screen)', function() local screen --- @type test.screen local config --- @type table before_each(function() clear() screen = Screen.new(20, 17) screen:attach({ ext_messages = true }) local default_attrs = { [1] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.WebGray }, [2] = { foreground = Screen.colors.DodgerBlue }, [3] = { foreground = Screen.colors.SeaGreen }, [4] = { foreground = Screen.colors.Red }, [5] = { foreground = Screen.colors.Brown }, [6] = { foreground = Screen.colors.Blue1, bold = true }, [7] = { bold = true }, [8] = { foreground = Screen.colors.White, background = Screen.colors.Red }, [9] = { foreground = Screen.colors.SeaGreen, bold = true }, [11] = { foreground = Screen.colors.Red1, background = Screen.colors.WebGray }, [12] = { foreground = Screen.colors.DodgerBlue, background = Screen.colors.WebGray }, } -- Use the classic vim colorscheme, not the new defaults in nvim >= 0.10 if fn.has('nvim-0.12') == 0 then default_attrs[2].foreground = Screen.colors.NvimDarkCyan default_attrs[3].foreground = Screen.colors.NvimDarkGreen default_attrs[4].foreground = Screen.colors.NvimDarkRed default_attrs[11].foreground = Screen.colors.NvimDarkRed default_attrs[12] = { foreground = Screen.colors.NvimDarkCyan, background = Screen.colors.Gray } end command('colorscheme vim') screen:set_default_attr_ids(default_attrs) config = vim.deepcopy(test_config) command('cd ' .. system({ 'dirname', os.tmpname() })) end) after_each(function() cleanup() screen:detach() end) it('can run basic setup', function() setup_gitsigns() check({ status = {}, signs = {} }) end) it('gitdir watcher works on a fresh repo', function() --- @type integer local nvim_ver = exec_lua('return vim.version().minor') screen:try_resize(20, 6) setup_test_repo({ no_add = true }) -- Don't set this too low, or else the test will lock up config.watch_gitdir = { interval = 100 } setup_gitsigns(config) edit(test_file) match_dag({ 'attach.attach(1): Attaching (trigger=BufReadPost)', p('system.system: git .* config user.name'), p(revparse_pat), p( 'system.system: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard %-%-eol ' .. vim.pesc(test_file) ), p('attach%.attach%(1%): Watching git dir .*'), }) check({ status = { head = '', added = 18, changed = 0, removed = 0 }, signs = { untracked = nvim_ver == 9 and 8 or 7 }, }) git('add', test_file) check({ status = { head = '', added = 0, changed = 0, removed = 0 }, signs = {}, }) end) it('can open files not in a git repo', function() setup_gitsigns(config) local tmpfile = os.tmpname() edit(tmpfile) match_debug_messages({ 'attach.attach(1): Attaching (trigger=BufReadPost)', np(revparse_pat), np('Not in git repo'), np('Empty git obj'), }) command('Gitsigns clear_debug') insert('line') command('write') match_debug_messages({ n('attach.attach(1): Attaching (trigger=BufWritePost)'), np(revparse_pat), n('git.new: Not in git repo'), n('attach.attach(1): Empty git obj'), }) end) describe('when attaching', function() before_each(function() setup_test_repo() setup_gitsigns(config) end) it('can setup mappings', function() edit(test_file) expectf(function() local res = split(api.nvim_exec2('nmap ', { output = true }).output, '\n') table.sort(res) -- Check all keymaps get set match_lines(res, { n('n mhS *@lua require"gitsigns".stage_buffer()'), n('n mhU *@lua require"gitsigns".reset_buffer_index()'), n('n mhp *@lua require"gitsigns".preview_hunk()'), n('n mhr *@lua require"gitsigns".reset_hunk()'), n('n mhs *@lua require"gitsigns".stage_hunk()'), n('n mhu *@lua require"gitsigns".undo_stage_hunk()'), }) end) end) it('does not attach inside .git', function() edit(scratch .. '/.git/index') match_debug_messages({ 'attach.attach(1): Attaching (trigger=BufReadPost)', n('system.system: git --version'), p(revparse_pat), n('git.new: Not in git repo'), n('attach.attach(1): Empty git obj'), }) end) it("doesn't attach to ignored files", function() write_to_file(scratch .. '/.gitignore', { 'dummy_ignored.txt' }) local ignored_file = scratch .. '/dummy_ignored.txt' system({ 'touch', ignored_file }) edit(ignored_file) match_debug_messages({ 'attach.attach(1): Attaching (trigger=BufReadPost)', np(revparse_pat), np('system.system: git .* config user.name'), np('system.system: git .* ls%-files .*/dummy_ignored.txt'), n('attach.attach(1): Cannot resolve file in repo'), }) check({ status = { head = 'main' } }) end) it("doesn't attach to non-existent files", function() edit(newfile) match_debug_messages({ 'attach.attach(1): Attaching (trigger=BufNewFile)', np(revparse_pat), np('system.system: git .* config user.name'), np( 'system.system: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard %-%-eol ' .. vim.pesc(newfile) ), 'attach.attach(1): Cannot resolve file in repo', }) check({ status = { head = 'main' } }) end) it("doesn't attach to non-existent files with non-existent sub-dirs", function() edit(scratch .. '/does/not/exist') match_debug_messages({ 'attach.attach(1): Attaching (trigger=BufNewFile)', n('attach.attach(1): Not a path'), }) helpers.pcall_err(get_buf_var, 0, 'gitsigns_head') helpers.pcall_err(get_buf_var, 0, 'gitsigns_status_dict') end) it('can run copen', function() command('copen') match_debug_messages({ 'attach.attach(2): Attaching (trigger=BufReadPost)', n('attach.attach(2): Non-normal buffer'), }) end) it('can run get_hunks()', function() edit(test_file) insert('line1') feed('oline2') expectf(function() eq({ { head = '@@ -1,1 +1,2 @@', type = 'change', lines = { '-This', '+line1This', '+line2' }, added = { count = 2, start = 1, lines = { 'line1This', 'line2' } }, removed = { count = 1, start = 1, lines = { 'This' } }, }, }, exec_lua([[return require'gitsigns'.get_hunks()]])) end) end) end) describe('current line blame', function() before_each(function() config.current_line_blame = true config.current_line_blame_formatter = ' , - ' setup_gitsigns(config) end) local function blame_line_ui_test(autocrlf, file_ending) setup_test_repo() exec_lua([[vim.g.editorconfig = false]]) git('config', 'core.autocrlf', autocrlf) if file_ending == 'dos' then system("printf 'This\r\nis\r\na\r\nwindows\r\nfile\r\n' > " .. newfile) else system("printf 'This\nis\na\nwindows\nfile\n' > " .. newfile) end git('add', newfile) git('commit', '-m', 'commit on main') edit(newfile) feed('gg') check({ signs = {} }) screen:expect({ grid = [[ ^{MATCH:This {6: You, %d second.}}| is | a | windows | file | {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| ]], }) end it('does handle dos fileformats', function() -- Add a file with windows line ending into the repo -- Disable autocrlf, so that the file keeps the \r\n file endings. blame_line_ui_test('false', 'dos') end) it('does handle autocrlf', function() blame_line_ui_test('true', 'dos') end) it('does handle unix', function() blame_line_ui_test('false', 'unix') end) end) describe('falls back from right_align to eol when text is too long (#1322)', function() before_each(function() setup_test_repo({ test_file_text = { 'short', string.rep('a', 25), string.rep('b', 40), }, }) config.current_line_blame = true config.current_line_blame_formatter = ' , - ' config.current_line_blame_opts = { virt_text_pos = 'right_align' } setup_gitsigns(config) end) it('with nowrap', function() edit(test_file) command('set nowrap') feed('gg') screen:expect({ grid = [[ ^short {MATCH:{6: You, %d+ second}}| aaaaaaaaaaaaaaaaaaaa| bbbbbbbbbbbbbbbbbbbb| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| ]], }) -- Medium line: blame should fallback to eol (no space for right_align) feed('j') screen:expect({ grid = [[ short | ^aaaaaaaaaaaaaaaaaaaa| bbbbbbbbbbbbbbbbbbbb| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| ]], }) -- Move to very long line feed('j') screen:expect({ grid = [[ short | aaaaaaaaaaaaaaaaaaaa| ^bbbbbbbbbbbbbbbbbbbb| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| ]], }) end) it('with wrap', function() edit(test_file) command('set wrap') feed('gg') -- Short line: blame should appear with right_align (normal behavior) screen:expect({ grid = [[ ^short {MATCH:{6: You, %d+ second}}| aaaaaaaaaaaaaaaaaaaa| aaaaa | bbbbbbbbbbbbbbbbbbbb| bbbbbbbbbbbbbbbbbbbb| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| ]], }) -- Move to medium line (will wrap and blame appears at end of wrapped line) feed('j') screen:expect({ grid = [[ short | ^aaaaaaaaaaaaaaaaaaaa| {MATCH:aaaaa {6: You, %d second.*}}| bbbbbbbbbbbbbbbbbbbb| bbbbbbbbbbbbbbbbbbbb| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| ]], }) -- Move to very long line (wraps across multiple lines, no blame visible) feed('j') screen:expect({ grid = [[ short | aaaaaaaaaaaaaaaaaaaa| aaaaa | ^bbbbbbbbbbbbbbbbbbbb| bbbbbbbbbbbbbbbbbbbb| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| {6:~ }| ]], }) end) end) -- TODO(lewis6991): All deprecated fields removed. Re-add when we have another deprecated field -- describe('configuration', function() -- it('handled deprecated fields', function() -- pending() -- -- config.current_line_blame_delay = 100 -- -- setup_gitsigns(config) -- -- eq(100, exec_lua([[return package.loaded['gitsigns.config'].config.current_line_blame_opts.delay]])) -- end) -- end) describe('on_attach()', function() it('can prevent attaching to a buffer', function() setup_test_repo({ no_add = true }) setup_gitsigns(config, true) edit(test_file) match_debug_messages({ 'attach.attach(1): Attaching (trigger=BufReadPost)', np(revparse_pat), np('system.system: git .* rev%-parse %-%-short HEAD'), np('system.system: git .* config user.name'), np( 'system.system: git .* %-%-git%-dir .* %-%-stage %-%-others %-%-exclude%-standard %-%-eol.*' ), n('attach.attach(1): User on_attach() returned false'), }) end) end) describe('change_base()', function() it('works', function() setup_test_repo() edit(test_file) feed('oEDIT') command('write') git('add', test_file) git('commit', '-m', 'commit on main') -- Don't setup gitsigns until the repo has two commits setup_gitsigns(config) check({ status = { head = 'main', added = 0, changed = 0, removed = 0 }, signs = {}, }) command('Gitsigns change_base ~') check({ status = { head = 'main', added = 1, changed = 0, removed = 0 }, signs = { added = 1 }, }) end) end) local function testsuite(internal_diff) return function() before_each(function() config.diff_opts = { internal = internal_diff, } setup_test_repo() end) it('apply basic signs', function() setup_gitsigns(config) edit(test_file) command('set signcolumn=yes') feed('dd') -- Top delete feed('j') feed('o') -- Add feed('2j') feed('x') -- Change feed('3j') feed('dd') -- Delete feed('j') feed('ddx') -- Change delete check({ status = { head = 'main', added = 1, changed = 2, removed = 3 }, signs = { topdelete = 1, changedelete = 1, added = 1, delete = 1, changed = 1 }, }) end) it('can enable numhl', function() config.numhl = true setup_gitsigns(config) edit(test_file) command('set signcolumn=no') command('set number') feed('dd') -- Top delete feed('j') feed('o') -- Add feed('2j') feed('x') -- Change feed('3j') feed('dd') -- Delete feed('j') feed('ddx') -- Change delete -- screen:snapshot_util() screen:expect({ grid = [[ {4: 1 }is | {5: 2 }a | {3: 3 } | {5: 4 }file | {2: 5 }sed | {5: 6 }for | {4: 7 }testing | {5: 8 }The | {2: 9 }^oesn't | {5: 10 }matter, | {5: 11 }it | {5: 12 }just | {5: 13 }needs | {5: 14 }to | {5: 15 }be | {5: 16 }static. | {6:~ }| ]], }) end) it('attaches to newly created files', function() setup_gitsigns(config) edit(newfile) match_debug_messages({ 'attach.attach(1): Attaching (trigger=BufNewFile)', np(revparse_pat), np('system.system: git .* config user.name'), np('system.system: git .* ls%-files .*'), n('attach.attach(1): Cannot resolve file in repo'), }) command('write') local messages = { 'attach.attach(1): Attaching (trigger=BufWritePost)', np(revparse_pat), np('system.system: git .* ls%-files .*'), np('attach%.attach%(1%): Watching git dir .*'), } if not internal_diff then table.insert(messages, np('system.system: git .* diff .* /.* /.*')) end match_debug_messages(messages) check({ status = { head = 'main', added = 1, changed = 0, removed = 0 }, signs = { untracked = 1 }, }) end) it('can add untracked files to the index', function() setup_gitsigns(config) edit(newfile) feed('iline') check({ status = { head = 'main' } }) command('write') check({ status = { head = 'main', added = 1, changed = 0, removed = 0 }, signs = { untracked = 1 }, }) feed('mhs') -- Stage the file (add file to index) check({ status = { head = 'main', added = 0, changed = 0, removed = 0 }, signs = {}, }) end) it('tracks files in new repos', function() setup_gitsigns(config) system({ 'touch', newfile }) edit(newfile) feed('iEDIT') command('write') check({ status = { head = 'main', added = 1, changed = 0, removed = 0 }, signs = { untracked = 1 }, }) git('add', newfile) check({ status = { head = 'main', added = 0, changed = 0, removed = 0 }, signs = {}, }) git('reset') check({ status = { head = 'main', added = 1, changed = 0, removed = 0 }, signs = { untracked = 1 }, }) end) it('can detach from buffers', function() setup_gitsigns(config) edit(test_file) command('set signcolumn=yes') feed('dd') -- Top delete feed('j') feed('o') -- Add feed('2j') feed('x') -- Change feed('3j') feed('dd') -- Delete feed('j') feed('ddx') -- Change delete check({ status = { head = 'main', added = 1, changed = 2, removed = 3 }, signs = { topdelete = 1, added = 1, changed = 1, delete = 1, changedelete = 1 }, }) command('Gitsigns detach') check({ status = {}, signs = {} }) end) it('can stages file with merge conflicts', function() setup_gitsigns(config) command('set signcolumn=yes') -- Edit a file and commit it on main branch edit(test_file) check({ status = { head = 'main', added = 0, changed = 0, removed = 0 } }) feed('iedit') check({ status = { head = 'main', added = 0, changed = 1, removed = 0 } }) command('write') command('bwipe') git('add', test_file) git('commit', '-m', 'commit on main') -- Create a branch, remove last commit, edit file again git('checkout', '-B', 'abranch') git('reset', '--hard', 'HEAD~1') edit(test_file) check({ status = { head = 'abranch', added = 0, changed = 0, removed = 0 } }) feed('idiff') check({ status = { head = 'abranch', added = 0, changed = 1, removed = 0 } }) command('write') command('bwipe') git('add', test_file) git('commit', '-m', 'commit on branch') git('rebase', 'main') -- test_file should have a conflict edit(test_file) check({ status = { head = 'HEAD(rebasing)', added = 4, changed = 1, removed = 0 }, signs = { changed = 1, added = 4 }, }) -- Minor delay to avoid the test being flaky helpers.sleep(50) exec_lua(function() require('gitsigns.actions').stage_hunk() end) check({ status = { head = 'HEAD(rebasing)', added = 0, changed = 0, removed = 0 }, signs = {}, }) end) it('handle files with spaces', function() setup_gitsigns(config) command('set signcolumn=yes') local spacefile = scratch .. '/a b c d' write_to_file(spacefile, { 'spaces', 'in', 'file' }) edit(spacefile) check({ status = { head = 'main', added = 3, removed = 0, changed = 0 }, signs = { untracked = 3 }, }) git('add', spacefile) edit(spacefile) check({ status = { head = 'main', added = 0, removed = 0, changed = 0 }, signs = {}, }) end) end end -- Run regular config describe('diff-ext', testsuite(false)) -- Run with: -- - internal diff (ffi) -- - decoration provider describe('diff-int', testsuite(true)) it('can handle vimgrep', function() setup_test_repo() write_to_file(scratch .. '/t1.txt', { 'hello ben' }) write_to_file(scratch .. '/t2.txt', { 'hello ben' }) write_to_file(scratch .. '/t3.txt', { 'hello lewis' }) setup_gitsigns(config) helpers.exc_exec('vimgrep ben ' .. scratch .. '/*') if fn.has('nvim-0.12') > 0 then screen:expect({ messages = { { kind = '', content = { { scratch .. '/dummy.txt' } }, }, { kind = 'quickfix', content = { { '(1 of 2): hello ben' } }, }, }, }) else screen:expect({ messages = { { kind = 'quickfix', content = { { '(1 of 2): hello ben' } }, }, }, }) end match_debug_messages({ 'gitsigns.attach_autocmd(2): Attaching is disabled', n('gitsigns.attach_autocmd(3): Attaching is disabled'), n('gitsigns.attach_autocmd(4): Attaching is disabled'), n('gitsigns.attach_autocmd(5): Attaching is disabled'), }) end) it('show short SHA when detached head', function() setup_test_repo() git('checkout', '--detach') -- Disable debug_mode so the sha is calculated config.debug_mode = false setup_gitsigns(config) edit(test_file) -- SHA is not deterministic so just check it can be cast as a hex value expectf(function() helpers.neq(nil, tonumber('0x' .. get_buf_var(0, 'gitsigns_head'))) end) end) it('handles a quick undo', function() setup_test_repo() setup_gitsigns(config) edit(test_file) -- This test isn't deterministic so run it a few times for _ = 1, 3 do feed('x') check({ signs = { changed = 1 } }) feed('u') check({ signs = {} }) end end) it('handles filenames with unicode characters', function() screen:try_resize(20, 2) setup_test_repo() setup_gitsigns(config) local uni_filename = scratch .. '/föobær' write_to_file(uni_filename, { 'Lorem ipsum' }) git('add', uni_filename) git('commit', '-m', 'another commit') edit(uni_filename) screen:expect({ grid = [[ ^Lorem ipsum | {6:~ }| ]], }) feed('x') if fn.has('nvim-0.11') > 0 then screen:expect({ grid = [[ {12:~ }^orem ipsum | {6:~ }| ]], }) else screen:expect({ grid = [[ {2:~ }^orem ipsum | {6:~ }| ]], }) end end) it('handle #521', function() screen:detach() screen:attach() screen:try_resize(20, 4) setup_test_repo() setup_gitsigns(config) edit(test_file) feed('dd') local function check_screen(unchanged) if fn.has('nvim-0.11') > 0 then -- TODO(lewis6991): ??? screen:expect({ grid = [[ {11:^ }^is | {1: }a | {1: }file | | ]], unchanged = unchanged, }) else screen:expect({ grid = [[ {4:^ }^is | {1: }a | {1: }file | {1: }used | ]], unchanged = unchanged, }) end end check_screen() -- Write over the text with itself. This will remove all the signs but the -- calculated hunks won't change. exec_lua(function() local text = vim.api.nvim_buf_get_lines(0, 0, -1, false) vim.api.nvim_buf_set_lines(0, 0, -1, true, text) end) check_screen(true) end) it('shows "No newline at end of file" in preview popup', function() setup_test_repo({ test_file_text = { 'a' } }) setup_gitsigns(config) screen:try_resize(30, 10) edit(test_file) wait_for_attach() -- Remove newline at end of file (`printf a >a`) local file_path = scratch .. '/dummy.txt' local f = assert(io.open(file_path, 'wb')) f:write('a') -- Write without trailing newline f:close() command('checktime') helpers.sleep(50) feed('mhp') screen:expect({ any = [[No newline at end of file]] }) end) end) describe('gitsigns attach', function() local config --- @type table before_each(function() clear() config = vim.deepcopy(test_config) command('cd ' .. system({ 'dirname', os.tmpname() })) end) after_each(function() cleanup() end) it('handle #888', function() setup_test_repo() local path1 = scratch .. '/cargo.toml' local subdir = scratch .. '/subdir' local path2 = subdir .. '/cargo.toml' write_to_file(path1, { 'some text' }) git('add', path1) git('commit', '-m', 'add cargo') -- move file and stage move system({ 'mkdir', subdir }) system({ 'mv', path1, path2 }) git('add', path1, path2) config.base = 'HEAD' setup_gitsigns(config) edit(path1) command('write') helpers.sleep(100) end) it('does not error on non-file fugitive buffers (#1277)', function() -- Note this test is testing the attach logic before the git_obj -- is created. setup_gitsigns(config) -- Since this bufname isn't a valid path, Nvim will not trigger the -- BufNewFile autocmd, therefore we need to manually attach. edit(('fugitive://%s/.git//'):format(scratch)) command('Gitsigns attach') match_debug_messages({ 'attach.attach(1): Empty git obj', }) end) it('can run diffthis/show when cwd is a subdir of a git repo (#1277)', function() helpers.git_init_scratch() local file = scratch .. '/sub/test' system({ 'mkdir', vim.fs.dirname(file) }) write_to_file(file, { 'hello' }) git('add', file) git('commit', '-m', 'commit 1') command('cd ' .. vim.fs.dirname(file)) setup_gitsigns(config) edit('test') wait_for_attach() command('Gitsigns show') wait_for_attach() eq('gitsigns://' .. scratch .. '/.git//:0:sub/test', api.nvim_buf_get_name(0)) local gfile, toplevel, gitdir, abbrev_head = exec_lua(function() local git_obj = assert(require('gitsigns.cache').cache[1]).git_obj return git_obj.file, git_obj.repo.toplevel, git_obj.repo.gitdir, git_obj.repo.abbrev_head end) eq(file, gfile) eq(scratch, toplevel) eq(scratch .. '/.git', gitdir) eq('main', abbrev_head) end) it('does not error after git system callbacks (#1425)', function() setup_test_repo() setup_gitsigns(config) edit(test_file) wait_for_attach() local ok = exec_lua(function() local async = require('gitsigns.async') local git_cmd = require('gitsigns.git.cmd') return async .run(function() -- `git_cmd()` ultimately uses `vim.system`, whose on_exit callback runs -- in fast event context. Ensure we yield to the scheduler after the -- command completes so Neovim API calls here don't raise E5560. git_cmd({ '--version' }, { text = true }) local b = vim.api.nvim_create_buf(false, true) vim.bo[b].buftype = 'nofile' vim.api.nvim_buf_delete(b, { force = true }) return true end) :wait() end) eq(true, ok) end) it('does not error when attaching to files out of tree (#1297)', function() setup_test_repo() setup_gitsigns(config) exec_lua(function(scratch0) vim.env.GIT_DIR = scratch0 .. '/.git' vim.env.GIT_WORK_TREE = scratch0 end, scratch) edit(fn.tempname()) match_debug_messages({ p("get_info: '.*' is outside worktree '.*'"), 'attach.attach(1): Empty git obj', }) end) end) neovim-gitsigns-2.0.0/test/gs_helpers.lua000066400000000000000000000206021513053142700204510ustar00rootroot00000000000000local helpers = require('nvim-test.helpers') local timeout = 2000 local M = helpers local exec_lua = helpers.exec_lua local matches = helpers.matches local eq = helpers.eq local buf_get_var = helpers.api.nvim_buf_get_var local system = helpers.fn.system M.scratch = os.getenv('PJ_ROOT') .. '/scratch' M.test_file = M.scratch .. '/dummy.txt' M.newfile = M.scratch .. '/newfile.txt' M.test_config = { debug_mode = true, _test_mode = true, signs = { add = { text = '+' }, delete = { text = '_' }, change = { text = '~' }, topdelete = { text = '^' }, changedelete = { text = '%' }, untracked = { text = '#' }, }, on_attach = { { 'n', 'mhs', 'lua require"gitsigns".stage_hunk()' }, { 'n', 'mhu', 'lua require"gitsigns".undo_stage_hunk()' }, { 'n', 'mhr', 'lua require"gitsigns".reset_hunk()' }, { 'n', 'mhp', 'lua require"gitsigns".preview_hunk()' }, { 'n', 'mhS', 'lua require"gitsigns".stage_buffer()' }, { 'n', 'mhU', 'lua require"gitsigns".reset_buffer_index()' }, }, attach_to_untracked = true, update_debounce = 5, } local test_file_text = { 'This', 'is', 'a', 'file', 'used', 'for', 'testing', 'gitsigns.', 'The', 'content', "doesn't", 'matter,', 'it', 'just', 'needs', 'to', 'be', 'static.', } --- Run a git command --- @param ... string function M.git(...) system({ 'git', '-C', M.scratch, ... }) end function M.cleanup() system({ 'rm', '-rf', M.scratch }) end function M.git_init_scratch() M.cleanup() system({ 'mkdir', M.scratch }) M.git('init', '-b', 'main') -- Always force color to test settings don't interfere with gitsigns systems -- commands (addresses #23) M.git('config', 'color.branch', 'always') M.git('config', 'color.ui', 'always') M.git('config', 'color.diff', 'always') M.git('config', 'color.interactive', 'always') M.git('config', 'color.status', 'always') M.git('config', 'color.grep', 'always') M.git('config', 'color.pager', 'true') M.git('config', 'color.decorate', 'always') M.git('config', 'color.showbranch', 'always') M.git('config', 'merge.conflictStyle', 'merge') M.git('config', 'user.email', 'tester@com.com') M.git('config', 'user.name', 'tester') M.git('config', 'init.defaultBranch', 'main') end --- Setup a basic git repository in directory `helpers.scratch` with a single file --- `helpers.test_file` committed. --- @param opts? {test_file_text?: string[], no_add?: boolean} function M.setup_test_repo(opts) local text = opts and opts.test_file_text or test_file_text M.git_init_scratch() system({ 'touch', M.test_file }) M.write_to_file(M.test_file, text) if not (opts and opts.no_add) then M.git('add', M.test_file) M.git('commit', '-m', 'init commit') end end --- @param cond fun() --- @param interval? integer function M.expectf(cond, interval) local duration = 0 interval = interval or 1 while duration < timeout do local ok, ret = pcall(cond) if ok and (ret == nil or ret == true) then return end duration = duration + interval helpers.sleep(interval) interval = interval * 2 end cond() end --- @param path string function M.edit(path) helpers.api.nvim_command('edit ' .. path) end --- @param path string --- @param text string[] function M.write_to_file(path, text) local f = assert(io.open(path, 'wb')) for _, l in ipairs(text) do f:write(l) f:write('\n') end f:close() end --- @param line string --- @param spec string|{next:boolean, pattern:boolean, text:string} --- @return boolean local function match_spec_elem(line, spec) if spec.pattern then if line:match(spec.text) then return true end elseif spec.next then -- local matcher = spec.pattern and matches or eq -- matcher(spec.text, line) if spec.pattern then matches(spec.text, line) else eq(spec.text, line) end return true end return spec == line end --- Match lines in spec. Not all lines have to match --- @param lines string[] --- @param spec table function M.match_lines(lines, spec) local i = 1 for _, line in ipairs(lines) do local s = spec[i] if line ~= '' and s and match_spec_elem(line, s) then i = i + 1 end end if i < #spec + 1 then local lines_msg = table.concat( --- @param v any --- @return string vim.tbl_map(function(v) return string.format(' - %s', v) end, lines), '\n' ) error(('Did not match pattern %s with:\n%s'):format(vim.inspect(spec[i]), lines_msg)) end end function M.p(str) return { text = str, pattern = true } end function M.n(str) return { text = str, next = true } end function M.np(str) return { text = str, pattern = true, next = true } end --- @return string[] function M.debug_messages() --- @type string[] local r = exec_lua("return require'gitsigns.debug.log'.get(true)") for i, line in ipairs(r) do -- Remove leading timestamp r[i] = line:gsub('^[0-9.]+ D ', '') end return r end --- Like match_debug_messages but elements in spec are unordered --- @param spec table function M.match_dag(spec) M.expectf(function() local messages = M.debug_messages() for _, s in ipairs(spec) do M.match_lines(messages, { s }) end end) end --- @param spec table function M.match_debug_messages(spec) M.expectf(function() M.match_lines(M.debug_messages(), spec) end) end function M.setup_path() exec_lua(function(path) package.path = path end, package.path) end --- @param config? table --- @param on_attach? boolean function M.setup_gitsigns(config, on_attach) M.setup_path() exec_lua(function(config0, on_attach0) if config0 and config0.on_attach then local maps = config0.on_attach --[[@as [string,string,string][] ]] config0.on_attach = function(bufnr) for _, map in ipairs(maps) do vim.keymap.set(map[1], map[2], map[3], { buffer = bufnr }) end end end if on_attach0 then config0.on_attach = function() return false end end require('gitsigns').setup(config0) vim.o.diffopt = 'internal,filler,closeoff' end, config, on_attach) end --- @param status table --- @param bufnr integer local function check_status(status, bufnr) if next(status) == nil then eq(false, pcall(buf_get_var, bufnr, 'gitsigns_head'), 'b:gitsigns_head is unexpectedly set') eq( false, pcall(buf_get_var, bufnr, 'gitsigns_status_dict'), 'b:gitsigns_status_dict is unexpectedly set' ) return end eq(status.head, buf_get_var(bufnr, 'gitsigns_head'), 'b:gitsigns_head does not match') --- @type table local bstatus = buf_get_var(bufnr, 'gitsigns_status_dict') for _, i in ipairs({ 'added', 'changed', 'removed', 'head' }) do eq(status[i], bstatus[i], string.format("status['%s'] did not match gitsigns_status_dict", i)) end -- Catch any extra keys for i, v in pairs(status) do eq(v, bstatus[i], string.format("status['%s'] did not match gitsigns_status_dict", i)) end end --- @param signs table --- @param bufnr integer local function check_signs(signs, bufnr) local buf_signs = {} --- @type string[] local buf_marks = helpers.api.nvim_buf_get_extmarks(bufnr, -1, 0, -1, { details = true }) for _, s in ipairs(buf_marks) do buf_signs[#buf_signs + 1] = assert(s[4]).sign_hl_group end --- @type table local act = {} for _, name in ipairs(buf_signs) do for t, hl in pairs({ added = 'GitSignsAdd', changed = 'GitSignsChange', delete = 'GitSignsDelete', changedelete = 'GitSignsChangedelete', topdelete = 'GitSignsTopdelete', untracked = 'GitSignsUntracked', }) do if name == hl then act[t] = (act[t] or 0) + 1 end end end eq(signs, act, vim.inspect(buf_signs)) end --- @param attrs {signs?:table,status?:table} --- @param bufnr? integer function M.check(attrs, bufnr) bufnr = bufnr or 0 if not attrs then return end M.expectf(function() if attrs.status then check_status(attrs.status, bufnr) end if attrs.signs then check_signs(attrs.signs, bufnr) end end) end return M neovim-gitsigns-2.0.0/test/highlights_spec.lua000066400000000000000000000061261513053142700214670ustar00rootroot00000000000000local Screen = require('nvim-test.screen') local helpers = require('test.gs_helpers') local clear = helpers.clear local command = helpers.api.nvim_command local cleanup = helpers.cleanup local test_config = helpers.test_config local expectf = helpers.expectf local match_dag = helpers.match_dag local p = helpers.p local setup_gitsigns = helpers.setup_gitsigns local eq = helpers.eq helpers.env() describe('highlights', function() local screen --- @type test.screen local config --- @type Gitsigns.Config before_each(function() clear() screen = Screen.new(20, 17) screen:attach() local default_attrs = { [1] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.WebGray }, [2] = { foreground = Screen.colors.NvimDarkCyan }, [3] = { background = Screen.colors.LightBlue }, [4] = { foreground = Screen.colors.NvimDarkRed }, [5] = { foreground = Screen.colors.Brown }, [6] = { foreground = Screen.colors.Blue1, bold = true }, [7] = { bold = true }, [8] = { foreground = Screen.colors.White, background = Screen.colors.Red }, [9] = { foreground = Screen.colors.SeaGreen, bold = true }, } -- Use the classic vim colorscheme, not the new defaults in nvim >= 0.10 if helpers.fn.has('nvim-0.10') > 0 then command('colorscheme vim') else default_attrs[2] = { background = Screen.colors.LightMagenta } default_attrs[4] = { background = Screen.colors.LightCyan1, bold = true, foreground = Screen.colors.Blue1 } end screen:set_default_attr_ids(default_attrs) config = vim.deepcopy(test_config) end) after_each(function() cleanup() screen:detach() end) it('get set up correctly', function() command('set termguicolors') config.numhl = true config.linehl = true config._test_mode = true setup_gitsigns(config) local nvim10 = helpers.fn.has('nvim-0.10') > 0 expectf(function() match_dag({ p('Deriving GitSignsAdd from ' .. (nvim10 and 'Added' or 'DiffAdd')), p('Deriving GitSignsAddLn from DiffAdd'), p('Deriving GitSignsAddNr from GitSignsAdd'), p('Deriving GitSignsChangeLn from DiffChange'), p('Deriving GitSignsChangeNr from GitSignsChange'), p('Deriving GitSignsDelete from ' .. (nvim10 and 'Removed' or 'DiffDelete')), p('Deriving GitSignsDeleteNr from GitSignsDelete'), }) end) end) it('update when colorscheme changes', function() command('set termguicolors') config.linehl = true setup_gitsigns(config) end) it('get_temp_hl handles equal min/max', function() helpers.setup_path() local res = helpers.exec_lua(function() vim.api.nvim_set_hl(0, 'Normal', { bg = 0x000000 }) package.loaded['gitsigns.highlight'] = nil local hl = require('gitsigns.highlight') local name = hl.get_temp_hl(0, 0, 0, 0.5, true) local info = vim.api.nvim_get_hl(0, { name = name, link = false }) return { name = name, fg = info.fg } end) assert(res.name:match('^GitSignsColorTemp%.fg%.%d+$') ~= nil) eq(0x00007F, res.fg) end) end) neovim-gitsigns-2.0.0/test/hunk_spec.lua000066400000000000000000000052701513053142700203010ustar00rootroot00000000000000local helpers = require('test.gs_helpers') local exec_lua = helpers.exec_lua local eq = helpers.eq helpers.env() --- @param hunks [string,integer,integer,integer,integer][] --- @return [string,integer,integer?][] local function calc_signs(hunks) local hunks1 = {} --- @type Gitsigns.Hunk.Hunk[] for i, hunk in ipairs(hunks) do hunks1[i] = { added = { count = hunk[4], start = hunk[5] }, removed = { count = hunk[2], start = hunk[3] }, type = hunk[1], } end local signs = exec_lua( --- @param hunks0 Gitsigns.Hunk.Hunk[] function(hunks0) local Hunks = require('gitsigns.hunks') local signs0 = {} --- @type Gitsigns.Sign[] for i, hunk in ipairs(hunks0) do local prev_hunk, next_hunk = hunks0[i - 1], hunks0[i + 1] vim.list_extend(signs0, Hunks.calc_signs(prev_hunk, hunk, next_hunk)) end return signs0 end, hunks1 ) local r = {} --- @type [string,integer,integer?][] for i, s in ipairs(signs) do r[i] = { s.type, s.lnum, s.count } end return r end describe('hunksigns', function() before_each(function() exec_lua(function(path) package.path = path require('gitsigns').setup({ _new_sign_calc = true }) end, package.path) end) it('calculate topdelete signs', function() eq({ { 'topdelete', 1, 1 } }, calc_signs({ { 'delete', 1, 1, 0, 0 } })) end) it('calculate topdelete signs with changedelete', function() eq( { { 'changedelete', 1, 1 } }, calc_signs({ { 'delete', 1, 1, 0, 0 }, { 'change', 1, 2, 1, 1 }, }) ) end) it('delete, change, topdelete', function() eq( { { 'delete', 1, 1 }, { 'change', 2, 1 }, { 'topdelete', 3, 1 }, }, calc_signs({ { 'delete', 1, 2, 0, 1 }, { 'change', 1, 3, 1, 2 }, { 'delete', 1, 4, 0, 2 }, }) ) end) it('delete, change, change, topdelete', function() eq( { { 'delete', 1, 1 }, { 'change', 2, 2 }, { 'change', 3 }, { 'topdelete', 4, 1 }, }, calc_signs({ { 'delete', 1, 2, 0, 1 }, { 'change', 2, 3, 2, 2 }, { 'delete', 1, 5, 0, 3 }, }) ) end) it('delete, change, changedelete', function() local r = calc_signs({ { 'delete', 1, 2, 0, 1 }, { 'change', 1, 3, 1, 2 }, { 'delete', 1, 4, 0, 2 }, { 'change', 1, 5, 1, 3 }, }) -- TODO(lewis6991): not perfect. Better signs would be -- { 'delete', 1, 1 }, -- { 'changedelete', 2, 1 }, -- { 'change', 3, 1 }, eq({ { 'delete', 1, 1 }, { 'change', 2, 1 }, { 'delete', 2, 1 }, { 'change', 3, 1 }, }, r) end) end) neovim-gitsigns-2.0.0/test/word_diff_spec.lua000066400000000000000000000106461513053142700213020ustar00rootroot00000000000000local helpers = require('test.gs_helpers') local exec_lua = helpers.exec_lua local eq = helpers.eq local setup_test_repo = helpers.setup_test_repo local setup_gitsigns = helpers.setup_gitsigns local test_config = helpers.test_config local edit = helpers.edit local test_file = helpers.test_file local clear = helpers.clear local cleanup = helpers.cleanup local expectf = helpers.expectf helpers.env() describe('word diff', function() before_each(function() clear() setup_gitsigns() end) it('treats whitespace padding as a single region', function() local rems, adds = exec_lua(function() local diff = require('gitsigns.diff_int') local removed = { 'foo = 1', 'bar = 2' } local added = { 'foo = 1', 'bar = 2' } return diff.run_word_diff(removed, added) end) eq({ { 1, 'add', 5, 5 }, { 2, 'add', 5, 5 }, }, rems) eq({ { 1, 'add', 5, 9 }, { 2, 'add', 5, 9 }, }, adds) end) it('anchors indentation changes to the start of the line', function() local rems, adds = exec_lua(function() local diff = require('gitsigns.diff_int') local removed = { ' foo = 1' } local added = { ' foo = 1' } return diff.run_word_diff(removed, added) end) eq({ { 1, 'add', 3, 3 } }, rems) eq({ { 1, 'add', 3, 9 } }, adds) end) it('highlights only changed characters inside a word', function() local rems, adds = exec_lua(function() local diff = require('gitsigns.diff_int') local removed = { 'local foo = 1' } local added = { 'local foz = 1' } return diff.run_word_diff(removed, added) end) eq({ { 1, 'change', 9, 10 } }, rems) eq({ { 1, 'change', 9, 10 } }, adds) end) end) describe('inline preview', function() before_each(function() clear() end) after_each(function() cleanup() end) it('word diff aligns highlights after multibyte characters', function() if helpers.fn.has('nvim-0.11') == 0 then pending('requires Neovim 0.11+') end setup_test_repo({ test_file_text = { 'éx' } }) local config = vim.deepcopy(test_config) config.word_diff = true setup_gitsigns(config) edit(test_file) exec_lua(function() vim.api.nvim_buf_set_lines(0, 0, 1, false, { 'éy' }) end) exec_lua(function() require('gitsigns').refresh() end) expectf(function() local hunks = exec_lua("return require('gitsigns').get_hunks()") eq(1, #hunks) end) exec_lua(function() require('gitsigns').preview_hunk_inline() end) local start_col, end_col = exec_lua(function() local ns = vim.api.nvim_get_namespaces().gitsigns_preview_inline local marks = vim.api.nvim_buf_get_extmarks(0, ns, 0, -1, { details = true }) local start_col0, end_col0 --- @type integer?, integer? for _, mark in ipairs(marks) do local details = mark[4] if details and details.hl_group == 'GitSignsChangeInline' then start_col0 = mark[3] end_col0 = details.end_col break end end return start_col0, end_col0 end) local expected_start, expected_end = exec_lua(function() local line = assert(vim.api.nvim_buf_get_lines(0, 0, 1, false)[1]) -- Use UTF-32 indexes so we can count characters. return vim.str_byteindex(line, 'utf-32', 1), vim.str_byteindex(line, 'utf-32', 2) end) eq(expected_start, start_col) eq(expected_end, end_col) end) it('deleted highlights each removed line exactly once', function() setup_test_repo({ test_file_text = { 'alpha', 'bravo', 'charlie', }, }) setup_gitsigns(test_config) edit(test_file) helpers.api.nvim_buf_set_lines(0, 0, 2, false, {}) expectf(function() local hunk = exec_lua(function() return require('gitsigns').get_hunks()[1] end) assert(hunk and hunk.removed.count == 2) end) local deleted_marks = exec_lua(function() local async = require('gitsigns.async') local winid = async .run(function() return require('gitsigns.actions.preview').preview_hunk_inline() end) :wait() assert(winid, 'preview window not found') local buf = vim.api.nvim_win_get_buf(winid) local ns = vim.api.nvim_get_namespaces().gitsigns_preview_inline return #vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true }) end) eq(2, deleted_marks) end) end)