pax_global_header00006660000000000000000000000064147655727040014533gustar00rootroot0000000000000052 comment=7010000889bfb6c26065e0b0f7f1e6aa9163edd9 lewis6991-gitsigns.nvim-7010000/000077500000000000000000000000001476557270400161555ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/.editorconfig000066400000000000000000000004261476557270400206340ustar00rootroot00000000000000root = 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 lewis6991-gitsigns.nvim-7010000/.gitattributes000066400000000000000000000000351476557270400210460ustar00rootroot00000000000000doc/* linguist-documentation lewis6991-gitsigns.nvim-7010000/.github/000077500000000000000000000000001476557270400175155ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/.github/FUNDING.yml000066400000000000000000000000241476557270400213260ustar00rootroot00000000000000github: [lewis6991] lewis6991-gitsigns.nvim-7010000/.github/ISSUE_TEMPLATE/000077500000000000000000000000001476557270400217005ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/.github/ISSUE_TEMPLATE/bug_report.yaml000066400000000000000000000061141476557270400247360ustar00rootroot00000000000000name: 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 `minimal.lua`. If necessary, add plugins and config options from your `init.lua` at the indicated lines. Please **DO NOT** include a package manager (such as `lazy.nvim`) in this. 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. value: | 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 minimal.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`. lewis6991-gitsigns.nvim-7010000/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000003071476557270400236700ustar00rootroot00000000000000blank_issues_enabled: true contact_links: - name: Question url: https://gitter.im/gitsigns-nvim/community about: Usage questions and support requests are answered in the Gitsigns Gitter lewis6991-gitsigns.nvim-7010000/.github/workflows/000077500000000000000000000000001476557270400215525ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/.github/workflows/ci.yml000066400000000000000000000037751476557270400227040ustar00rootroot00000000000000name: 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.9.5' - 'v0.10.0' - 'nightly' env: NVIM_TEST_VERSION: ${{ matrix.neovim_branch }} steps: - name: Checkout uses: actions/checkout@v4 - uses: leafo/gh-actions-lua@v9 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 stylua: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Download stylua run: make stylua - name: Lint run: make stylua-check luals: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - uses: leafo/gh-actions-lua@v9 with: luaVersion: "5.1.5" - uses: leafo/gh-actions-luarocks@v4 - name: Download nvim-test run: make nvim-test - name: Download LuaLS run: make luals - name: Lint run: make luals-check doc: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - uses: leafo/gh-actions-lua@v10 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 lewis6991-gitsigns.nvim-7010000/.github/workflows/release-please.yml000066400000000000000000000047131476557270400251710ustar00rootroot00000000000000on: 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@v5 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 gen_help - 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 lewis6991-gitsigns.nvim-7010000/.github/workflows/response.yml000066400000000000000000000040131476557270400241310ustar00rootroot00000000000000name: 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", }); } } lewis6991-gitsigns.nvim-7010000/.gitignore000066400000000000000000000000171476557270400201430ustar00rootroot00000000000000doc/tags deps lewis6991-gitsigns.nvim-7010000/.luarc.json000066400000000000000000000015211476557270400202330ustar00rootroot00000000000000{ "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", "runtime": { "version": "LuaJIT" }, "workspace": { "library": [ "lua", "$VIMRUNTIME", "nvim-test", "${3rd}/busted/library", "${3rd}/luassert/library", "${3rd}/luv/library" ], "checkThirdParty": false }, "diagnostics": { "libraryFiles": "Disable", "groupFileStatus": { "strict": "Opened", "strong": "Opened", "ambiguity" : "Opened", "duplicate" : "Opened", "global" : "Opened", "luadoc" : "Opened", "redefined" : "Opened", "type-check" : "Opened", "unbalanced" : "Opened", "unused" : "Opened" }, "groupSeverity": { "strong": "Warning", "strict": "Warning" }, "unusedLocalExclude": [ "_*" ] } } lewis6991-gitsigns.nvim-7010000/.release-please-manifest.json000066400000000000000000000000251476557270400236160ustar00rootroot00000000000000{ ".": "1.0.2" } lewis6991-gitsigns.nvim-7010000/.stylua.toml000066400000000000000000000002161476557270400204500ustar00rootroot00000000000000column_width = 100 line_endings = "Unix" indent_type = "Spaces" indent_width = 2 quote_style = "AutoPreferSingle" call_parentheses = "Always" lewis6991-gitsigns.nvim-7010000/CHANGELOG.md000066400000000000000000000645351476557270400200030ustar00rootroot00000000000000# Changelog ## [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)) lewis6991-gitsigns.nvim-7010000/CONTRIBUTING.md000066400000000000000000000016461476557270400204150ustar00rootroot00000000000000**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 documentation is generated with the lua script `gen_help.lua` which has been developed just enough to handle the current configuration schema so from time to time this script might need small improvements to handle new features but for the most part it works. The documentation can be updated with: ```bash make gen_help ``` ## Testsuite The testsuite uses the same framework as Neovims funcitonaltest 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 ``` lewis6991-gitsigns.nvim-7010000/LICENSE000066400000000000000000000020561476557270400171650ustar00rootroot00000000000000MIT 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. lewis6991-gitsigns.nvim-7010000/Makefile000066400000000000000000000051471476557270400176240ustar00rootroot00000000000000 export PJ_ROOT=$(PWD) FILTER ?= .* export NVIM_RUNNER_VERSION := v0.10.3 export NVIM_TEST_VERSION ?= v0.10.3 ifeq ($(shell uname -s),Darwin) UNAME ?= MACOS else UNAME ?= LINUX endif .DEFAULT_GOAL := build NVIM_TEST := deps/nvim-test .PHONY: nvim-test nvim-test: $(NVIM_TEST) $(NVIM_TEST): git clone --depth 1 --branch v1.1.0 https://github.com/lewis6991/nvim-test $@ $@/bin/nvim-test --init .PHONY: test test: $(NVIM_TEST) NVIM_TEST_VERSION=$(NVIM_TEST_VERSION) \ $(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-095 test-095: $(MAKE) $(MAKEFLAGS) test NVIM_TEST_VERSION=v0.9.5 .PHONY: test-010 test-010: $(MAKE) $(MAKEFLAGS) test NVIM_TEST_VERSION=v0.10.3 .PHONY: test-nightly test-nightly: $(MAKE) $(MAKEFLAGS) test NVIM_TEST_VERSION=nightly export XDG_DATA_HOME ?= $(HOME)/.data NVIM := $(XDG_DATA_HOME)/nvim-test/nvim-runner-$(NVIM_RUNNER_VERSION)/bin/nvim .PHONY: gen_help gen_help: $(NVIM_TEST) $(NVIM) -l ./gen_help.lua @echo Updated help 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: stylua-check stylua-check: $(STYLUA) $(STYLUA) --check $(LUA_FILES) .PHONY: stylua-run stylua-run: $(STYLUA) $(STYLUA) $(LUA_FILES) .PHONY: build build: gen_help stylua-run .PHONY: doc-check doc-check: gen_help git diff --exit-code -- doc ifeq ($(shell uname -m),arm64) LUALS_ARCH ?= arm64 else LUALS_ARCH ?= x64 endif LUALS_VERSION := 3.13.8 LUALS := deps/lua-language-server-$(LUALS_VERSION)-$(shell uname -s)-$(LUALS_ARCH) LUALS_TARBALL := $(LUALS).tar.gz LUALS_URL := https://github.com/LuaLS/lua-language-server/releases/download/$(LUALS_VERSION)/$(notdir $(LUALS_TARBALL)) .PHONY: luals luals: $(LUALS) $(LUALS): wget --directory-prefix=$(dir $@) $(LUALS_URL) mkdir -p $@ tar -xf $(LUALS_TARBALL) -C $@ rm -rf $(LUALS_TARBALL) .PHONY: luals-check luals-check: $(LUALS) $(NVIM_TEST) VIMRUNTIME=$(XDG_DATA_HOME)/nvim-test/nvim-test-$(NVIM_TEST_VERSION)/share/nvim/runtime \ $(LUALS)/bin/lua-language-server \ --configpath=../.luarc.json \ --check=lua lewis6991-gitsigns.nvim-7010000/README.md000066400000000000000000000246711476557270400174460ustar00rootroot00000000000000# 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) [![Gitter](https://badges.gitter.im/gitsigns-nvim/community.svg)](https://gitter.im/gitsigns-nvim/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![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. For recommended setup with all batteries included: ```lua require('gitsigns').setup() ``` 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 border = 'single', 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', 'td', gitsigns.toggle_deleted) 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 lewis6991-gitsigns.nvim-7010000/doc/000077500000000000000000000000001476557270400167225ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/doc/gitsigns.txt000066400000000000000000001642731476557270400213270ustar00rootroot00000000000000*gitsigns.txt* Gitsigns *gitsigns.nvim* Author: Lewis Russell Version: v1.0.2 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* For basic setup with all batteries included: >lua require('gitsigns').setup() < 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 border = 'single', 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. setup({cfg}) *gitsigns.setup()* Setup and start Gitsigns. Parameters: ~ {cfg} (table|nil): Configuration for Gitsigns. See |gitsigns-usage| for more details. attach({bufnr}, {ctx}, {callback?}) *gitsigns.attach()* Attach Gitsigns to the buffer. Attributes: ~ {async} Parameters: ~ {bufnr} (integer): Buffer number {ctx} (table|nil): Git context data that may optionally be used to attach to any buffer that represents a real git object. โ€ข {file}: (string) Path to the file represented by the buffer, relative to the top-level. โ€ข {toplevel}: (string?) Path to the top-level of the parent git repository. โ€ข {gitdir}: (string?) Path to the git directory of the parent git repository (typically the ".git/" directory). โ€ข {commit}: (string?) The git revision that the file belongs to. โ€ข {base}: (string?) The git revision that the file should be compared to. 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. refresh({callback?}) *gitsigns.refresh()* Refresh all buffers. Attributes: ~ {async} get_actions() *gitsigns.get_actions()* Get all the available line specific actions for the current buffer at the cursor position. Returns: ~ (table|nil): 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|string): 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|string): 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} (table|nil): Additional options: โ€ข {use_location_list}: (boolean) Populate the location list instead of the quickfix list. Default to `false`. โ€ข {nr}: (integer) Window number or ID when using location list. Expand folds when navigating to a hunk which is inside a fold. Defaults to `0`. โ€ข {open}: (boolean) Open the quickfix/location list viewer. Defaults to `true`. 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: >vim " View the index version of the file :Gitsigns show " View revision of file in the last commit :Gitsigns show ~1 < For a more complete list of ways to specify bases, see |gitsigns-revision|. Attributes: ~ {async} diffthis({base}, {opts}) *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: >vim " Diff against the index :Gitsigns diffthis " Diff against the last commit :Gitsigns diffthis ~1 < For a more complete list of ways to specify bases, see |gitsigns-revision|. Attributes: ~ {async} Parameters: ~ {base} (string|nil): Revision to diff against. Defaults to index. {opts} (table|nil): Additional options: โ€ข {vertical}: {boolean}. Split window vertically. Defaults to config.diff_opts.vertical. If running via command line, then this is taken from the command modifiers. โ€ข {split}: {string}. One of: 'aboveleft', 'belowright', 'botright', 'rightbelow', 'leftabove', 'topleft'. Defaults to 'aboveleft'. If running via command line, then this is taken from the command modifiers. reset_base({global}) *gitsigns.reset_base()* Reset the base revision to diff against back to the index. Alias for `change_base(nil, {global})` . 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: >vim " Change base to 1 commit behind head :lua require('gitsigns').change_base('HEAD~1') " Also works using the Gitsigns command :Gitsigns change_base HEAD~1 " Other variations :Gitsigns change_base ~1 :Gitsigns change_base ~ :Gitsigns change_base ^ " Commits work too :Gitsigns change_base 92eb3dd " Revert to original base :Gitsigns change_base < For a more complete list of ways to specify bases, see |gitsigns-revision|. Parameters: ~ {base} (string|nil): The object/revision to diff against. {global} (boolean|nil): Change the base of all buffers. blame({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} 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} (table|nil): Additional options: โ€ข {full}: (boolean) Display full commit message with hunk. โ€ข {ignore_whitespace}: (boolean) Ignore whitespace when running blame. โ€ข {extra_opts}: (string[]) Extra options passed to `git-blame`. 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|nil): 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} (table|nil): Additional options: โ€ข {greedy}: (boolean) 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. 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. Attributes: ~ {async} Parameters: ~ See |gitsigns.nav_hunk()|. 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. Attributes: ~ {async} Parameters: ~ See |gitsigns.nav_hunk()|. 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} (table|nil): Configuration table. Keys: โ€ข {wrap}: (boolean) Whether to loop around file or not. Defaults to the value 'wrapscan' โ€ข {navigation_message}: (boolean) Whether to show navigation messages or not. Looks at 'shortmess' for default behaviour. โ€ข {foldopen}: (boolean) Expand folds when navigating to a hunk which is inside a fold. Defaults to `true` if 'foldopen' contains `search`. โ€ข {preview}: (boolean) Automatically open preview_hunk() upon navigating to a hunk. โ€ข {greedy}: (boolean) Only navigate between non-contiguous hunks. Only useful if 'diff_opts' contains `linematch`. Defaults to `true`. โ€ข {target}: (`'unstaged'|'staged'|'all'`) Which kinds of hunks to target. Defaults to `'unstaged'`. โ€ข {count}: (integer) Number of times to advance. Defaults to |v:count1|. 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} stage_buffer({callback?}) *gitsigns.stage_buffer()* Stage all hunks in current buffer. Attributes: ~ {async} 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} 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} (table|nil): 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} (table|nil): Additional options: โ€ข {greedy}: (boolean) Stage all contiguous hunks. Only useful if 'diff_opts' contains `linematch`. Defaults to `true`. 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} (table|nil): 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} (table|nil): Additional options: โ€ข {greedy}: (boolean) Stage all contiguous hunks. Only useful if 'diff_opts' contains `linematch`. Defaults to `true`. toggle_deleted({value}) *gitsigns.toggle_deleted()* DEPRECATED: Use |gitsigns.preview_hunk_inline()| Toggle |gitsigns-config-show_deleted| Parameters: ~ {value} (boolean|nil): 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|nil): 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|nil): 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|nil): 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|nil): 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|nil): Value to set toggle. If `nil` the toggle value is inverted. Returns: ~ (boolean): Current value of |gitsigns-config-signcolumn| ============================================================================== 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: `nil` 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. 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: > `{ border = "single", 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. 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-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: lewis6991-gitsigns.nvim-7010000/etc/000077500000000000000000000000001476557270400167305ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/etc/doc_template.txt000066400000000000000000000201671476557270400221370ustar00rootroot00000000000000*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* For basic setup with all batteries included: >lua require('gitsigns').setup() < 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: lewis6991-gitsigns.nvim-7010000/gen_help.lua000077500000000000000000000314321476557270400204470ustar00rootroot00000000000000#!/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') -- 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 --- @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(' HARD-DEPRECATED') else out(' DEPRECATED') end if type(dep_info) == 'table' then if dep_info.message then out(' ' .. 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( (' Please instead use the field `%s` in |gitsigns-config-%s|.'):format(field, opts_key) ) else out((' Please instead use |gitsigns-config-%s|.'):format(dep_info.new_field)) end end end out('') 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 = inspect(v.default):gsub('\n', '\n ') d = ('`%s`'):format(d) end local vtype = (function() local ty = v.type_help or v.type if ty == 'table' and v.deep_extend then return 'table[extended]' end if type(ty) == 'table' then v.type = table.concat(ty, '|') end return v.type end)() if d:find('\n') then out((' Type: `%s`'):format(vtype)) out(' Default: >') out(' ' .. d:gsub('\n([^\n\r])', '\n %1')) out('<') else out((' Type: `%s`, Default: %s'):format(vtype, d)) out() end out(v.description:gsub(' +$', '')) end end --- @return string local function gen_config_doc() local res = {} ---@type string[] local function out(line) res[#res + 1] = line or '' end for _, k in ipairs(get_ordered_schema_keys()) do gen_config_doc_field(k, out) end return table.concat(res, '\n') end --- @param line string --- @return string local function parse_func_header(line) -- match: -- prefix.name = ... -- function prefix.name(... local func = line:match('^%w+%.([%w_]+) =') or line:match('^function %w+%.([%w_]+)%(') if not func then error('Unable to parse: ' .. line) end local args_raw = line:match('function%((.*)%)') -- M.name = function(args) or line:match('function%s+%w+%.[%w_]+%((.*)%)') -- function M.name(args) local args = {} --- @type string[] for k in string.gmatch(args_raw, '([%w_]+)') do if k:sub(1, 1) ~= '_' then args[#args + 1] = string.format('{%s}', k) end end if line:match('async.create%(%d, function%(') then args[#args + 1] = '{callback?}' end return string.format( '%-40s%38s', string.format('%s(%s)', func, table.concat(args, ', ')), '*gitsigns.' .. func .. '()*' ) end --- @param x string --- @return string? name --- @return string? type --- @return string? description local function parse_param(x) local name, ty, des = x:match('([^ ]+) +([^ ]+) *(.*)') return name, ty, des 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(min_pad + 1) end return r 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) ty = ty:gsub('Gitsigns%.%w+', 'table') name_pad = name_pad and (name_pad + 3) or 0 local name_str --- @type string if name == ':' then name_str = '' else local nf = '%-' .. tostring(name_pad) .. 's' name_str = nf:format(string.format('{%s} ', name)) end if #desc == 0 then return { string.format(' %s(%s)', name_str, ty) } end local r = {} --- @type string[] local desc1 = desc[1] == '' and '' or ' ' .. desc[1] r[#r + 1] = string.format(' %s(%s):%s', name_str, ty, desc1) local remain_desc = trim_lines(vim.list_slice(desc, 2)) for _, d in ipairs(remain_desc) do r[#r + 1] = ' ' .. string.rep(' ', name_pad) .. d end return r end --- @param x string[] --- @param amount integer --- @return string[] local function pad(x, amount) local pad_str = string.rep(' ', amount) local r = {} --- @type string[] for _, e in ipairs(x) do r[#r + 1] = pad_str .. e end return r end --- @param state EmmyState --- @param doc_comment string --- @param desc string[] --- @param params [string, string, string[]][] --- @param returns [string, string, string[]][] --- @return EmmyState local function process_doc_comment(state, doc_comment, desc, params, returns) if state == 'none' then state = 'in_block' end local emmy_type, emmy_str = doc_comment:match(' ?@([a-z]+) (.*)') if emmy_type == 'param' then local name, ty, pdesc = parse_param(emmy_str) params[#params + 1] = { name, ty, { pdesc } } return 'in_param' end if emmy_type == 'return' then local ty, name, rdesc = parse_param(emmy_str) returns[#returns + 1] = { name, ty, { rdesc } } return 'in_return' end if state == 'in_param' then -- Consume any remaining doc document lines as the description for the -- last parameter local lastdes = params[#params][3] lastdes[#lastdes + 1] = doc_comment elseif state == 'in_return' then -- Consume any remaining doc document lines as the description for the -- last return local lastdes = returns[#returns][3] lastdes[#lastdes + 1] = doc_comment else if doc_comment ~= '' and doc_comment ~= '<' then doc_comment = string.rep(' ', 16) .. doc_comment end desc[#desc + 1] = doc_comment end return state end --- @param header string --- @param block string[] --- @param params [string, string, string[]][] --- @param returns [string, string, string[]][] --- @param deprecated string? --- @return string[]? local function render_block(header, block, params, returns, deprecated) if vim.startswith(header, '_') then return end local res = { header } if deprecated then list_extend(res, { ' DEPRECATED: '..deprecated, '' }) end list_extend(res, block) -- filter arguments beginning with '_' params = vim.tbl_filter( --- @param v [string, string, string[]] --- @return boolean function(v) return not startswith(v[1], '_') end, params ) if #params > 0 then 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, desc = v[1], v[2], v[3] list_extend(param_block, render_param_or_return(name, ty, desc, name_pad)) end list_extend(res, pad(param_block, 16)) end if #returns > 0 then res[#res + 1] = '' local param_block = { 'Returns: ~' } for _, v in ipairs(returns) do local name, ty, desc = v[1], v[2], v[3] list_extend(param_block, render_param_or_return(name, ty, desc)) end list_extend(res, pad(param_block, 16)) end return res end --- @param path string --- @return string local function gen_functions_doc_from_file(path) local i = io.lines(path) --- @type Iterator[string] local blocks = {} --- @type string[][] --- @alias EmmyState 'none'|'in_block'|'in_param'|'in_return' local state = 'none' --- @type EmmyState local desc = {} --- @type string[] local params = {} --- @type [string, string, string[]][] local returns = {} --- @type [string, string, string[]][] local deprecated --- @type string? for l in i do local doc_comment = l:match('^%-%-%- ?(.*)') --- @type string? if doc_comment then local depre = doc_comment:match('@deprecated ?(.*)') if depre then deprecated = depre else state = process_doc_comment(state, doc_comment, desc, params, returns) end elseif state ~= 'none' then -- First line after block local ok, header = pcall(parse_func_header, l) if ok then blocks[#blocks + 1] = render_block(header, desc, params, returns, deprecated) end state = 'none' desc = {} params = {} returns = {} deprecated = nil end end local res = {} --- @type string[] for j = #blocks, 1, -1 do local b = blocks[j] for k = 1, #b do res[#res + 1] = b[k]:match('^ *$') and '' or b[k] end res[#res + 1] = '' end return table.concat(res, '\n') end --- @param files string[] --- @return string local function gen_functions_doc(files) local res = {} --- @type string[] for _, path in ipairs(files) do res[#res + 1] = gen_functions_doc_from_file(path) end return table.concat(res, '\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', string.rep(' ', 8), spec.desc) res[#res + 1] = '' end res[#res + 1] = string.format('%sFallbacks: %s', string.rep(' ', 8), 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 ' ' .. 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 = 'v1.0.2', -- x-release-please-version CONFIG = gen_config_doc, FUNCTIONS = function() return gen_functions_doc({ 'lua/gitsigns.lua', 'lua/gitsigns/attach.lua', 'lua/gitsigns/actions.lua', }) end, HIGHLIGHTS = gen_highlights_doc, SETUP = get_setup_from_readme, })[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 marker = l: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('%%', '%%%%') l = l:gsub('{{' .. marker .. '}}', sub) end end out:write(l or '', '\n') end end main() lewis6991-gitsigns.nvim-7010000/gitsigns.nvim-scm-1.rockspec000066400000000000000000000013061476557270400234250ustar00rootroot00000000000000local _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' } } lewis6991-gitsigns.nvim-7010000/lua/000077500000000000000000000000001476557270400167365ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/lua/gitsigns.lua000066400000000000000000000133371476557270400212770ustar00rootroot00000000000000local async = require('gitsigns.async') local log = require('gitsigns.debug.log') local Config = require('gitsigns.config') local config = Config.config local api = vim.api local uv = vim.uv or vim.loop --- @class gitsigns.main local M = {} local cwd_watcher ---@type uv.uv_fs_event_t? --- @async --- @return string? gitdir --- @return string? head local function get_gitdir_and_head() local cwd = assert(uv.cwd()) -- Run on the main loop to avoid: -- https://github.com/LazyVim/LazyVim/discussions/3407#discussioncomment-9622211 async.schedule() -- Look in the cache first 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 local info = require('gitsigns.git').Repo.get_info(cwd) if info then return info.gitdir, info.abbrev_head end end local update_cwd_head = async.async(function() 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' 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') 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, async.async(function() local git = require('gitsigns.git') local new_head = git.Repo.get_info(cwd).abbrev_head async.schedule() vim.g.gitsigns_head = new_head end) ) -- Watch .git/HEAD to detect branch changes cwd_watcher:start( towatch, {}, async.async(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() end) ) end) local function setup_cli() api.nvim_create_user_command('Gitsigns', function(params) require('gitsigns.cli').run(params) end, { force = true, nargs = '*', range = true, complete = function(arglead, line) return require('gitsigns.cli').complete(arglead, line) end, }) end --- @async local function setup_attach() if not config.auto_attach then return end 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) local bufnr = args.buf --[[@as integer]] if attach_autocmd_disabled then local __FUNC__ = 'attach_autocmd' log.dprint('Attaching is disabled') return end require('gitsigns.attach').attach(bufnr, nil, args.event) end, }) -- If the buffer name is about to change, then detach api.nvim_create_autocmd('BufFilePre', { group = 'gitsigns', desc = 'Gitsigns: detach when changing buffer names', callback = function(args) require('gitsigns.attach').detach(args.buf) 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 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.attach').attach(buf, nil, 'setup') end end end local function setup_cwd_head() local debounce = require('gitsigns.debounce').debounce_trailing local update_cwd_head_debounced = debounce(100, update_cwd_head) 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 --- Setup and start Gitsigns. --- --- @param cfg table|nil Configuration for Gitsigns. --- See |gitsigns-usage| for more details. function M.setup(cfg) Config.build(cfg) if vim.fn.executable('git') == 0 then print('gitsigns: git not in path. Aborting setup') return end api.nvim_create_augroup('gitsigns', {}) log.setup(config) setup_cli() require('gitsigns.highlight').setup() setup_attach() setup_cwd_head() end --- @type gitsigns.main|gitsigns.actions|gitsigns.attach|gitsigns.debug M = setmetatable(M, { __index = function(_, f) local attach = require('gitsigns.attach') if attach[f] then return attach[f] end 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/000077500000000000000000000000001476557270400205655ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/lua/gitsigns/actions.lua000066400000000000000000000673651476557270400227510ustar00rootroot00000000000000local async = require('gitsigns.async') local Hunks = require('gitsigns.hunks') local manager = require('gitsigns.manager') local message = require('gitsigns.message') local popup = require('gitsigns.popup') local util = require('gitsigns.util') local run_diff = require('gitsigns.diff') 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 --- @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 --- @class Gitsigns.CmdParams --- @field range integer --- @field line1 integer --- @field line2 integer --- @field count integer --- @field smods Gitsigns.CmdParams.Smods --- Variations of functions from M which are used for the Gitsigns command --- @type table local C = {} local CP = {} --- @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 --- 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| M.toggle_signs = function(value) if value ~= nil then config.signcolumn = value else config.signcolumn = not config.signcolumn end M.refresh() 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| M.toggle_numhl = function(value) if value ~= nil then config.numhl = value else config.numhl = not config.numhl end M.refresh() 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 M.refresh() 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| M.toggle_word_diff = function(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| M.toggle_current_line_blame = function(value) if value ~= nil then config.current_line_blame = value else config.current_line_blame = not config.current_line_blame end M.refresh() 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| M.toggle_deleted = function(value) if value ~= nil then config.show_deleted = value else config.show_deleted = not config.show_deleted end M.refresh() 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.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 table|nil 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 table|nil Additional options: --- โ€ข {greedy}: (boolean) --- Stage all contiguous hunks. Only useful if 'diff_opts' --- contains `linematch`. Defaults to `true`. M.stage_hunk = mk_repeatable(async.create(2, function(range, opts) --- @cast range [integer, integer]? opts = opts or {} local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end if bcache:locked() then print('Error: busy') 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 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) bcache:invalidate(true) update(bufnr) end)) 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 table|nil 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 table|nil Additional options: --- โ€ข {greedy}: (boolean) --- Stage all contiguous hunks. Only useful if 'diff_opts' --- contains `linematch`. Defaults to `true`. M.reset_hunk = mk_repeatable(async.create(2, function(range, opts) --- @cast range [integer, integer]? 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)) C.reset_hunk = function(_, params) M.reset_hunk(get_range(params)) end --- Reset the lines of all hunks in the buffer. M.reset_buffer = function() 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]) 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} M.undo_stage_hunk = async.create(0, function() local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end if bcache:locked() then print('Error: busy') return end 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 bcache:invalidate(true) update(bufnr) end) --- Stage all hunks in current buffer. --- --- Attributes: ~ --- {async} M.stage_buffer = async.create(0, function() local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end if bcache:locked() then print('Error: busy') return end -- 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 bcache:invalidate(true) update(bufnr) 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} M.reset_buffer_index = async.create(0, function() local bufnr = current_buf() local bcache = cache[bufnr] if not bcache then return end if bcache:locked() then print('Error: busy') return end -- `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() bcache:invalidate(true) update(bufnr) 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 table|nil Configuration table. Keys: --- โ€ข {wrap}: (boolean) --- Whether to loop around file or not. Defaults --- to the value 'wrapscan' --- โ€ข {navigation_message}: (boolean) --- Whether to show navigation messages or not. --- Looks at 'shortmess' for default behaviour. --- โ€ข {foldopen}: (boolean) --- Expand folds when navigating to a hunk which is --- inside a fold. Defaults to `true` if 'foldopen' --- contains `search`. --- โ€ข {preview}: (boolean) --- Automatically open preview_hunk() upon navigating --- to a hunk. --- โ€ข {greedy}: (boolean) --- Only navigate between non-contiguous hunks. Only useful if --- 'diff_opts' contains `linematch`. Defaults to `true`. --- โ€ข {target}: (`'unstaged'|'staged'|'all'`) --- Which kinds of hunks to target. Defaults to `'unstaged'`. --- โ€ข {count}: (integer) --- Number of times to advance. Defaults to |v:count1|. M.nav_hunk = async.create(2, function(direction, opts) --- @cast opts Gitsigns.NavOpts? require('gitsigns.nav').nav_hunk(direction, opts) end) C.nav_hunk = function(args, _) 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. --- --- Attributes: ~ --- {async} --- --- Parameters: ~ --- See |gitsigns.nav_hunk()|. M.next_hunk = async.create(1, function(opts) require('gitsigns.nav').nav_hunk('next', opts) end) C.next_hunk = function(args, _) 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. --- --- Attributes: ~ --- {async} --- --- Parameters: ~ --- See |gitsigns.nav_hunk()|. M.prev_hunk = async.create(1, function(opts) require('gitsigns.nav').nav_hunk('prev', opts) end) C.prev_hunk = function(args, _) 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. M.preview_hunk = function() require('gitsigns.preview').preview_hunk() end --- Preview the hunk at the cursor position inline in the buffer. M.preview_hunk_inline = async.create(0, function() require('gitsigns.preview').preview_hunk_inline() end) --- Select the hunk under the cursor. --- --- @param opts table|nil Additional options: --- โ€ข {greedy}: (boolean) --- Select all contiguous hunks. Only useful if 'diff_opts' --- contains `linematch`. Defaults to `true`. M.select_hunk = function(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 .arun(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|nil : 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) bufnr = bufnr or current_buf() 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 --- @async --- @param repo Gitsigns.Repo --- @param info Gitsigns.BlameInfoPublic --- @return Gitsigns.Hunk.Hunk?, integer?, integer local function get_blame_hunk(repo, info) local a = {} -- If no previous so sha of blame added the file if info.previous_sha and info.previous_filename then a = repo:get_show_text(info.previous_sha .. ':' .. info.previous_filename) end 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) return hunk, i, #hunks end --- @param is_committed boolean --- @param full boolean --- @return [string, string][][] local function create_blame_fmt(is_committed, full) if not is_committed then return { { { '', 'Label' } }, } end return { { { ' ', 'Directory' }, { ' ', 'MoreMsg' }, { '()', 'Label' }, { ':', 'NormalFloat' }, }, { { full and '' or '', 'NormalFloat' } }, } 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 table|nil Additional options: --- โ€ข {full}: (boolean) --- Display full commit message with hunk. --- โ€ข {ignore_whitespace}: (boolean) --- Ignore whitespace when running blame. --- โ€ข {extra_opts}: (string[]) --- Extra options passed to `git-blame`. M.blame_line = async.create(1, function(opts) if popup.focus_open('blame') then return end --- @type Gitsigns.LineBlameOpts opts = opts or {} local bufnr = 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 result = bcache:get_blame(lnum, opts) pcall(function() loading:close() end) if not bcache:schedule() then return end result = util.convert_blame_info(assert(result)) local is_committed = result.sha and tonumber('0x' .. result.sha) ~= 0 local blame_linespec = create_blame_fmt(is_committed, opts.full) if is_committed and opts.full then local body = bcache.git_obj.repo:command( { 'show', '-s', '--format=%B', result.sha }, { text = true } ) local hunk, hunk_no, num_hunks = get_blame_hunk(bcache.git_obj.repo, result) assert(hunk and hunk_no and num_hunks) result.hunk_no = hunk_no result.body = body result.num_hunks = num_hunks result.hunk_head = hunk.head vim.list_extend(blame_linespec, { { { 'Hunk of ', 'Title' }, { ' ', 'LineNr' } }, unpack(Hunks.linespec_for_hunk(hunk, fileformat)), }) end if not bcache:schedule() then return end popup.create(popup.lines_format(blame_linespec, result), config.preview_config, 'blame') end) C.blame_line = function(args, _) 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} M.blame = async.create(0, function() return require('gitsigns.blame').blame() 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: >vim --- " Change base to 1 commit behind head --- :lua require('gitsigns').change_base('HEAD~1') --- --- " Also works using the Gitsigns command --- :Gitsigns change_base HEAD~1 --- --- " Other variations --- :Gitsigns change_base ~1 --- :Gitsigns change_base ~ --- :Gitsigns change_base ^ --- --- " Commits work too --- :Gitsigns change_base 92eb3dd --- --- " Revert to original base --- :Gitsigns change_base --- < --- --- For a more complete list of ways to specify bases, see --- |gitsigns-revision|. --- --- @param base string|nil The object/revision to diff against. --- @param global boolean|nil Change the base of all buffers. M.change_base = async.create(2, function(base, global) 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) 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: >vim --- " Diff against the index --- :Gitsigns diffthis --- --- " Diff against the last commit --- :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 table|nil Additional options: --- โ€ข {vertical}: {boolean}. Split window vertically. Defaults to --- config.diff_opts.vertical. If running via command line, then --- this is taken from the command modifiers. --- โ€ข {split}: {string}. One of: 'aboveleft', 'belowright', --- 'botright', 'rightbelow', 'leftabove', 'topleft'. Defaults to --- 'aboveleft'. If running via command line, then this is taken --- from the command modifiers. M.diffthis = function(base, opts) -- 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 require('gitsigns.diffthis').diffthis(base, opts) end C.diffthis = function(args, params) -- TODO(lewis6991): validate these local opts = { vertical = args.vertical, split = args.split, } 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: >vim --- " View the index version of the file --- :Gitsigns show --- --- " View revision of file in the last commit --- :Gitsigns show ~1 --- < --- --- For a more complete list of ways to specify bases, see --- |gitsigns-revision|. --- --- Attributes: ~ --- {async} M.show = function(revision, callback) local bufnr = api.nvim_get_current_buf() if not cache[bufnr] then print('Error: Buffer is not attached.') return end local diffthis = require('gitsigns.diffthis') diffthis.show(bufnr, revision, callback) end C.show = function(args, _) M.show(args[1]) end CP.show = complete_heads --- Populate the quickfix list with hunks. Automatically opens the --- quickfix window. --- --- Attributes: ~ --- {async} --- --- @param target integer|string --- 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 table|nil Additional options: --- โ€ข {use_location_list}: (boolean) --- Populate the location list instead of the --- quickfix list. Default to `false`. --- โ€ข {nr}: (integer) --- Window number or ID when using location list. --- Expand folds when navigating to a hunk which is --- inside a fold. Defaults to `0`. --- โ€ข {open}: (boolean) --- Open the quickfix/location list viewer. --- Defaults to `true`. M.setqflist = async.create(2, function(target, opts) require('gitsigns.qflist').setqflist(target, opts) end) C.setqflist = function(args, _) local target = tonumber(args[1]) or args[1] 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|string See |gitsigns.setqflist()|. M.setloclist = function(nr, target) M.setqflist(target, { nr = nr, use_location_list = true, }) end C.setloclist = function(args, _) local target = tonumber(args[2]) or args[2] M.setloclist(tonumber(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} M.refresh = async.create(0, function() manager.reset_signs() require('gitsigns.highlight').setup_highlights() require('gitsigns.current_line_blame').setup() for k, v in pairs(cache) do v:invalidate(true) manager.update(k) 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/async.lua000066400000000000000000000266171476557270400224210ustar00rootroot00000000000000--- @type fun(...: any): { [integer]: any, n: integer } local pack_len = vim.F.pack_len --- @type fun(t: { [integer]: any, n: integer }) local unpack_len = vim.F.unpack_len --- @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 private _callbacks table --- @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? {close: fun(self, callback: fun())} --- --- 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? any[] local Task = {} Task.__index = Task --- @package --- @param func function --- @return Gitsigns.async.Task function Task._new(func) local thread = coroutine.create(func) local self = setmetatable({ _closing = false, _thread = thread, _callbacks = {}, }, Task) threads[thread] = self return self end --- @param callback fun(err?: any, result?: 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, self._result) else table.insert(self._callbacks, callback) 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. 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: --- --- local ok, err_or_result = task:pwait(10) --- --- local _, result = assert(task:pwait(10), task:traceback()) --- --- 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 -- TODO(lewis6991): test me return true, unpack_len(assert(self._result)) end end --- Synchronously wait for a task to finish (blocking) --- @param timeout? integer --- @return any ... result function Task:wait(timeout) local res = pack_len(self:pwait(timeout)) local stat = table.remove(res, 1) res.n = res.n - 1 if not stat then error(res[1]) end return unpack_len(res) end --- @param obj any --- @return boolean local function is_task(obj) return type(obj) == 'table' and getmetatable(obj) == Task end --- @private --- @param msg? string --- @param _lvl? integer --- @return string function Task:_traceback(msg, _lvl) _lvl = _lvl or 0 local thread = ('[%s] '):format(self._thread) local child = self._current_child if is_task(child) then --- @cast child Gitsigns.async.Task msg = child:_traceback(msg, _lvl + 1) end local tblvl = is_task(child) and 2 or nil msg = msg .. 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 --- @param msg? string --- @return string function Task:traceback(msg) return self:_traceback(msg) end --- @package --- @param err? any --- @param result? any[] function Task:_finish(err, result) self._current_child = nil self._err = err self._result = result threads[self._thread] = nil for _, cb in pairs(self._callbacks) do -- Needs to be pcall as step() (who calls this function) cannot error pcall(cb, err, result) end end --- @return boolean function Task:is_closing() return self._closing end --- @param callback? fun() function Task:close(callback) if self:_completed() then if callback then callback() end return end if callback then self:await(function() callback() end) end if self._closing then return end self._closing = true if self._current_child then self._current_child:close(function() self:_finish('closed') end) else self:_finish('closed') end end --- @param callback function --- @param ... any --- @return fun() local function wrap_cb(callback, ...) local args = pack_len(...) return function() return callback(unpack_len(args)) 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 function Task:_resume(...) --- @type [boolean, string|Gitsigns.async.CallbackFn] local ret = { coroutine.resume(self._thread, ...) } local stat = table.remove(ret, 1) --- @type boolean --- @cast ret [string|Gitsigns.async.CallbackFn] if not stat then -- Coroutine had error self:_finish(ret[1]) elseif coroutine.status(self._thread) == 'dead' then -- Coroutine finished self:_finish(nil, ret) else --- @cast ret [Gitsigns.async.CallbackFn] local fn = ret[1] -- 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. r:close(wrap_cb(self._resume, self, ...)) 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 --- @param func function --- @param ... any --- @return Gitsigns.async.Task function M.arun(func, ...) local task = Task._new(func) task:_resume(...) return task end --- Create an async function function M.async(func) return function(...) return M.arun(func, ...) end 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(is_task(task), 'Expected Task') return task:status() end end --- @generic R1, R2, R3, R4 --- @param fun fun(callback: fun(r1: R1, r2: R2, r3: R3, r4: R4)): any? --- @return R1, R2, R3, R4 local function yield(fun) assert(type(fun) == 'function', 'Expected function') return coroutine.yield(fun) end --- @param task Gitsigns.async.Task --- @return any ... local function await_task(task) --- @param callback fun(err?: string, result?: any[]) --- @return function local err, result = yield(function(callback) task:await(callback) return task end) if err then -- TODO(lewis6991): what is the correct level to pass? error(err, 0) end assert(result) return (unpack(result, 1, table.maxn(result))) end --- Asynchronous blocking wait --- @param argc integer --- @param func Gitsigns.async.CallbackFn --- @param ... any func arguments --- @return any ... local function await_cbfun(argc, func, ...) local args = pack_len(...) args.n = math.max(args.n, argc) --- @param callback fun(success: boolean, result: any[]) --- @return any? return yield(function(callback) args[argc] = callback return func(unpack_len(args)) end) end --- Asynchronous blocking wait --- @overload fun(task: Gitsigns.async.Task): any ... --- @overload fun(argc: integer, func: Gitsigns.async.CallbackFn, ...:any): any ... function M.await(...) assert(running(), 'Cannot await in non-async context') local arg1 = select(1, ...) if type(arg1) == 'number' then return await_cbfun(...) elseif is_task(arg1) then return await_task(...) else error('Invalid arguments, expected Task or (argc, func) got: ' .. type(arg1), 2) end end --- Creates an async function with a callback style function. --- @param argc integer --- @param func Gitsigns.async.CallbackFn --- @return function function M.awrap(argc, func) assert(type(argc) == 'number') assert(type(func) == 'function') return function(...) return M.await(argc, func, ...) end end --- create([argc, ] func) --- --- Use this to create a function which executes in an async context but --- called from a non-async context. Inherently this cannot return anything --- since it is non-blocking --- --- If argc is not provided, then the created async function cannot be continued --- --- @generic F: function --- @param argc integer --- @param func F --- @return F function M.create(argc, func) assert(type(argc) == 'number') assert(type(func) == 'function') --- @param ... any --- @return any ... return function(...) local task = Task._new(func) local callback = argc and select(argc + 1, ...) or nil if callback and type(callback) == 'function' then task:await(callback) end task:_resume(unpack({ ... }, 1, argc)) return task end end --- An async function that when called will yield to the Neovim scheduler to be --- able to call the API. M.schedule = M.awrap(1, vim.schedule) --- @param tasks Gitsigns.async.Task[] --- @return fun(): (integer?, any?, any[]?) function M.iter(tasks) local results = {} --- @type [integer, any, any[]][] -- Iter shuold block in an async context so only one waiter is needed local waiter = nil local remaining = #tasks for i, task in ipairs(tasks) do task:await(function(err, result) local callback = waiter -- Clear waiter before calling it waiter = nil remaining = remaining - 1 if callback then -- Iterator is waiting, yield to it callback(i, err, result) else -- Task finished before Iterator was called. Store results. table.insert(results, { i, err, result }) end end) end --- @param callback fun(i?: integer, err?: any, result?: any) return M.awrap(1, function(callback) if next(results) then local res = table.remove(results, 1) callback(unpack(res, 1, table.maxn(res))) elseif remaining == 0 then callback() -- finish else assert(not waiter, 'internal error: waiter already set') waiter = callback end end) end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/attach.lua000066400000000000000000000236731476557270400225470ustar00rootroot00000000000000local 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 cache = Cache.cache local config = require('gitsigns.config').config local dprint = log.dprint local dprintf = log.dprintf local throttle_by_id = require('gitsigns.debounce').throttle_by_id local api = vim.api local uv = vim.loop --- @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 dprintf("%s buffer for file '%s' from path '%s' on commit '%s'", plugin, rel_path, file, 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) local __FUNC__ = 'on_reload' assert(cache[bufnr]):invalidate() dprint('Reload') manager.update_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 --- @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() api.nvim_create_autocmd('OptionSet', { group = 'gitsigns', pattern = { 'fileformat', 'bomb', 'eol' }, callback = function() local buf = vim.api.nvim_get_current_buf() local bcache = cache[buf] if not bcache then return end bcache:invalidate(true) async.arun(function() manager.update(buf) end) end, }) require('gitsigns.current_line_blame').setup() api.nvim_create_autocmd('VimLeavePre', { group = 'gitsigns', callback = M.detach_all, }) end) --- @class Gitsigns.GitContext --- @field file string --- @field toplevel? string --- @field gitdir? string --- @field base? string --- @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 file = uv.fs_realpath(api.nvim_buf_get_name(bufnr)) or api.nvim_buf_call(bufnr, function() return vim.fn.expand('%:p') end) local rel_path, commit, gitdir_from_bufname = parse_git_path(file) if not gitdir_from_bufname then if vim.bo[bufnr].buftype ~= '' then return nil, 'Non-normal buffer' end local file_dir = util.dirname(file) if not file_dir or not util.path_exists(file_dir) then return nil, 'Not a path' end end local gitdir_oap, toplevel_oap = on_attach_pre(bufnr) return { file = rel_path or file, 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 --- 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 local attach_throttled = throttle_by_id(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 = ctx.file if not vim.startswith(file, '/') and ctx.toplevel then file = ctx.toplevel .. util.path_sep .. file end local revision = ctx.base or config.base local git_obj = git.Obj.new(file, revision, encoding, ctx.gitdir, ctx.toplevel) if not git_obj and not passed_ctx then for _, wt in ipairs(config.worktrees or {}) 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 passed_ctx and (not util.path_exists(file) or uv.fs_stat(file).type == 'directory') then dprint('Not a file') return end 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({ bufnr = cbuf, file = file, git_obj = git_obj, }) if config.watch_gitdir.enable then local watcher = require('gitsigns.watcher') cache[cbuf].gitdir_watcher = watcher.watch_gitdir(cbuf, git_obj.repo.gitdir) 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_debounced(cbuf) end, }) -- Initial update manager.update(cbuf) 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 --- Attach Gitsigns to the buffer. --- --- Attributes: ~ --- {async} --- --- @param bufnr integer Buffer number --- @param ctx Gitsigns.GitContext|nil --- Git context data that may optionally be used to attach to any --- buffer that represents a real git object. --- โ€ข {file}: (string) --- Path to the file represented by the buffer, relative to the --- top-level. --- โ€ข {toplevel}: (string?) --- Path to the top-level of the parent git repository. --- โ€ข {gitdir}: (string?) --- Path to the git directory of the parent git repository --- (typically the ".git/" directory). --- โ€ข {commit}: (string?) --- The git revision that the file belongs to. --- โ€ข {base}: (string?) --- The git revision that the file should be compared to. --- @param _trigger? string M.attach = async.create(3, function(bufnr, ctx, _trigger) attach_throttled(bufnr or api.nvim_get_current_buf(), ctx, _trigger) end) return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/blame.lua000066400000000000000000000267201476557270400223570ustar00rootroot00000000000000local async = require('gitsigns.async') local cache = require('gitsigns.cache').cache local log = require('gitsigns.debug.log') local util = require('gitsigns.util') 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 = tonumber(x, 16) return math.min(0xdf, 0x20 + math.floor((y * 0x10 + (15 - y)) * 0.75)) 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)') local color = mod(r) * 0x10000 + mod(g) * 0x100 + mod(b) if hash_colors[sha] then return hash_colors[sha] 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 ---@param amount integer ---@param text string ---@return string local function lalign(amount, text) local len = vim.str_utfindex(text) return text .. string.rep(' ', math.max(0, amount - len)) end local chars = { first = 'โ”', mid = 'โ”‚', last = 'โ”•', single = 'โ•บ', } local M = {} --- @param blame table --- @param win integer --- @param main_win integer --- @param buf_sha string local function render(blame, win, main_win, buf_sha) local max_author_len = 0 for _, blame_info in pairs(blame) do max_author_len = math.max(max_author_len, (vim.str_utfindex(blame_info.commit.author))) end local lines = {} --- @type string[] local last_sha --- @type string? local cnt = 0 local commit_lines = {} --- @type table for i, hl in pairs(blame) do local sha = hl.commit.abbrev_sha local next_sha = blame[i + 1] and blame[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 string.format('%s %s', c, hl.commit.summary) or c else cnt = 0 commit_lines[i] = true lines[i] = string.format( '%s %s %s %s', chars.first, sha, lalign(max_author_len, hl.commit.author), util.expand_format('', hl.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) -- Apply highlights for i, blame_info in ipairs(blame) do local hash_color = 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_color, }) if commit_lines[i] then local width = string.len(lines[i]) api.nvim_buf_set_extmark(bufnr, ns, i - 1, width - 10, { end_col = width, hl_group = 'Title', }) else 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 api.nvim_buf_set_extmark(bufnr, ns, i - 1, 0, { line_hl_group = '@markup.italic', }) end if commit_lines[i] and commit_lines[i + 1] then api.nvim_buf_set_extmark(bufnr, ns, i - 1, 0, { virt_lines = { { { chars.last, hash_color }, { ' ' }, { blame_info.commit.summary, 'Comment' } }, }, }) local fillchar = string.rep(vim.opt.fillchars:get().diff or '-', 1000) api.nvim_buf_set_extmark(main_buf, ns, i - 1, 0, { virt_lines = { { { fillchar, 'Comment' } } }, virt_lines_leftcol = true, }) end end end --- @param blame table --- @param win integer --- @param revision? string --- @param parent? boolean local function reblame(blame, win, revision, parent) local blm_win = api.nvim_get_current_win() local lnum = unpack(api.nvim_win_get_cursor(blm_win)) local sha = 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) require('gitsigns').show( sha, vim.schedule_wrap(function() local bufnr = api.nvim_get_current_buf() local ok = vim.wait(1000, function() return cache[bufnr] ~= nil end) if not ok then error('Timeout waiting for attach') end async.arun(M.blame) end) ) end --- @param win integer --- @param open 'vsplit'|'tabnew' --- @param bcache Gitsigns.CacheEntry local show_commit = async.async(function(win, open, bcache) local cursor = api.nvim_win_get_cursor(win)[1] local sha = bcache.blame[cursor].commit.sha local res = bcache.git_obj.repo:command({ 'show', sha }) async.schedule() local buffer_name = bcache:get_rev_bufname(sha, true) local commit_buf = nil -- find preexisting commit buffer or create a new one for _, bufnr in ipairs(api.nvim_list_bufs()) do if api.nvim_buf_get_name(bufnr) == buffer_name then commit_buf = bufnr 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) end vim.cmd[open]({ mods = { keepalt = true } }) api.nvim_win_set_buf(0, commit_buf) vim.bo[commit_buf].filetype = 'git' vim.bo[commit_buf].bufhidden = 'wipe' 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 = unpack(api.nvim_win_get_cursor(w)) 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 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) if vim.fn.pumvisible() == 0 then return '' else return '' end end, opts) end --- @async function M.blame() local __FUNC__ = 'blame' 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 bcache:get_blame() 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) 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 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') 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() reblame(blame, win, bcache.git_obj.revision) end, { desc = 'Reblame at commit', buffer = blm_bufnr, }) pmap('n', 'R', function() reblame(blame, win, bcache.git_obj.revision, true) end, { desc = 'Reblame at commit parent', buffer = blm_bufnr, }) pmap('n', 's', function() show_commit(blm_win, 'vsplit', bcache) end, { desc = 'Show commit in a vertical split', buffer = blm_bufnr, }) pmap('n', 'S', function() show_commit(blm_win, 'tabnew', bcache) end, { desc = 'Show commit in a new tab', buffer = blm_bufnr, }) menu('GitsignsBlame', { { 'Reblame at commit', 'r' }, { 'Reblame at commit parent', 'R' }, { 'Show commit (vsplit)', 's' }, { ' (tab)', 'S' }, }) local group = api.nvim_create_augroup('GitsignsBlame', {}) api.nvim_create_autocmd({ 'CursorMoved', 'BufLeave' }, { buffer = blm_bufnr, group = group, callback = function() api.nvim_buf_clear_namespace(blm_bufnr, ns_hl, 0, -1) api.nvim_buf_clear_namespace(bufnr, ns_hl, 0, -1) end, }) -- Highlight the same commit under the cursor api.nvim_create_autocmd('CursorMoved', { buffer = blm_bufnr, group = group, callback = function() local cursor = unpack(api.nvim_win_get_cursor(blm_win)) local cur_sha = blame[cursor].commit.abbrev_sha for i, info in pairs(blame) do if info.commit.abbrev_sha == cur_sha then api.nvim_buf_set_extmark(blm_bufnr, ns_hl, i - 1, 0, { line_hl_group = '@markup.strong', }) end end 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/cache.lua000066400000000000000000000176411476557270400223440ustar00rootroot00000000000000local 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 --- @field bufnr integer --- @field file string --- @field compare_text? string[] --- @field hunks? Gitsigns.Hunk.Hunk[] --- @field force_next_update? boolean --- @field file_mode? boolean --- --- @field compare_text_head? string[] --- @field hunks_staged? Gitsigns.Hunk.Hunk[] --- --- @field staged_diffs? Gitsigns.Hunk.Hunk[] --- @field gitdir_watcher? uv.uv_fs_event_t --- @field git_obj Gitsigns.GitObj --- @field blame? table --- --- @field update_lock? true Update in progress local CacheEntry = M.CacheEntry function CacheEntry:get_rev_bufname(rev, nofile) rev = rev or self.git_obj.revision or ':0' if nofile then return string.format('gitsigns://%s//%s', self.git_obj.repo.gitdir, rev) end return string.format('gitsigns://%s//%s:%s', self.git_obj.repo.gitdir, rev, self.git_obj.relpath) end function CacheEntry:locked() return self.git_obj.lock or self.update_lock or false 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 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 o Gitsigns.CacheEntry --- @return Gitsigns.CacheEntry function M.new(o) o.staged_diffs = o.staged_diffs or {} return setmetatable(o, { __index = CacheEntry }) end local sleep = async.awrap(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 --- @param opts? Gitsigns.BlameOpts --- @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 = 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, lnum0 == nil end end 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[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[i] then return false end end return true end --- If lnum is nil then run blame for the entire buffer. --- @async --- @param lnum? integer --- @param opts? Gitsigns.BlameOpts --- @return Gitsigns.BlameInfo? function CacheEntry:get_blame(lnum, opts) local blame = self.blame if not blame or not self:blame_valid(lnum) then self:wait_for_hunks() blame = blame or {} local Hunks = require('gitsigns.hunks') if lnum and Hunks.find_hunk(lnum, self.hunks) 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) blame[lnum] = Blame.get_blame_nc(relpath, lnum) else -- Refresh/update cache local b, full = self:run_blame(lnum, opts) if lnum and not full then blame[lnum] = b[lnum] else blame = b end end self.blame = blame end return blame[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 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 vim.deepcopy(self.hunks_staged) end return vim.deepcopy(self.hunks) 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 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(self.compare_text, staged_top, staged_bot) hunk.removed.lines = vim.list_slice( 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( self.compare_text, hunk.removed.start, hunk.removed.start + hunk.removed.count - 1 ) end return hunk end function CacheEntry:destroy() local w = self.gitdir_watcher if w and not w:is_closing() then w:close() end self.git_obj.repo:unref() end ---@type table M.cache = {} --- @param bufnr integer function M.destroy(bufnr) assert(M.cache[bufnr]):destroy() M.cache[bufnr] = nil end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/cli.lua000066400000000000000000000051001476557270400220330ustar00rootroot00000000000000local 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,boolean> local sources = { [actions] = true, [attach] = false, [Debug] = false, } -- 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' 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 pairs(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(words[2]) if cmp_func then return cmp_func(arglead) end end return matches end M.run = async.create(1, function(params) local __FUNC__ = 'cli.run' 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, vim.ui.select, 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, has_named in pairs(sources) do local f = m[func] if type(f) == 'function' then -- Note functions here do not have named arguments f(unpack(pos_args), has_named and named_args or nil) return end end message.error('%s is not a valid function or action', func) end) return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/cli/000077500000000000000000000000001476557270400213345ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/lua/gitsigns/cli/argparse.lua000066400000000000000000000051121476557270400236420ustar00rootroot00000000000000local 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/config.lua000066400000000000000000000661131476557270400225440ustar00rootroot00000000000000--- @class (exact) Gitsigns.SchemaElem --- @field type string|string[]|fun(x:any): boolean --- @field type_help? string --- @field refresh? 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.DiffOpts --- @field algorithm string --- @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 hl string --- @field text string --- @field numhl string --- @field linehl string --- @field culhl 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 --- @field ignore_whitespace? boolean --- @field extra_opts? string[] --- @class (exact) Gitsigns.LineBlameOpts : Gitsigns.BlameOpts --- @field full? boolean --- @class (exact) Gitsigns.Config --- @field debug_mode boolean --- @field diff_opts Gitsigns.DiffOpts --- @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) --- @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 table --- @field auto_attach boolean --- @field attach_to_untracked boolean --- @field worktrees {toplevel: string, gitdir: string}[] --- @field word_diff boolean --- @field trouble 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 local M = { Config = { DiffOpts = {}, SignConfig = {}, watch_gitdir = {}, current_line_blame_opts = {}, Worktree = {}, }, } --- @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 = string.sub(o, ('algorithm:'):len() + 1) elseif vim.startswith(o, 'linematch:') then r.linematch = tonumber(string.sub(o, ('linematch:'):len() + 1)) end end return r end --- @type Gitsigns.Config M.config = setmetatable({}, { __index = function(t, k) if rawget(t, k) == nil then local field = M.schema[k] if not field then return end rawset(t, k, resolve_default(field)) if field.refresh then field.refresh(function() rawset(t, k, resolve_default(field)) end) end end return rawget(t, 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 local warnings --- @type table? --- @diagnostic disable-next-line:no-unknown for kind, s in pairs(M.schema.signs.default) do --- @diagnostic disable-next-line:no-unknown for ty, v in pairs(s) do if x[kind] and x[kind][ty] and vim.endswith(ty, 'hl') then warnings = warnings or {} local w = string.format( "'signs.%s.%s' is now deprecated, please define highlight '%s' e.g:\n" .. " vim.api.nvim_set_hl(0, '%s', { link = '%s' })", kind, ty, v, v, x[kind][ty] ) warnings[w] = true end end end if warnings then for w in vim.spairs(warnings) do warn(w) end end return true end --- @type table M.schema = { signs = { type_help = 'table', type = validate_signs, deep_extend = true, default = { add = { hl = 'GitSignsAdd', text = 'โ”ƒ', numhl = 'GitSignsAddNr', linehl = 'GitSignsAddLn', culhl = 'GitSignsAddCul', }, change = { hl = 'GitSignsChange', text = 'โ”ƒ', numhl = 'GitSignsChangeNr', linehl = 'GitSignsChangeLn', culhl = 'GitSignsChangeCul', }, delete = { hl = 'GitSignsDelete', text = 'โ–', numhl = 'GitSignsDeleteNr', linehl = 'GitSignsDeleteLn', culhl = 'GitSignsDeleteCul', }, topdelete = { hl = 'GitSignsTopdelete', text = 'โ–”', numhl = 'GitSignsTopdeleteNr', linehl = 'GitSignsTopdeleteLn', culhl = 'GitSignsTopdeleteCul', }, changedelete = { hl = 'GitSignsChangedelete', text = '~', numhl = 'GitSignsChangedeleteNr', linehl = 'GitSignsChangedeleteLn', culhl = 'GitSignsChangedeleteCul', }, untracked = { hl = 'GitSignsUntracked', text = 'โ”†', numhl = 'GitSignsUntrackedNr', linehl = 'GitSignsUntrackedLn', culhl = 'GitSignsUntrackedCul', }, }, 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 = { hl = 'GitSignsStagedAdd', text = 'โ”ƒ', numhl = 'GitSignsStagedAddNr', linehl = 'GitSignsStagedAddLn', culhl = 'GitSignsStagedAddCul', }, change = { hl = 'GitSignsStagedChange', text = 'โ”ƒ', numhl = 'GitSignsStagedChangeNr', linehl = 'GitSignsStagedChangeLn', culhl = 'GitSignsStagedChangeCul', }, delete = { hl = 'GitSignsStagedDelete', text = 'โ–', numhl = 'GitSignsStagedDeleteNr', linehl = 'GitSignsStagedDeleteLn', culhl = 'GitSignsStagedDeleteCul', }, topdelete = { hl = 'GitSignsStagedTopdelete', text = 'โ–”', numhl = 'GitSignsStagedTopdeleteNr', linehl = 'GitSignsStagedTopdeleteLn', culhl = 'GitSignsStagedTopdeleteCul', }, changedelete = { hl = 'GitSignsStagedChangedelete', text = '~', numhl = 'GitSignsStagedChangedeleteNr', linehl = 'GitSignsStagedChangedeleteLn', culhl = 'GitSignsStagedChangedeleteCul', }, }, 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 = nil, 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', default = nil, 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', default = nil, 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, refresh = 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. ]], }, base = { type = 'string', default = nil, 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 = { border = 'single', 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. ]], }, _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 = false, description = [[ Use new sign calculation method ]], }, debug_mode = { type = 'boolean', default = false, description = [[ Enables debug logging and makes the following functions available: `dump_cache`, `debug_messages`, `clear_debug`. ]], }, } local function validate(k, v, ty) if vim.fn.has('nvim-0.11') == 1 then --- @diagnostic disable-next-line: redundant-parameter,param-type-mismatch vim.validate(k, v, ty) else vim.validate({ [k] = { v, ty } }) end end --- @param config Gitsigns.Config local function validate_config(config) for k, v in pairs(config --[[@as table]]) do local kschema = M.schema[k] if kschema == nil then warn("gitsigns: Ignoring invalid configuration field '%s'", k) else local ty = kschema.type if type(ty) == 'string' or type(ty) == 'function' then validate(k, v, ty) end end end end --- @param cfg table local function handle_deprecated(cfg) for k, v in pairs(M.schema) do local dep = v.deprecated if dep and cfg[k] ~= nil then if type(dep) == 'table' then if dep.hard then if dep.message then warn(dep.message) else warn('%s is now deprecated; ignoring', k) end end else warn('%s is now deprecated; ignoring', k) end end end end --- @param k string --- @param v Gitsigns.SchemaElem --- @param user_val any local function build_field(k, v, user_val) local config = M.config --[[@as table]] if v.deep_extend then local d = resolve_default(v) config[k] = vim.tbl_deep_extend('force', d, user_val) else config[k] = user_val end end --- @param user_config Gitsigns.Config|nil function M.build(user_config) user_config = user_config or {} handle_deprecated(user_config) validate_config(user_config) for k, v in pairs(M.schema) do if user_config[k] ~= nil then build_field(k, v, user_config[k]) if v.refresh then v.refresh(function() build_field(k, v, user_config[k]) end) end end end end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/current_line_blame.lua000066400000000000000000000147421476557270400251310ustar00rootroot00000000000000local 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').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 --- @return integer local function win_width() local winid = 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 = 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 bufnr integer ---@param blame_info Gitsigns.BlameInfoPublic ---@return [string, string][] local function get_blame_virt_text(bufnr, blame_info) local git_obj = assert(cache[bufnr]).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 return res end 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 return default_formatter(clb_formatter)(git_obj.repo.username, blame_info) end --- @param bufnr integer --- @param lnum integer --- @param blame_info Gitsigns.BlameInfo --- @param opts Gitsigns.CurrentLineBlameOpts local function handle_blame_info(bufnr, lnum, blame_info, opts) blame_info = util.convert_blame_info(blame_info) local virt_text = get_blame_virt_text(bufnr, 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 virt_text_pos == 'right_align' then if api.nvim_strwidth(virt_text_str) > (win_width() - 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 --- @param bufnr integer local function update0(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 update0(bufnr) return end handle_blame_info(bufnr, lnum, blame_info, opts) end local update = async.create(1, debounce.throttle_by_id(update0)) --- @type fun(bufnr: integer) M.update = nil 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 local opts = config.current_line_blame_opts -- TODO(lewis6991): opts.delay is always defined as the schema set -- deep_extend=true M.update = debounce.debounce_trailing(assert(opts.delay), update) -- show current buffer line blame immediately M.update(api.nvim_get_current_buf()) local update_events = { 'BufEnter', 'CursorMoved', 'CursorMovedI' } local reset_events = { 'InsertEnter', 'BufLeave' } if vim.fn.exists('#WinResized') == 1 then -- For nvim 0.9+ update_events[#update_events + 1] = 'WinResized' end if 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 return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/debounce.lua000066400000000000000000000040601476557270400230540ustar00rootroot00000000000000local uv = vim.uv or vim.loop local M = {} --- Debounces a function on the trailing edge. --- --- @generic F: function --- @param ms number Timeout in ms --- @param fn F Function to debounce --- @param hash? integer|fun(...): any Function that determines id from arguments to fn --- @return F Debounced function. function M.debounce_trailing(ms, fn, hash) local running = {} --- @type table if type(hash) == 'number' then local hash_i = hash hash = function(...) return select(hash_i, ...) end end return function(...) local id = hash and hash(...) or true if running[id] == nil then running[id] = assert(uv.new_timer()) end local timer = running[id] local argv = { ... } timer:start(ms, 0, function() timer:stop() running[id] = nil fn(unpack(argv, 1, table.maxn(argv))) end) end end --- Throttles a 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 _/โ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พ\/โ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พ\____________ -- --- fn#2 ______/โ€พ\___________/โ€พ\___________________ --- throttled#2 ______/โ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พ\__/โ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พโ€พ\__________ --- --- --- @generic F: function --- @param fn F Function to throttle --- @param schedule? boolean --- @return F throttled function. function M.throttle_by_id(fn, schedule) local scheduled = {} --- @type table local running = {} --- @type table return function(id, ...) 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(id, ...) running[id] = nil end end end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/debug.lua000066400000000000000000000022431476557270400223570ustar00rootroot00000000000000local 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/debug/000077500000000000000000000000001476557270400216535ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/lua/gitsigns/debug/log.lua000066400000000000000000000103471476557270400231440ustar00rootroot00000000000000local uv = vim.uv or vim.loop local start_time = uv.hrtime() --- @class Gitsigns.log --- @field private messages [number, string, string, string][] local M = { debug_mode = false, verbose = false, messages = {}, } --- @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 lvl integer --- @return {name:string, bufnr: integer} local function get_context(lvl) lvl = lvl + 1 local name = getvarvalue('__FUNC__', lvl) if not name then local name0 = debug.getinfo(lvl, 'n').name or '' name = name0:gsub('(.*)%d+$', '%1') end local bufnr = getvarvalue('bufnr', lvl) or getvarvalue('_bufnr', lvl) or getvarvalue('cbuf', lvl) or getvarvalue('buf', lvl) return { name = name, 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.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] --- @return [string,string][] local function build_msg(m) local time, kind, ctx, msg = m[1], m[2], m[3], m[4] local hl = sev_to_hl[kind] return { { string.format('%.2f ', time), 'Comment' }, { kind:upper():sub(1, 1), hl }, { string.format(' %s:', ctx), 'Tag' }, { ' ' }, { msg }, } end function M.show() for _, m in ipairs(M.messages) do vim.api.nvim_echo(build_msg(m), false, {}) end end --- @return string[]? function M.get() local r = {} --- @type string[] for _, m in ipairs(M.messages) do local e = build_msg(m) 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 --- @param config Gitsigns.Config function M.setup(config) M.debug_mode = config.debug_mode M.verbose = config._verbose end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/diff.lua000066400000000000000000000011351476557270400222000ustar00rootroot00000000000000local 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/diff_ext.lua000066400000000000000000000047631476557270400230720ustar00rootroot00000000000000local Hunks = require('gitsigns.hunks') local util = require('gitsigns.util') 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 = util.tmpname() local file_cmp = util.tmpname() 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] 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/diff_int.lua000066400000000000000000000120371476557270400230550ustar00rootroot00000000000000local async = require('gitsigns.async') local uv = vim.uv or vim.loop local create_hunk = require('gitsigns.hunks').create_hunk local config = require('gitsigns.config').config local function getencdec() local m = jit and package.preload['string.buffer'] and require('string.buffer') or vim.mpack return m.encode, m.decode end --- @param f function --- @param args any[] --- @param cb function(...) local new_thread = async.awrap(3, function(f, args, cb) local encode, decode = getencdec() uv.new_work(function(getencdec_bc, fd, argse) local getencdec0 = getencdec or assert(loadstring(getencdec_bc)) local encode0, decode0 = getencdec0() local args0 = decode0(argse) --[[@as any[] ]] local f0 = assert(loadstring(fd)) return encode0(f0(unpack(args0))) end, function(r) cb(decode(r)) end):queue(string.dump(getencdec), string.dump(f), encode(args)) 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 return 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) -- Denoise the hunks local ret = { hunks[1] } --- @type Gitsigns.Hunk.Hunk[] 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 -- pair lines by position local a = table.concat(vim.split(removed[i], ''), '\n') local b = table.concat(vim.split(added[i], ''), '\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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/diffthis.lua000066400000000000000000000161351476557270400230760ustar00rootroot00000000000000local async = require('gitsigns.async') 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_by_id = require('gitsigns.debounce').throttle_by_id local api = vim.api local M = {} --- @async --- @param bufnr integer --- @param dbufnr integer --- @param base string? local function bufread(bufnr, dbufnr, base) 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) if err then error(err, 2) end async.schedule() if not api.nvim_buf_is_valid(bufnr) then return end end vim.bo[dbufnr].fileformat = vim.bo[bufnr].fileformat vim.bo[dbufnr].filetype = vim.bo[bufnr].filetype 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 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:stage_lines(buftext) 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? --- @return string? buf Buffer name --- @return integer? bufnr Buffer number local function create_revision_buf(bufnr, base) local bcache = assert(cache[bufnr]) base = util.norm_base(base) local bufname = bcache:get_rev_bufname(base) 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) 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.arun(bufread, bufnr, dbuf, base) end, }) api.nvim_create_autocmd('BufWriteCmd', { group = 'gitsigns', buffer = dbuf, callback = function() async.arun(bufwrite, bufnr, dbuf, base) end, }) else vim.bo[dbuf].buftype = 'nowrite' vim.bo[dbuf].modifiable = false end return bufname, dbuf end --- @class Gitsigns.DiffthisOpts --- @field vertical? boolean --- @field split? string --- @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 'aboveleft', 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 --- @param base string? --- @param opts Gitsigns.DiffthisOpts --- @param _callback? fun() M.diffthis = async.create(2, function(base, opts, _callback) 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) --- @param bufnr integer --- @param base string --- @param _callback? fun() M.show = async.create(2, function(bufnr, base, _callback) __FUNC__ = 'show' local bufname = create_revision_buf(bufnr, base) if not bufname then log.dprint('No bufname for revision ' .. base) return 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) end end) --- @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') and vim.fn.FugitiveParse(name)[1] ~= ':' end --- This function needs to be throttled as there is a call to vim.ui.input --- @async --- @param bufnr integer --- @param _callback? fun() M.update = throttle_by_id(function(bufnr, _callback) 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/git.lua000066400000000000000000000143121476557270400220540ustar00rootroot00000000000000local log = require('gitsigns.debug.log') local async = require('gitsigns.async') local util = require('gitsigns.util') local Repo = require('gitsigns.git.repo') 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? true local Obj = {} Obj.__index = Obj M.Obj = Obj --- @param file string --- @return boolean local function in_git_dir(file) for _, p in ipairs(vim.split(file, util.path_sep)) do if p == '.git' then return true end end return false end --- @async --- @param revision? string --- @return string? err function Obj:change_revision(revision) self.revision = util.norm_base(revision) return self:refresh() 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 --- @return string[] stdout, string? stderr function Obj:get_show_text(revision) if revision and not self.relpath then log.dprint('no relpath') return {} end local object = revision and (revision .. ':' .. self.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) 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.lock = true self.repo:command({ 'reset', self.file }) self.lock = nil autocmd_changed(self.file) end --- @async --- @param contents? string[] --- @param lnum? integer --- @param revision? string --- @param opts? Gitsigns.BlameOpts --- @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() self.lock = true 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, self.relpath, true) end self:refresh() self.lock = nil end --- @async --- Stage 'lines' as the entire contents of the file --- @param lines string[] function Obj:stage_lines(lines) self.lock = true local new_object = self.repo:hash_object(self.relpath, lines) self.repo:update_index(self.mode_bits, new_object, self.relpath) self.lock = nil autocmd_changed(self.file) end local sleep = async.awrap(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.lock = true self:ensure_file_in_index() local patch = require('gitsigns.hunks').create_patch(self.relpath, hunks, self.mode_bits, invert) if not self.i_crlf and self.w_crlf then -- Remove cr for i = 1, #patch do patch[i] = patch[i]: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 self.lock = nil return err end -- Staging operations cause IO of the git directory so wait some time -- for the changes to settle. sleep(100) self.lock = nil autocmd_changed(self.file) end --- @async --- @param file string --- @param revision string? --- @param encoding string --- @param gitdir string? --- @param toplevel string? --- @return Gitsigns.GitObj? function Obj.new(file, revision, encoding, gitdir, toplevel) -- TODO(lewis6991): this check is flawed as any directory can be a git-dir -- Can use: git rev-parse --is-inside-git-dir if in_git_dir(file) then log.dprint('In git dir') return end if not vim.startswith(file, '/') and toplevel then file = toplevel .. util.path_sep .. file end local repo = Repo.get(util.dirname(file), gitdir, toplevel) if not repo then log.dprint('Not in git repo') 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, err = repo:file_info(file, revision) if err and not silent then log.eprint(err) end if not info then return end local self = setmetatable({}, Obj) self.repo = repo self.file = file 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 return self end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/git/000077500000000000000000000000001476557270400213505ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/lua/gitsigns/git/blame.lua000066400000000000000000000151431476557270400231370ustar00rootroot00000000000000local uv = vim.uv or vim.loop local error_once = require('gitsigns.message').error_once local log = require('gitsigns.debug.log') --- @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(tonumber(x)) end --- @param readline fun(): string? --- @param commits table --- @param result table local function incremental_iter(readline, commits, result) local line = assert(readline()) --- @type string, string, string, string local sha, orig_lnum_str, final_lnum_str, size_str = line:match('(%x+) (%d+) (%d+) (%d+)') assert(sha) local orig_lnum = asinteger(orig_lnum_str) local final_lnum = asinteger(final_lnum_str) local size = asinteger(size_str) --- @type table local commit = commits[sha] or { sha = sha, abbrev_sha = sha:sub(1, 8), } --- @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 value = tonumber(value) end commit[key] = value 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 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 --- @return string[] local function data_to_lines(data) local lines = vim.split(data, '\n') if lines[#lines] == '' then lines[#lines] = nil end return lines 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 = data_to_lines(data) local i = 0 local function readline(peek) if not data_lines[i + 1] then data = coroutine.yield() if not data then return end data_lines = data_to_lines(data) 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 --- @param obj Gitsigns.GitObj --- @param contents? string[] --- @param lnum? integer --- @param revision? string --- @param opts? Gitsigns.BlameOpts --- @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 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({ 'blame', '--incremental', contents and { '--contents', '-' }, opts.ignore_whitespace and '-w', lnum and { '-L', 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 error_once('Error running git-blame: ' .. stderr) return {} end return ret end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/git/cmd.lua000066400000000000000000000040021476557270400226120ustar00rootroot00000000000000local async = require('gitsigns.async') local log = require('gitsigns.debug.log') local system = require('gitsigns.system').system --- @type fun(cmd: string[], opts?: vim.SystemOpts): vim.SystemCompleted local asystem = async.awrap(3, system) --- @class Gitsigns.Git.JobSpec : vim.SystemOpts --- @field ignore_error? boolean --- @param x table --- @return string[] local function flatten(x) local ret = {} --- @type string[] for k, v in pairs(x) do if type(k) == 'number' then if type(v) == 'table' then vim.list_extend(ret, flatten(v)) elseif type(v) == 'string' then ret[#ret + 1] = v end end end return ret end --- @async --- @param args table --- @param spec? Gitsigns.Git.JobSpec --- @return string[] stdout, string? stderr, integer code local function git_command(args, spec) spec = spec or {} local cmd = flatten({ 'git', '--no-pager', '--no-optional-locks', '--literal-pathspecs', '-c', 'gc.auto=0', -- Disable auto-packing which emits messages to stderr args, }) if spec.text == nil then spec.text = true end -- Fix #895. Only needed for Nvim 0.9 and older spec.clear_env = true --- @type vim.SystemCompleted local obj = asystem(cmd, spec) 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/git/repo.lua000066400000000000000000000275411476557270400230310ustar00rootroot00000000000000local async = require('gitsigns.async') local git_command = require('gitsigns.git.cmd') local log = require('gitsigns.debug.log') local util = require('gitsigns.util') local system = require('gitsigns.system').system local check_version = require('gitsigns.git.version').check local uv = vim.uv or vim.loop --- @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 local M = {} --- 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 return git_command({ '--git-dir', self.gitdir, self.detached and { '--work-tree', self.toplevel }, args, }, spec) end --- @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 --- @async function M:update_abbrev_head() local info, err = M.get_info(self.toplevel) if not info then log.eprintf('Could not get info for repo at %s: %s', self.gitdir, err or '') return end self.abbrev_head = info.abbrev_head end --- @async --- @private --- @param info Gitsigns.RepoInfo --- @return Gitsigns.Repo local function new(info) local self = setmetatable({}, { __index = M }) for k, v in pairs(info --[[@as table]]) do ---@diagnostic disable-next-line:no-unknown self[k] = v end self.username = self:command({ 'config', 'user.name' }, { ignore_error = true })[1] return self end --- @type table local repo_cache = setmetatable({}, { __mode = 'v' }) --- @async --- @param dir string --- @param gitdir? string --- @param toplevel? string --- @return Gitsigns.Repo? function M.get(dir, gitdir, toplevel) local info = M.get_info(dir, gitdir, toplevel) if not info then return end gitdir = info.gitdir if not repo_cache[gitdir] then repo_cache[gitdir] = { 1, new(info) } else repo_cache[gitdir][1] = repo_cache[gitdir][1] + 1 end return repo_cache[gitdir][2] end function M:unref() local gitdir = self.gitdir local repo = repo_cache[gitdir] if not repo then -- Already reclaimed by GC return end local refcount = repo[1] if refcount <= 1 then repo_cache[gitdir] = nil else repo_cache[gitdir][1] = refcount - 1 end end local has_cygpath = jit and jit.os == 'Windows' and vim.fn.executable('cygpath') == 1 --- @async --- @generic S --- @param path S --- @return S local function normalize_path(path) if path and has_cygpath and not uv.fs_stat(path) then -- If on windows and path isn't recognizable as a file, try passing it -- through cygpath --- @type string path = async.await(3, system, { 'cygpath', '-aw', path }).stdout end return path end --- @async --- @param gitdir? string --- @param head_str string --- @param cwd string --- @return string local function process_abbrev_head(gitdir, head_str, cwd) if not gitdir or head_str ~= 'HEAD' then return head_str end local short_sha = git_command({ 'rev-parse', '--short', 'HEAD' }, { ignore_error = true, cwd = cwd, })[1] or '' if log.debug_mode and short_sha ~= '' then short_sha = 'HEAD' end if util.path_exists(gitdir .. '/rebase-merge') or util.path_exists(gitdir .. '/rebase-apply') then return short_sha .. '(rebasing)' end return short_sha end --- @async --- @param cwd string --- @param gitdir? string --- @param worktree? string --- @return Gitsigns.RepoInfo? info, string? err function M.get_info(cwd, 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() -- 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({ gitdir and worktree and { '--git-dir', gitdir, '--work-tree', worktree, }, 'rev-parse', '--show-toplevel', has_abs_gd and '--absolute-git-dir' or '--git-dir', '--abbrev-ref', 'HEAD', }, { ignore_error = true, cwd = worktree or cwd, }) -- If the repo has no commits yet, rev-parse will fail. Ignore this error. if code > 0 and stderr and stderr:match("fatal: ambiguous argument '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 local toplevel_r = assert(normalize_path(stdout[1])) local gitdir_r = assert(normalize_path(stdout[2])) 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, assert(stdout[3]), cwd), 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' --- @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 info, relpath = unpack(vim.split(results[1], '\t')) local mode_bits, object_type, object_name = unpack(vim.split(info, '%s+')) 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({ '-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('^warning: could not open directory .*: No such file or directory') ) 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(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(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 --- @param mode_bits string --- @param object string --- @param path string --- @param add? boolean function M:update_index(mode_bits, object, path, add) self:command({ 'update-index', add and '--add', '--cacheinfo', ('%s,%s,%s'):format(mode_bits, object, path), }) end --- @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') return self:command({ 'hash-object', '-w', '--path', path, '--stdin' }, { stdin = text })[1] end --- @async --- @return string[] function M:rename_status() local out = self:command({ 'diff', '--name-status', '--find-renames', '--find-copies', '--cached', }) local ret = {} --- @type table for _, l in ipairs(out) do local parts = vim.split(l, '%s+') if #parts == 3 then local stat, orig_file, new_file = parts[1], parts[2], parts[3] if vim.startswith(stat, 'R') then ret[orig_file] = new_file end end end return ret end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/git/version.lua000066400000000000000000000045371476557270400235510ustar00rootroot00000000000000local 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 M = {} --- @type fun(cmd: string[], opts?: vim.SystemOpts): vim.SystemCompleted local asystem = async.awrap(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 ret = {} local parts = vim.split(version, '%.') ret.major = assert(tonumber(parts[1])) ret.minor = assert(tonumber(parts[2])) if parts[3] == 'GIT' then ret.patch = 0 else local patch_ver = vim.split(parts[3], '-') ret.patch = assert(tonumber(patch_ver[1])) end return ret 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(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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/highlight.lua000066400000000000000000000163301476557270400232420ustar00rootroot00000000000000local 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') > 0 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 '') return hl, { desc = ("Used for %s of '%s' %ssigns."):format(what, ty, sty), fg_factor = staged and 0.5 or nil, unpack(fallbacks), } 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.', }, }, { 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 table 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? number --- @param factor number --- @return number? local function cmul(x, factor) if not x or factor == 1 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)) return math.floor( math.floor(r * factor) * 2 ^ 16 + math.floor(g * factor) * 2 ^ 8 + math.floor(b * factor) ) end local function dprintf(fmt, ...) dprintf = require('gitsigns.debug.log').dprintf dprintf(fmt, ...) end --- @param hl string --- @param hldef Gitsigns.Hldef local function derive(hl, hldef) 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 = cmul(dh.fg, hldef.fg_factor), 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() 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) end end end end function M.setup() M.setup_highlights() api.nvim_create_autocmd('ColorScheme', { group = 'gitsigns', callback = M.setup_highlights, }) end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/hunks.lua000066400000000000000000000374561476557270400224370ustar00rootroot00000000000000local 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 + math.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(vim.split(line, '@@', { plain = true })[2]) -- diffKey: "-xx,n +yy" -- pre: {xx, n}, now: {yy} --- @type string[], string[] local pre, now = unpack(vim.tbl_map( --- @param s string --- @return string[] function(s) return vim.split(string.sub(s, 2), ',') end, vim.split(diffkey, ' ') )) 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 Gitsigns.Hunk.Hunk? --- @param min_lnum integer --- @param max_lnum integer --- @param untracked? boolean --- @return Gitsigns.Sign[] local function calc_signs(hunk, next, 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 and next.type == 'delete') and hunk.added.start + hunk.added.count - 1 == next.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 = math.max(1, min_lnum or 1) max_lnum = max_lnum or math.huge 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, string.format('@@ -%s,%s +%s,%s @@', 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) --- @type Gitsigns.StatusObj 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 direction == 'first' then return 1 elseif direction == 'last' then return #hunks elseif direction == 'next' then if 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 hunks[i + 1].added.start > lnum then return i + 1 elseif wrap then return 1 end end end elseif direction == 'prev' then if math.max(hunks[#hunks].vend) < lnum then return #hunks end for i = 1, #hunks do if lnum <= math.max(hunks[i].vend, 1) then if i > 1 and math.max(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, math.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, Gitsigns.HlMark[]][][] local removed, added = hunk.removed.lines, hunk.added.lines for _, spec in ipairs({ { sym = '-', lines = removed, hl = 'GitSignsDeletePreview' }, { sym = '+', lines = added, hl = 'GitSignsAddPreview' }, }) do for _, l in ipairs(spec.lines) do if fileformat == 'dos' then l = l:gsub('\r$', '') --[[@as string]] end hls[#hls + 1] = { { spec.sym .. l, { { hl_group = spec.hl, end_row = 1, -- Highlight whole line }, }, }, } 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] table.insert(hls[i][1][2], { 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] table.insert(hls[i][1][2], { hl_group = 'GitSignsAddInline', start_col = region[3], end_col = region[4], }) end end return hls end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/manager.lua000066400000000000000000000303111476557270400227000ustar00rootroot00000000000000local 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_by_id = require('gitsigns.debounce').throttle_by_id local cache = require('gitsigns.cache').cache local config = require('gitsigns.config').config local api = vim.api local signs_normal --- @type Gitsigns.Signs local signs_staged --- @type Gitsigns.Signs --- @class gitsigns.manager local M = {} --- @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 for i, hunk in ipairs(hunks or {}) 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 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 on_lines_blame(bcache.blame, first, last_orig, last_new) 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_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 = hunk.added.lines[pos] local removed_line = 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 --- @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_by_id(function(bufnr) local bcache = cache[bufnr] if not bcache or not bcache:schedule() then return end bcache.update_lock = true 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 bcache.update_lock = nil end, true) --- @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 end end function M.reset_signs() -- Remove all signs if signs_normal then signs_normal:reset() end if signs_staged then signs_staged:reset() end end --- @param _cb 'win' --- @param _winid integer --- @param bufnr integer --- @param topline integer --- @param botline_guess integer --- @return false? local function on_win(_cb, _winid, 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 --- @param _cb 'line' --- @param _winid integer --- @param bufnr integer --- @param row integer local function on_line(_cb, _winid, bufnr, row) apply_word_diff(bufnr, row) 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 = on_win, on_line = on_line, }) signs_normal = Signs.new(config.signs) if config.signs_staged_enable then signs_staged = Signs.new(config.signs_staged, 'staged') end M.update_debounced = debounce_trailing(config.update_debounce, async.create(1, M.update)) end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/message.lua000066400000000000000000000010611476557270400227120ustar00rootroot00000000000000local 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(...), vim.log.levels.ERROR, { title = 'gitsigns' }) end) --- @type fun(fmt: string, ...: string) M.error_once = vim.schedule_wrap(function(fmt, ...) vim.notify_once(fmt:format(...), vim.log.levels.ERROR, { title = 'gitsigns' }) end) return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/nav.lua000066400000000000000000000077551476557270400220720ustar00rootroot00000000000000local 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 --- @field wrap boolean --- @field foldopen boolean --- @field navigation_message boolean --- @field greedy boolean --- @field preview boolean --- @field count integer --- @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] 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*') api.nvim_win_set_cursor(0, { line, col }) return end line = forwards and hunks[index].added.start or hunks[index].vend end -- 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*') 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.preview') if opts.preview or Popup.is_open('hunk') ~= nil 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/popup.lua000066400000000000000000000172461476557270400224450ustar00rootroot00000000000000local util = require('gitsigns.util') local 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 string local function expand_height(winid, nlines, border) 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 --- Each element represents a multi-line segment --- @alias Gitsigns.LineSpec [string, string|Gitsigns.HlMark[]][][] --- @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 or 0) + 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 = part[1], part[2] 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] = { 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 lines string[] --- @param highlights Gitsigns.HlMark[] --- @return integer bufnr local function create_buf(lines, highlights) local ts = vim.bo.tabstop local bufnr = api.nvim_create_buf(false, true) assert(bufnr, 'Failed to create buffer') vim.bo[bufnr].bufhidden = 'wipe' -- 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 -- Set tabstop before calculating the buffer width so that the correct width -- is calculated vim.bo[bufnr].tabstop = ts 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, 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 return bufnr end --- @param bufnr integer --- @param opts table --- @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) then api.nvim_win_set_config(winid, opts1) end end, }) return winid end --- @param lines_spec Gitsigns.LineSpec --- @param opts table --- @param id? string --- @return integer winid, integer bufnr function M.create(lines_spec, opts, id) local lines, highlights = partition_linesspec(lines_spec) local bufnr = create_buf(lines, highlights) local winid = create_win(bufnr, opts, id) return winid, bufnr 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 --- @param fmt Gitsigns.LineSpec --- @param info table --- @return Gitsigns.LineSpec function M.lines_format(fmt, info) local ret = vim.deepcopy(fmt) for _, line in ipairs(ret) do for _, s in ipairs(line) do s[1] = util.expand_format(s[1], info) end end return ret end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/preview.lua000066400000000000000000000171501476557270400227550ustar00rootroot00000000000000local 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 local ok, data = pcall(api.nvim_eval_statusline, statuscol, { winid = win, use_statuscol_lnum = lnum, highlights = true, }) if ok then 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(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 = 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', }) vim.bo[pbufnr].filetype = vim.bo[bufnr].filetype vim.bo[pbufnr].bufhidden = 'wipe' vim.wo[pwinid].scrolloff = 0 api.nvim_win_call(pwinid, function() -- Expand folds vim.cmd('normal! ' .. 'zR') -- Navigate to hunk vim.cmd('normal! ' .. tostring(hunk.removed.start) .. 'gg') vim.cmd('normal! ' .. vim.api.nvim_replace_termcodes('z', true, false, true)) end) local last_lnum = api.nvim_buf_line_count(bufnr) -- Apply highlights for i = hunk.removed.start, hunk.removed.start + hunk.removed.count do api.nvim_buf_set_extmark(pbufnr, nsd, i - 1, 0, { hl_group = 'GitSignsDeleteVirtLn', hl_eol = true, end_row = i, strict = i == last_lnum, 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 of ', 'Title' } }, unpack(Hunks.linespec_for_hunk(hunk, vim.bo[bufnr].fileformat)), } local lines_spec = popup.lines_format(preview_linespec, { hunk_no = index, num_hunks = #cache[bufnr].hunks, }) popup.create(lines_spec, config.preview_config, 'hunk') end) --- Preview the hunk at the cursor position inline in the buffer. --- @async 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' }, { 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 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/qflist.lua000066400000000000000000000063511476557270400225770ustar00rootroot00000000000000local 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 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 i, hunk in ipairs(hunks) do 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 = string.format('Lines %d-%d (%d/%d)', hunk.added.start, hunk.vend, i, #hunks), } end end --- @async --- @param target 'all'|'attached'|integer|nil --- @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 if not cache[bufnr] then return end hunks_to_qflist(bufnr, cache[bufnr].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(vim.loop.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 = vim.loop.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) async.schedule() 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 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/repeat.lua000066400000000000000000000011311476557270400225440ustar00rootroot00000000000000local 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/signs.lua000066400000000000000000000071321476557270400224160ustar00rootroot00000000000000local api = vim.api local config = require('gitsigns.config').config --- @class Gitsigns.Sign --- @field type Gitsigns.SignType --- @field count? integer --- @field lnum integer --- @class Gitsigns.Signs --- @field hls table --- @field name string --- @field group string --- @field config table --- @field ns integer local M = {} --- @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) else 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 then -- Don't place signs if it won't show anything return end for _, s in ipairs(signs) do if (not filter or filter(s.lnum)) and not self:contains(bufnr, s.lnum) then local cs = self.config[s.type] local text = cs.text if config.signcolumn and cs.show_count and s.count then local count = s.count local cc = config.count_chars local count_char = cc[count] or cc['+'] or '' text = cs.text .. count_char end local hls = self.hls[s.type] local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, self.ns, s.lnum - 1, 0, { id = s.lnum, sign_text = config.signcolumn and text or '', priority = config.sign_priority, sign_hl_group = hls.hl, number_hl_group = config.numhl and hls.numhl or nil, line_hl_group = config.linehl and hls.linehl or nil, cursorline_hl_group = config.culhl and hls.culhl or nil, }) if not ok and config.debug_mode then vim.schedule(function() error(table.concat({ string.format('Error placing extmark on line %d', s.lnum), 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 -- local function capitalise_word(x: string): string -- return x:sub(1, 1):upper()..x:sub(2) -- end function M.new(cfg, name) local __FUNC__ = 'signs.init' -- Add when config.signs.*.[hl,numhl,linehl] are removed -- for _, t in ipairs { -- 'add', -- 'change', -- 'delete', -- 'topdelete', -- 'changedelete', -- 'untracked', -- } do -- local hl = string.format('GitSigns%s%s', name, capitalise_word(t)) -- obj.hls[t] = { -- hl = hl, -- numhl = hl..'Nr', -- linehl = hl..'Ln', -- } -- end local self = setmetatable({}, { __index = M }) self.config = cfg self.hls = name == 'staged' and config.signs_staged or config.signs self.group = 'gitsigns_signs_' .. (name or '') self.ns = api.nvim_create_namespace(self.group) return self end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/status.lua000066400000000000000000000022431476557270400226140ustar00rootroot00000000000000local 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/system.lua000066400000000000000000000007211476557270400226140ustar00rootroot00000000000000local log = require('gitsigns.debug.log') local M = {} -- compat module contains 0.11 fixes. local system = vim.fn.has('nvim-0.11') == 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) local __FUNC__ = 'run_job' log.dprint(unpack(cmd)) return system(cmd, opts, on_exit) end return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/system/000077500000000000000000000000001476557270400221115ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/lua/gitsigns/system/compat.lua000066400000000000000000000202521476557270400241000ustar00rootroot00000000000000local uv = vim.uv or vim.loop --- @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) 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 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? uv.read_start.callback|false --- @param text? boolean --- @return uv.uv_stream_t? pipe --- @return uv.read_start.callback? handler --- @return string[]? data local function setup_output(output, text) if output == false then return end local bucket --- @type string[]? local handler --- @type uv.read_start.callback 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)) --- @type uv.read_start.callback 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 clear_env then return env end --- @type table env = vim.tbl_extend('force', base_env(), env or {}) 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 --- @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) local handle, pid_or_err = uv.spawn(cmd, opts, on_exit) if not handle then on_error() error(pid_or_err) 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 = assert(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) local __FUNC__ = 'run_job' vim.validate({ cmd = { cmd, 'table' }, opts = { opts, 'table', true }, on_exit = { on_exit, 'function', true }, }) opts = opts or {} local stdout, stdout_handler, stdout_data = setup_output(opts.stdout) local stderr, stderr_handler, stderr_data = setup_output(opts.stderr) 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 state.handle, state.pid = spawn(cmd[1], { args = vim.list_slice(cmd, 2), stdio = { stdin, stdout, stderr }, cwd = opts.cwd, --- @diagnostic disable-next-line:assign-type-mismatch 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: invisible obj:_timeout() end end) end return obj end return system lewis6991-gitsigns.nvim-7010000/lua/gitsigns/test.lua000066400000000000000000000014341476557270400222510ustar00rootroot00000000000000local 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 lewis6991-gitsigns.nvim-7010000/lua/gitsigns/util.lua000066400000000000000000000213501476557270400222460ustar00rootroot00000000000000local M = {} function M.path_exists(path) return vim.loop.fs_stat(path) and true or false end local jit_os --- @type string if jit then jit_os = jit.os:lower() end local is_unix = false if jit_os then is_unix = jit_os == 'linux' or jit_os == 'osx' or jit_os == 'bsd' else local binfmt = package.cpath:match('%p[\\|/]?%p(%a+)') is_unix = binfmt ~= 'dll' end --- @param file string --- @return string function M.dirname(file) return file:match(string.format('^(.+)%s[^%s]+', M.path_sep, M.path_sep)) end --- @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 bom .. x 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) 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 --- @return string function M.tmpname() if is_unix then return os.tmpname() end return vim.fn.tempname() 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) else vim.api.nvim__buf_redraw_range(opts.buf, 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 if xs[i]: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 xs[i] = xs[i]: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 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) --- @diagnostic disable-next-line:param-type-mismatch 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 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 return M lewis6991-gitsigns.nvim-7010000/lua/gitsigns/watcher.lua000066400000000000000000000107121476557270400227260ustar00rootroot00000000000000local api = vim.api local uv = vim.loop local async = require('gitsigns.async') local log = require('gitsigns.debug.log') local util = require('gitsigns.util') local Status = require('gitsigns.status') local cache = require('gitsigns.cache').cache local config = require('gitsigns.config').config local throttle_by_id = require('gitsigns.debounce').throttle_by_id local debounce_trailing = require('gitsigns.debounce').debounce_trailing local dprint = log.dprint local dprintf = log.dprintf --- @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 git_obj.orig_relpath = assert(git_obj.orig_relpath or old_relpath) local new_name = git_obj.repo:rename_status()[git_obj.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 = git_obj.repo.toplevel .. util.path_sep .. 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 = git_obj.repo.toplevel .. util.path_sep .. 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 watcher_handler0(bufnr) local __FUNC__ = 'watcher_handler' local bcache = cache[bufnr] if not bcache then return end -- Avoid cache hit for detached buffer -- ref: https://github.com/lewis6991/gitsigns.nvim/issues/956 if not bcache:schedule() then dprint('buffer invalid (1)') return end local git_obj = bcache.git_obj git_obj.repo:update_abbrev_head() if not bcache:schedule() then dprint('buffer invalid (2)') return end Status:update(bufnr, { head = git_obj.repo.abbrev_head }) local was_tracked = git_obj.object_name ~= nil local old_relpath = git_obj.relpath git_obj:refresh() if not bcache:schedule() then dprint('buffer invalid (3)') 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 (4)') return end end cache[bufnr]:invalidate(true) require('gitsigns.manager').update(bufnr) end --- Debounce to: --- - wait for all changes to the gitdir to complete. --- 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 watcher_handler = debounce_trailing(200, async.create(1, throttle_by_id(watcher_handler0, true)), 1) --- vim.inspect but on one line --- @param x any --- @return string local function inspect(x) return vim.inspect(x, { indent = '', newline = ' ' }) end local M = {} --- @param bufnr integer --- @param gitdir string --- @return uv.uv_fs_event_t function M.watch_gitdir(bufnr, gitdir) dprintf('Watching git dir') local w = assert(uv.new_fs_event()) w:start(gitdir, {}, function(err, filename, events) local __FUNC__ = 'watcher_cb' if err then 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 dprintf("Git dir update: '%s' %s", filename, inspect(events)) watcher_handler(bufnr) end) return w end return M lewis6991-gitsigns.nvim-7010000/release-please-config.json000066400000000000000000000004451476557270400232050ustar00rootroot00000000000000{ "$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" ] } } } lewis6991-gitsigns.nvim-7010000/test/000077500000000000000000000000001476557270400171345ustar00rootroot00000000000000lewis6991-gitsigns.nvim-7010000/test/actions_spec.lua000066400000000000000000000220151476557270400223110ustar00rootroot00000000000000local 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 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 = 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) lewis6991-gitsigns.nvim-7010000/test/gitdir_watcher_spec.lua000066400000000000000000000122061476557270400236510ustar00rootroot00000000000000local 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 match_dag = helpers.match_dag 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) match_debug_messages({ 'attach(1): Attaching (trigger=BufReadPost)', np( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), np('run_job: git .* config user.name'), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file)), n('watch_gitdir(1): Watching git dir'), np('run_job: 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_dag({ "watcher_cb(1): Git dir update: 'index.lock' { rename = true }", "watcher_cb(1): Git dir update: 'index' { rename = true }", "watcher_cb(1): Git dir update: 'index' { rename = true }", }) match_debug_messages({ np( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file)), np('run_job: git .* diff %-%-name%-status .* %-%-cached'), n('handle_moved(1): File moved to dummy.txt2'), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file2)), np('handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt to .*/dummy.txt2'), np('run_job: 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_dag({ "watcher_cb(1): Git dir update: 'index.lock' { rename = true }", "watcher_cb(1): Git dir update: 'index' { rename = true }", "watcher_cb(1): Git dir update: 'index' { rename = true }", }) match_debug_messages({ p( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file2)), np('run_job: git .* diff %-%-name%-status .* %-%-cached'), n('handle_moved(1): File moved to dummy.txt3'), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file3)), np('handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt2 to .*/dummy.txt3'), np('run_job: git .* show .*'), }) eq({ [1] = test_file3 }, get_bufs()) command('Gitsigns clear_debug') git('mv', test_file3, test_file) match_dag({ "watcher_cb(1): Git dir update: 'index.lock' { rename = true }", "watcher_cb(1): Git dir update: 'index' { rename = true }", "watcher_cb(1): Git dir update: 'index' { rename = true }", }) match_debug_messages({ p( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file3)), np('run_job: git .* diff %-%-name%-status .* %-%-cached'), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file)), n('handle_moved(1): Moved file reset'), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file)), np('handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt3 to .*/dummy.txt'), np('run_job: git .* show .*'), }) eq({ [1] = test_file }, get_bufs()) end) it('can debounce and throttle updates per buffer', function() helpers.cleanup() system({ 'mkdir', helpers.scratch }) helpers.git_init() 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) end) lewis6991-gitsigns.nvim-7010000/test/gitsigns_spec.lua000066400000000000000000000551351476557270400225110ustar00rootroot00000000000000local Screen = require('nvim-test.screen') local helpers = require('test.gs_helpers') local clear = helpers.clear local command = helpers.api.nvim_command local feed = helpers.feed local insert = helpers.insert local exec_lua = helpers.exec_lua local split = vim.split local get_buf_var = helpers.api.nvim_buf_get_var local fn = helpers.fn local system = fn.system local expectf = helpers.expectf local write_to_file = helpers.write_to_file local edit = helpers.edit local cleanup = helpers.cleanup local test_file = helpers.test_file local git = helpers.git local scratch = helpers.scratch local newfile = helpers.newfile local match_dag = helpers.match_dag local match_lines = helpers.match_lines local n, p, np = helpers.n, helpers.p, helpers.np local match_debug_messages = helpers.match_debug_messages local setup_gitsigns = helpers.setup_gitsigns local setup_test_repo = helpers.setup_test_repo local test_config = helpers.test_config local check = helpers.check local eq = helpers.eq helpers.env() 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.NvimDarkCyan }, [3] = { foreground = Screen.colors.NvimDarkGreen }, [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 }, [10] = { foreground = Screen.colors.Red }, [11] = { foreground = Screen.colors.NvimDarkRed, background = Screen.colors.WebGray }, [12] = { foreground = Screen.colors.NvimDarkCyan, background = Screen.colors.WebGray }, } -- Use the classic vim colorscheme, not the new defaults in nvim >= 0.10 if fn.has('nvim-0.10') > 0 then command('colorscheme vim') else default_attrs[2] = { background = Screen.colors.LightMagenta } default_attrs[3] = { background = Screen.colors.LightBlue } 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) 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(1): Attaching (trigger=BufReadPost)', p('run_job: git .* config user.name'), p( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), p( 'run_job: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard %-%-eol ' .. vim.pesc(test_file) ), 'watch_gitdir(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(1): Attaching (trigger=BufReadPost)', np( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), n('new: Not in git repo'), n('attach(1): Empty git obj'), }) command('Gitsigns clear_debug') insert('line') command('write') match_debug_messages({ n('attach(1): Attaching (trigger=BufWritePost)'), np( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), n('new: Not in git repo'), n('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(helpers.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(1): Attaching (trigger=BufReadPost)', n('new: In git dir'), n('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(1): Attaching (trigger=BufReadPost)', np( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), np('run_job: git .* config user.name'), np('run_job: git .* ls%-files .*/dummy_ignored.txt'), n('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(1): Attaching (trigger=BufNewFile)', np( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), np('run_job: git .* config user.name'), np( 'run_job: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard %-%-eol ' .. vim.pesc(newfile) ), n('attach(1): Not a file'), }) 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(1): Attaching (trigger=BufNewFile)', n('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(2): Attaching (trigger=BufReadPost)', n('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) -- 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(1): Attaching (trigger=BufReadPost)', np( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), np('run_job: git .* rev%-parse %-%-short HEAD'), np('run_job: git .* config user.name'), np('run_job: git .* %-%-git%-dir .* %-%-stage %-%-others %-%-exclude%-standard %-%-eol.*'), n('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(1): Attaching (trigger=BufNewFile)', np( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), np('run_job: git .* config user.name'), np('run_job: git .* ls%-files .*'), n('attach(1): Not a file'), }) command('write') local messages = { 'attach(1): Attaching (trigger=BufWritePost)', np( 'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD' ), np('run_job: git .* ls%-files .*'), n('watch_gitdir(1): Watching git dir'), } if not internal_diff then table.insert(messages, np('run_job: git .* diff .* /tmp/lua_.* /tmp/lua_.*')) 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 }, }) exec_lua('require("gitsigns.actions").stage_hunk()') 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 .. '/*') screen:expect({ messages = { { kind = 'quickfix', content = { { '(1 of 2): hello ben' } }, }, }, }) match_debug_messages({ 'attach_autocmd(2): Attaching is disabled', n('attach_autocmd(3): Attaching is disabled'), n('attach_autocmd(4): Attaching is disabled'), n('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) end) describe('gitsigns', 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) end) lewis6991-gitsigns.nvim-7010000/test/gs_helpers.lua000066400000000000000000000210021476557270400217650ustar00rootroot00000000000000local 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() 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.cleanup() system({ 'mkdir', M.scratch }) M.git_init() 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 --- @type boolean, boolean? 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 unmatched_msg = table.concat( --- @param v any --- @return string vim.tbl_map(function(v) return (' - %s'):format(v.text or v) end, spec), '\n' ) 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 patterns:\n%s\nwith:\n%s'):format(unmatched_msg, 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()") 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 --- @param config? table --- @param on_attach? boolean function M.setup_gitsigns(config, on_attach) exec_lua(function(path, config0, on_attach0) package.path = path 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, package.path, 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] = 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 lewis6991-gitsigns.nvim-7010000/test/highlights_spec.lua000066400000000000000000000056151476557270400230120ustar00rootroot00000000000000local Screen = require('nvim-test.screen') local helpers = require('test.gs_helpers') local clear = helpers.clear local exec_lua = helpers.exec_lua 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 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.signs.add.hl = nil config.signs.change.hl = nil config.signs.delete.hl = nil config.signs.changedelete.hl = nil config.signs.topdelete.hl = nil 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.signs.add.hl = nil config.signs.change.hl = nil config.signs.delete.hl = nil config.signs.changedelete.hl = nil config.signs.topdelete.hl = nil config.linehl = true setup_gitsigns(config) end) end) lewis6991-gitsigns.nvim-7010000/test/hunk_spec.lua000066400000000000000000000053061476557270400216220ustar00rootroot00000000000000local 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) for i, hunk in ipairs(hunks) do if hunk[1] then hunks[i] = { added = { count = hunk[4], start = hunk[5] }, removed = { count = hunk[2], start = hunk[3] }, type = hunk[1], } end end --- @type Gitsigns.Sign[] local signs = exec_lua( --- @param hunks0 Gitsigns.Hunk.Hunk[] --- @return Gitsigns.Sign[] function(hunks0) local Hunks = require('gitsigns.hunks') local signs = {} for i, hunk in ipairs(hunks0) do local prev_hunk, next_hunk = hunks0[i - 1], hunks0[i + 1] vim.list_extend(signs, Hunks.calc_signs(prev_hunk, hunk, next_hunk)) end return signs end, hunks ) 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)