diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f85875dc8..f32d4804d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -195,7 +195,7 @@ jobs: uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9 with: # If you change this, make sure to also update scripts/golangci-lint-shim.sh - version: v2.4.0 + version: v2.12.2 upload-coverage: # List all jobs that produce coverage files needs: [unit-tests, integration-tests] diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 611ad553d..56466a07c 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v7 - name: Generate Sponsors 💖 - uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1.6.0 + uses: JamesIves/github-sponsors-readme-action@02650b8cd445fc16dfef73195f9c406dce041623 # v1.6.1 with: token: ${{ secrets.SPONSORS_TOKEN }} file: "README.md" diff --git a/.golangci.yml b/.golangci.yml index 5ed7fb32d..e6a2f37ab 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -99,8 +99,6 @@ linters: generated: lax presets: - comments - - common-false-positives - - legacy - std-error-handling paths: - vendor/ diff --git a/AGENTS.md b/AGENTS.md index d621604be..9c3829af1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,11 +82,26 @@ while still being meaningful and self-contained. excuse bundling it in. Before committing, review your diff and split out any hunk that is behavior-preserving (an extraction, a rename, a move) into a preceding commit, by staging hunks or resetting and recommitting in order. +- **A preparatory refactor is a new commit only when it prepares something + new.** Before adding one, find the commit that introduced the code you are + about to restructure. If that commit is on this branch, the refactor is a + `fixup!` for it rather than a commit of its own: a branch must never contain + a commit whose code a later commit on the same branch tidies up. A prep + refactor earns a commit of its own only when the shape it corrects came from + before the branch. This holds across a branch stack too — if the commit that + introduced the code is in an earlier branch of the stack, the fixup belongs + there, and the branches above it get replayed. The one exception is when + fixing it there turns out to be unreasonably difficult; ask me what to do + rather than deciding to leave the repair at the tip. - **Do not use conventional commits** (no `feat:`/`fix:`/`chore:` prefixes). Match the plain English imperative style of the existing history. - **Wrap message body to 72 characters**. The subject is allowed to go up to 80 characters, or even a little more if needed to convey a good single-line summary; the body should be wrapped at 72 exactly, no more, no less. +- **End every commit message with the `Co-authored-by:` trailer** naming the + model that wrote it, exactly as your harness instructions spell it. Nothing + in `just check` catches a missing one, so it has to be part of writing the + message rather than something to notice afterwards. ## Iterate with `fixup!` commits @@ -105,6 +120,20 @@ separate, reviewable commit that the user decides when to fold in. A bare `--amend` rewrites the commit on the spot and skips that checkpoint. Don't treat "I'm only touching the tip commit" as an exception. +Always use `fixup!` or `amend!` commits, never amend changes directly, even if +you naturally would because "the branch isn't pushed yet". The user always wants +to review what you changed, so make this transparent; no exceptions. + +**When the tip is the wrong place for a fixup, insert it mid-branch.** +Committing a fixup at the tip of the branch only works while the code it +touches still looks the same there; once later commits have rewritten that +code — or the target has since been split — the fixup won't apply, and +rewriting the later commits to accommodate it defeats the point. Check out the +target, make the change, `git commit --fixup=`, then +`git rebase --onto ` to replay the rest of the +branch. The fixup stays a separate, reviewable commit; only its position +changes. + If the changes don't map cleanly onto existing commits — say they cut across several of them, or restructure something at a different layer than any existing commit naturally owns — stop and ask the user how to diff --git a/cpu.out b/cpu.out deleted file mode 100644 index 24b4b40a4..000000000 Binary files a/cpu.out and /dev/null differ diff --git a/docs-master/Custom_DiffRenderers.md b/docs-master/Custom_DiffRenderers.md index 1a6b1d5ce..509f42ebf 100644 --- a/docs-master/Custom_DiffRenderers.md +++ b/docs-master/Custom_DiffRenderers.md @@ -27,7 +27,7 @@ Fields only for `extDiff`: Fields only for `rawGit`: -- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`) +- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`), as an array of strings. Here's an example for a multi-renderer setup: @@ -40,7 +40,7 @@ git: - type: extDiff command: difft --color=always --context={{diffContext}} - type: rawGit - args: --color-words + args: [--color-words] name: color-words - type: rawGit # git's default diff name: default diff --git a/docs-master/Searching.md b/docs-master/Searching.md index 589831c55..4cba775df 100644 --- a/docs-master/Searching.md +++ b/docs-master/Searching.md @@ -4,10 +4,14 @@ Depending on the currently focused view, hitting '/' will bring up a filter or search prompt. When filtering, the contents of the view will be filtered down to only those lines which match the query string. When searching, the contents of the view are not filtered, but matching lines are highlighted and you can iterate through matches with `n`/`N`. -We intend to support filtering for the files view soon, but at the moment it uses searching. We intend to continue using search for the commits view because you typically care about the commits that come before/after a matching commit. +In the commits view we don't filter, but search; this is deliberate because you typically care about the commits that come before/after a matching commit. If you would like both filtering and searching to be enabled on a given view, please raise an issue for this. +## Menu filtering + +The keybindings (`?`) and recent repositories menus can be filtered simply by typing. The filter field appears at the bottom of the menu while you type; there is no need to press `/` or confirm the filter before navigating the results. + ## Filtering files by status You can filter the files view to only show staged/unstaged files by pressing `` in the files view. diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index 7d95e72dd..0f01dea7c 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -280,7 +280,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` w `` | New worktree | | | `` M `` | Merge in met huidige checked out branch | View options for merging the selected item into the current branch (regular merge, squash merge) | | `` r `` | Rebase branch | Rebase de uitgecheckte branch bovenop de geselecteerde branch. | -| `` d `` | Verwijderen | Delete the remote branch from the remote. | +| `` d `` | Verwijderen | Verwijder de remote branch van de remote. | | `` u `` | Instellen als upstream | Stel in als upstream van uitgecheckte branch | | `` s `` | Sort order | | | `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. | @@ -295,7 +295,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` `` | Bekijk branches | | | `` n `` | Voeg een nieuwe remote toe | | -| `` d `` | Verwijderen | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. | +| `` d `` | Verwijderen | Verwijder de geselecteerde remote. Locale branches die een branch tracken van de remote worden niet aangepast. | | `` e `` | Edit | Wijzig remote | | `` f `` | Fetch | Fetch remote | | `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index 1706a627e..0e5debfdf 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -9,28 +9,28 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 切換到最近使用的版本庫 | | | `` , K, (fn+up/shift+k) `` | 向上捲動主面板 | | | `` , J, (fn+down/shift+j) `` | 向下捲動主面板 | | -| `` @ `` | 開啟命令記錄選單 | View options for the command log e.g. show/hide the command log and focus the command log. | +| `` @ `` | 開啟命令記錄選單 | 檢視命令日誌的選項,例如顯示/隱藏命令日誌以及聚焦命令日誌。 | | `` P `` | 推送 | 推送到遠端。如果沒有設定遠端,會開啟設定視窗。 | | `` p `` | 拉取 | 從遠端同步當前分支。如果沒有設定遠端,會開啟設定視窗。 | -| `` ) `` | Increase rename similarity threshold | Increase the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | -| `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | -| `` } `` | 增加差異檢視中顯示變更周圍上下文的大小 | Increase the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | -| `` { `` | 減小差異檢視中顯示變更周圍上下文的大小 | Decrease the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | -| `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. | +| `` ) `` | 提高重新命名相似度閾值 | 提高將刪除和新增對視為重新命名所需的相似度閾值。

預設值可在設定檔中透過鍵 'git.renameSimilarityThreshold' 更改。 | +| `` ( `` | 降低重新命名相似度閾值 | 降低將刪除和新增對視為重新命名所需的相似度閾值。

預設值可在設定檔中透過鍵 'git.renameSimilarityThreshold' 更改。 | +| `` } `` | 增加差異檢視中顯示變更周圍上下文的大小 | 增加差異檢視中變更周圍顯示的上下文量。

預設值可在設定檔中透過鍵 'git.diffContextSize' 更改。 | +| `` { `` | 減小差異檢視中顯示變更周圍上下文的大小 | 減少差異檢視中變更周圍顯示的上下文量。

預設值可在設定檔中透過鍵 'git.diffContextSize' 更改。 | +| `` : `` | 執行 Shell 命令 | 調出可輸入shell命令執行的提示符。 | | `` `` | 檢視自訂補丁選項 | | -| `` m `` | 查看合併/變基選項 | View options to abort/continue/skip the current merge/rebase. | -| `` R `` | 重新整理 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. | +| `` m `` | 查看合併/變基選項 | 檢視目前合併或變基的中止、繼續、跳過選項。 | +| `` R `` | 重新整理 | 重新整理Git狀態(即在背景執行`git status`、`git branch`等命令以更新面板內容)。此操作不會執行`git fetch`。 | | `` + `` | 下一個螢幕模式(常規/半螢幕/全螢幕) | | | `` _ `` | 上一個螢幕模式 | | -| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. | -| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. | +| `` \| `` | 切換差異渲染器 | 選擇已設定的差異渲染器清單中的下一個渲染器。 | +| `` \ `` | 切換差異渲染器(反向) | 選擇已設定的差異渲染器清單中的上一個渲染器。 | | `` `` | 取消 | | | `` ? `` | 開啟選單 | | -| `` `` | 檢視篩選路徑選項 | View options for filtering the commit log, so that only commits matching the filter are shown. | -| `` W, `` | 開啟差異比較選單 | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. | +| `` `` | 檢視篩選路徑選項 | 檢視用於過濾提交日誌的選項,以便僅顯示與過濾器匹配的提交。 | +| `` W, `` | 開啟差異比較選單 | 檢視與比較兩個引用相關的選項,例如與選定的 ref 進行比較,輸入要比較的 ref,然後反轉比較方向。 | | `` q, `` | 結束 | | -| `` `` | Suspend the application | | -| `` `` | 切換是否在差異檢視中顯示空格變更 | Toggle whether or not whitespace changes are shown in the diff view.

The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. | +| `` `` | 掛起應用程式 | | +| `` `` | 切換是否在差異檢視中顯示空格變更 | 切換是否在差異檢視中顯示空白字元更改。

預設值可在設定檔中透過鍵 'git.ignoreWhitespaceInDiffView' 更改。 | | `` `` | 編輯設定檔案 | 使用外部編輯器開啟 | | `` z `` | 復原 | 將使用 reflog 確任 git 指令以復原。這不包括工作區更改;只考慮提交。 | | `` Z `` | 取消復原 | 將使用 reflog 確任 git 指令以重作。這不包括工作區更改;只考慮提交。 | @@ -44,21 +44,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` <, `` | 捲動到頂部 | | | `` >, `` | 捲動到底部 | | | `` v `` | 切換拖曳選擇 | | -| `` `` | Range select down | | -| `` `` | Range select up | | +| `` `` | 向下擴充套件選擇範圍 | | +| `` `` | 向上擴充套件選擇範圍 | | | `` / `` | 搜尋 | | | `` H `` | 向左捲動 | | | `` L `` | 向右捲動 | | | `` ] `` | 下一個索引標籤 | | | `` [ `` | 上一個索引標籤 | | -## Input prompt - -| Key | Action | Info | -|-----|--------|-------------| -| `` `` | 確認 | | -| `` `` | 關閉/取消 | | - ## 主面板 (補丁生成) | Key | Action | Info | @@ -66,12 +59,12 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | | `` v `` | 切換拖曳選擇 | | -| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | +| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 | | `` `` | 複製所選文本至剪貼簿 | | | `` o `` | 開啟檔案 | 使用預設軟體開啟 | | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | | `` `` | 向 (或從) 補丁中添加/刪除行 | | -| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. | +| `` d `` | 從提交中移除行 | 從本次提交中移除所選行。此操作會在背景執行互動式變基,因此如果後續提交也修改了這些行,您可能會遇到合併衝突。 | | `` `` | 退出自訂補丁建立器 | | | `` / `` | 搜尋 | | @@ -81,8 +74,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` (fn+up) `` | 向下捲動 | | | `` (fn+down) `` | 向上捲動 | | -| `` `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). | -| `` `` | Exit back to side panel | | +| `` `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 | +| `` `` | 退出回到側邊面板 | | | `` / `` | 搜尋 | | ## 主面板(合併) @@ -90,15 +83,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| | `` `` | 挑選程式碼片段 | | -| `` b `` | Pick both hunks | | +| `` b `` | 選取兩個區塊 | | | `` , k `` | 選擇上一段 | | | `` , j `` | 選擇下一段 | | | `` , h `` | 選擇上一個衝突 | | | `` , l `` | 選擇下一個衝突 | | -| `` z `` | 復原 | Undo last merge conflict resolution. | +| `` z `` | 復原 | 撤消上次合併衝突解決。 | | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | | `` o `` | 開啟檔案 | 使用預設軟體開啟 | -| `` M `` | View merge conflict options | View options for resolving merge conflicts. | +| `` M `` | 檢視合併衝突選項 | 檢視用於解決合併衝突的選項。 | | `` `` | 返回檔案面板 | | ## 主面板(預存) @@ -108,19 +101,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | | `` v `` | 切換拖曳選擇 | | -| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | +| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 | | `` `` | 複製所選文本至剪貼簿 | | | `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | -| `` d `` | 刪除變更 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | +| `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 | | `` o `` | 開啟檔案 | 使用預設軟體開啟 | | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | | `` `` | 返回檔案面板 | | -| `` `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). | -| `` E `` | 編輯程式碼塊 | Edit selected hunk in external editor. | +| `` `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 | +| `` E `` | 編輯程式碼塊 | 在外部編輯器中編輯選中的程式碼塊。 | | `` c `` | 提交變更 | 提交暫存區變更 | | `` w `` | 沒有預提交 hook 就提交更改 | | | `` C `` | 使用 git 編輯器提交變更 | | -| `` `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: | +| `` `` | 尋找 fixup 的基礎提交 | 找出目前變更所依據的提交,以便 amend/fixup。這樣不必逐一檢視分支中的提交來找出要 amend/fixup 的提交。請見文件: | | `` / `` | 搜尋 | | ## 功能表 @@ -135,19 +128,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| -| `` `` | Copy abbreviated commit hash to clipboard | | -| `` `` | 檢出 | Checkout the selected commit as a detached HEAD. | -| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | +| `` `` | 複製縮略提交雜湊值到剪貼簿 | | +| `` `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 | +| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 | | `` o `` | 在瀏覽器中開啟提交 | | | `` n `` | 從提交建立新分支 | | -| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | -| `` w `` | New worktree | | -| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. | -| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | +| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。

請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 | +| `` w `` | 新建工作樹 | | +| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 | +| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `` 來取消選擇。 | | `` `` | 重設選定的揀選 (複製) 提交 | | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` * `` | Select commits of current branch | | -| `` 0 `` | Focus main view | | +| `` * `` | 選擇目前分支的提交 | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視所選項目的檔案 | | | `` / `` | 搜尋 | | @@ -156,12 +149,12 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| | `` `` | 複製子模組名稱到剪貼簿 | | -| `` `` | Enter | 進入子模組 | -| `` d `` | Remove | Remove the selected submodule and its corresponding directory. | -| `` u `` | Update | 更新子模組 | +| `` `` | 進入 | 進入子模組 | +| `` d `` | 刪除 | 刪除選定的子模組及其相應的目錄。 | +| `` u `` | 更新 | 更新子模組 | | `` n `` | 新增子模組 | | | `` e `` | 更新子模組 URL | | -| `` i `` | Initialize | 初始化子模組 | +| `` i `` | 初始化 | 初始化子模組 | | `` b `` | 查看批量子模組選項 | | | `` / `` | 搜尋 | | @@ -169,27 +162,27 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| -| `` n `` | New worktree | | -| `` `` | Switch | Switch to the selected worktree. | +| `` n `` | 新建工作樹 | | +| `` `` | 切換 | 切換到選中的工作樹。 | | `` o `` | 在編輯器中開啟 | | -| `` d `` | Remove | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. | +| `` d `` | 刪除 | 刪除選定的工作樹。這將刪除工作樹的目錄以及 .git 目錄中有關工作樹的後設資料。 | | `` / `` | 搜尋 | | ## 提交 | Key | Action | Info | |-----|--------|-------------| -| `` `` | Copy abbreviated commit hash to clipboard | | +| `` `` | 複製縮略提交雜湊值到剪貼簿 | | | `` `` | 重設選定的揀選 (複製) 提交 | | | `` b `` | 查看二分選項 | | -| `` s `` | 壓縮 (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. | -| `` f `` | 修復 (Fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. | -| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. | +| `` s `` | 壓縮 (Squash) | 將已選提交壓縮到該提交之下。這些選定的提交的訊息會附加到該提交的訊息之下。 | +| `` f `` | 修復 (Fixup) | 將選定的提交合併到其下面的提交中。與壓縮類似,但所選提交的訊息將被丟棄。 | +| `` c `` | 設定修復提交資訊 | 設定修復提交的資訊選項。-C 選項表示使用此提交的資訊,而非目標提交的資訊。 | | `` r `` | 改寫提交 | 改寫選中的提交訊息 | | `` R `` | 使用編輯器改寫提交 | | -| `` d `` | 刪除提交 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. | +| `` d `` | 刪除提交 | 刪除選中的提交。這將透過變基從分支中刪除該提交,如果該提交修改的內容依賴於後續的提交,則需要解決合併衝突。 | | `` e `` | 編輯(開始互動變基) | 編輯提交 | -| `` i `` | 開始互動變基 | Start an interactive rebase for the commits on your branch. This will include all commits from the HEAD commit down to the first merge commit or main branch commit.
If you would instead like to start an interactive rebase from the selected commit, press `e`. | +| `` i `` | 開始互動變基 | 為分支上的提交啟動互動式變基。這將包括從 HEAD 提交到第一個合併提交或主分支提交的所有提交。
如果您想從所選提交啟動互動式變基,請按 `e`。 | | `` p `` | 挑選 | 挑選提交 (於變基過程中) | | `` F `` | 建立修復提交 | 為此提交建立修復提交 | | `` S `` | 壓縮上方所有「fixup」提交(自動壓縮) | 是否壓縮上方 {{.commit}} 所有「fixup」提交? | @@ -198,22 +191,22 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` V `` | 貼上提交 (揀選) | | | `` B `` | 為了變基已標注提交為基準提交 | 請為了下一次變基選擇一項基準提交;此將執行 `git rebase --onto`。 | | `` A `` | 修改 | 使用已預存的更改修正提交 | -| `` a `` | 設定/重設提交作者 | Set/Reset commit author or set co-author. | -| `` t `` | 還原 | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. | -| `` T `` | 打標籤到提交 | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. | -| `` `` | 開啟記錄選單 | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. | -| `` G `` | Open pull request in browser | | -| `` `` | 檢出 | Checkout the selected commit as a detached HEAD. | -| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | +| `` a `` | 設定/重設提交作者 | 設定或重置提交的作者,或新增其他作者。 | +| `` t `` | 還原 | 為所選提交建立還原提交,這會反向應用所選提交的更改。 | +| `` T `` | 打標籤到提交 | 建立一個新標籤指向所選提交。您可以在彈窗中輸入標籤名稱和描述(可選)。 | +| `` `` | 開啟記錄選單 | 檢視提交日誌的選項,例如更改排序順序、隱藏 git graph、顯示整個 git graph。 | +| `` G `` | 在瀏覽器中開啟拉取請求 | | +| `` `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 | +| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 | | `` o `` | 在瀏覽器中開啟提交 | | | `` n `` | 從提交建立新分支 | | -| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | -| `` w `` | New worktree | | -| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. | -| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | +| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。

請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 | +| `` w `` | 新建工作樹 | | +| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 | +| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `` 來取消選擇。 | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` * `` | Select commits of current branch | | -| `` 0 `` | Focus main view | | +| `` * `` | 選擇目前分支的提交 | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視所選項目的檔案 | | | `` / `` | 搜尋 | | @@ -231,30 +224,30 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 複製檔案名稱到剪貼簿 | | | `` y `` | 複製到剪貼簿 | | | `` c `` | 檢出 | 檢出檔案 | -| `` d `` | 捨棄 | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. | +| `` d `` | 捨棄 | 放棄對此檔案的提交變更。 | | `` o `` | 開啟檔案 | 使用預設軟體開啟 | | `` e `` | 編輯 | 使用外部編輯器開啟 | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` `` | 切換檔案是否包含在補丁中 | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. | -| `` a `` | 切換所有檔案是否包含在補丁中 | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. | -| `` `` | 輸入檔案以將選定的行添加至補丁(或切換目錄折疊) | If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory. | -| `` ` `` | 顯示檔案樹狀視圖 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | -| `` - `` | Collapse all files | Collapse all directories in the files tree | -| `` = `` | Expand all files | Expand all directories in the file tree | -| `` 0 `` | Focus main view | | +| `` `` | 切換檔案是否包含在補丁中 | 切換檔案是否包含在自定義補丁中。請參閱 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 | +| `` a `` | 切換所有檔案是否包含在補丁中 | 新增或刪除所有提交中的檔案到自定義的補丁中。請參閱 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 | +| `` `` | 輸入檔案以將選定的行添加至補丁(或切換目錄折疊) | 如果已選擇一個檔案,則Enter進入該檔案,以便您可以向自定義補丁新增/刪除單獨的行。如果選擇了目錄,則切換目錄。 | +| `` ` `` | 顯示檔案樹狀視圖 | 在平面佈局和樹佈局之間切換檔案檢視。平面佈局在單個列表中顯示所有檔案路徑,樹佈局按目錄分組檔案。

可以在設定檔中使用 'gui.showFileTree' 鍵更改預設設定。 | +| `` - `` | 摺疊全部檔案 | 摺疊檔案樹中的全部目錄 | +| `` = `` | 展開全部檔案 | 展開檔案樹中的全部目錄 | +| `` 0 `` | 聚焦主檢視 | | | `` / `` | 搜尋 | | ## 收藏 (Stash) | Key | Action | Info | |-----|--------|-------------| -| `` `` | 套用 | Apply the stash entry to your working directory. | -| `` g `` | 還原 | Apply the stash entry to your working directory and remove the stash entry. | -| `` d `` | 捨棄 | Remove the stash entry from the stash list. | -| `` n `` | 新分支 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. | -| `` w `` | New worktree | | +| `` `` | 套用 | 將貯藏項應用到您的工作目錄。 | +| `` g `` | 還原 | 將儲存項應用到工作目錄並刪除儲存項。 | +| `` d `` | 捨棄 | 從貯藏列表中刪除該貯藏項。 | +| `` n `` | 新分支 | 從選定的貯藏項建立一個新分支。這是透過 git 檢查建立貯藏項的提交,從該提交建立一個新分支,然後將貯藏項作為附加提交應用到新分支來實現的。 | +| `` w `` | 新建工作樹 | | | `` r `` | 重新命名收藏 | | -| `` 0 `` | Focus main view | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視所選項目的檔案 | | | `` / `` | 搜尋 | | @@ -262,19 +255,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| -| `` `` | Copy abbreviated commit hash to clipboard | | -| `` `` | 檢出 | Checkout the selected commit as a detached HEAD. | -| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | +| `` `` | 複製縮略提交雜湊值到剪貼簿 | | +| `` `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 | +| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 | | `` o `` | 在瀏覽器中開啟提交 | | | `` n `` | 從提交建立新分支 | | -| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | -| `` w `` | New worktree | | -| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. | -| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | +| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。

請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 | +| `` w `` | 新建工作樹 | | +| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 | +| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `` 來取消選擇。 | | `` `` | 重設選定的揀選 (複製) 提交 | | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` * `` | Select commits of current branch | | -| `` 0 `` | Focus main view | | +| `` * `` | 選擇目前分支的提交 | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視提交 | | | `` / `` | 搜尋 | | @@ -286,18 +279,18 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` i `` | 顯示 git-flow 選項 | | | `` `` | 檢出 | 檢出選定的項目。 | | `` n `` | 新分支 | | -| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | -| `` w `` | New worktree | | +| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。

請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 | +| `` w `` | 新建工作樹 | | | `` o `` | 建立拉取請求 | | | `` O `` | 建立拉取請求選項 | | -| `` G `` | Open pull request in browser | | +| `` G `` | 在瀏覽器中開啟拉取請求 | | | `` `` | 複製拉取請求的 URL 到剪貼板 | | -| `` c `` | 根據名稱檢出 | Checkout by name. In the input box you can enter '-' to switch to the previous branch. | -| `` - `` | Checkout previous branch | | -| `` F `` | 強制檢出 | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. | -| `` d `` | 刪除 | View delete options for local/remote branch. | -| `` r `` | 將已檢出的分支變基至此分支 | Rebase the checked-out branch onto the selected branch. | -| `` M `` | 合併到當前檢出的分支 | View options for merging the selected item into the current branch (regular merge, squash merge) | +| `` c `` | 根據名稱檢出 | 按名稱檢出。在輸入框中,您可以輸入'-' 來切換到最後一個分支。 | +| `` - `` | 簽出上一個分支 | | +| `` F `` | 強制檢出 | 強制檢出所選分支。這將在檢出所選分支之前放棄工作目錄中的所有本地更改。 | +| `` d `` | 刪除 | 檢視本地/遠端分支的刪除選項。 | +| `` r `` | 將已檢出的分支變基至此分支 | 將檢出的分支變基到所選的分支上。 | +| `` M `` | 合併到當前檢出的分支 | 檢視將選中項合併到目前分支的選項(正常合併,壓縮合並) | | `` f `` | 從上游快進此分支 | 從遠端快進所選的分支 | | `` T `` | 建立標籤 | | | `` s `` | 排序規則 | | @@ -305,7 +298,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` R `` | 重新命名分支 | | | `` u `` | 檢視遠端設定 | 檢視有關遠端分支的設定(例如重設至遠端) | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` 0 `` | Focus main view | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視提交 | | | `` / `` | 搜尋 | | @@ -313,15 +306,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| -| `` `` | Copy tag to clipboard | | -| `` `` | 檢出 | Checkout the selected tag as a detached HEAD. | -| `` n `` | 建立標籤 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. | -| `` w `` | New worktree | | -| `` d `` | 刪除 | View delete options for local/remote tag. | -| `` P `` | 推送標籤 | Push the selected tag to a remote. You'll be prompted to select a remote. | -| `` g `` | 重設 | View reset options (soft/mixed/hard) for resetting onto selected item. | +| `` `` | 複製標籤到剪貼簿 | | +| `` `` | 檢出 | 檢出選擇的標籤作為分離的HEAD。 | +| `` n `` | 建立標籤 | 基於目前提交建立一個新標籤。您將在彈窗中輸入標籤名稱和描述(可選)。 | +| `` w `` | 新建工作樹 | | +| `` d `` | 刪除 | 檢視本機/遠端標籤的刪除選項。 | +| `` P `` | 推送標籤 | 推送選擇的標籤到遠端。您將在彈窗中選擇一個遠端。 | +| `` g `` | 重設 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` 0 `` | Focus main view | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視提交 | | | `` / `` | 搜尋 | | @@ -330,40 +323,40 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| | `` `` | 複製檔案名稱到剪貼簿 | | -| `` `` | 切換預存 | Toggle staged for selected file. | +| `` `` | 切換預存 | 切換所選檔案的暫存狀態。 | | `` `` | 篩選檔案 (預存/未預存) | | | `` y `` | 複製到剪貼簿 | | | `` c `` | 提交變更 | 提交暫存區變更 | | `` w `` | 沒有預提交 hook 就提交更改 | | | `` A `` | 修改上次提交 | | | `` C `` | 使用 git 編輯器提交變更 | | -| `` `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: | +| `` `` | 尋找 fixup 的基礎提交 | 找出目前變更所依據的提交,以便 amend/fixup。這樣不必逐一檢視分支中的提交來找出要 amend/fixup 的提交。請見文件: | | `` e `` | 編輯 | 使用外部編輯器開啟 | | `` o `` | 開啟檔案 | 使用預設軟體開啟 | | `` i `` | 忽略或排除檔案 | | | `` r `` | 重新整理檔案 | | -| `` s `` | 收藏 | Stash all changes. For other variations of stashing, use the view stash options keybinding. | -| `` S `` | 檢視收藏選項 | View stash options (e.g. stash all, stash staged, stash unstaged). | -| `` a `` | 全部預存/取消預存 | Toggle staged/unstaged for all files in working tree. | -| `` `` | 選擇檔案中的單個程式碼塊/行,或展開/折疊目錄 | If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it. | +| `` s `` | 收藏 | 貯藏所有變更.若要使用其他貯藏變體,請使用檢視貯藏選項快捷鍵。 | +| `` S `` | 檢視收藏選項 | 檢視貯藏選項(例如:貯藏所有、貯藏已暫存變更、貯藏未暫存變更)。 | +| `` a `` | 全部預存/取消預存 | 切換工作區中所有檔案的已暫存/未暫存狀態。 | +| `` `` | 選擇檔案中的單個程式碼塊/行,或展開/折疊目錄 | 如果選中的是一個檔案,則會進入到暫存檢視,以便可以暫存單個程式碼塊/行。如果選中的是一個目錄,則會摺疊/展開這個目錄。 | | `` d `` | 捨棄 | 檢視選中變動進行捨棄復原 | | `` g `` | 檢視遠端重設選項 | | -| `` D `` | 重設 | View reset options for working tree (e.g. nuking the working tree). | -| `` ` `` | 顯示檔案樹狀視圖 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | +| `` D `` | 重設 | 檢視工作樹的重置選項(例如:清除工作樹)。 | +| `` ` `` | 顯示檔案樹狀視圖 | 在平面佈局和樹佈局之間切換檔案檢視。平面佈局在單個列表中顯示所有檔案路徑,樹佈局按目錄分組檔案。

可以在設定檔中使用 'gui.showFileTree' 鍵更改預設設定。 | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` M `` | View merge conflict options | View options for resolving merge conflicts. | +| `` M `` | 檢視合併衝突選項 | 檢視用於解決合併衝突的選項。 | | `` f `` | 擷取 | 同步遠端異動 | -| `` - `` | Collapse all files | Collapse all directories in the files tree | -| `` = `` | Expand all files | Expand all directories in the file tree | -| `` 0 `` | Focus main view | | +| `` - `` | 摺疊全部檔案 | 摺疊檔案樹中的全部目錄 | +| `` = `` | 展開全部檔案 | 展開檔案樹中的全部目錄 | +| `` 0 `` | 聚焦主檢視 | | | `` / `` | 搜尋 | | ## 次要 | Key | Action | Info | |-----|--------|-------------| -| `` `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). | -| `` `` | Exit back to side panel | | +| `` `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 | +| `` `` | 退出回到側邊面板 | | | `` / `` | 搜尋 | | ## 狀態 @@ -373,9 +366,9 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | 編輯設定檔案 | 使用外部編輯器開啟 | | `` u `` | 檢查更新 | | | `` `` | 切換到最近使用的版本庫 | | -| `` a `` | Show/cycle all branch logs | | -| `` A `` | Show/cycle all branch logs (reverse) | | -| `` 0 `` | Focus main view | | +| `` a `` | 顯示/迴圈所有分支日誌 | | +| `` A `` | 顯示/迴圈所有分支日誌(反向) | | +| `` 0 `` | 聚焦主檢視 | | ## 確認面板 @@ -385,16 +378,23 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 關閉/取消 | | | `` `` | 複製到剪貼簿 | | +## 輸入提示 + +| Key | Action | Info | +|-----|--------|-------------| +| `` `` | 確認 | | +| `` `` | 關閉/取消 | | + ## 遠端 | Key | Action | Info | |-----|--------|-------------| -| `` `` | View branches | | +| `` `` | 檢視分支 | | | `` n `` | 新增遠端 | | -| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. | +| `` d `` | 刪除 | 刪除選中的遠端。從遠端跟蹤遠端分支的任何本地分支都不會受到影響。 | | `` e `` | 編輯 | 編輯遠端 | | `` f `` | 擷取 | 擷取遠端 | -| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. | +| `` F `` | 新增復刻遠端倉庫 | 透過替換 origin URL 中的所有者來快速新增復刻遠端倉庫,並可選擇從新遠端倉庫檢出分支。 | | `` / `` | 搜尋 | | ## 遠端分支 @@ -402,16 +402,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| | `` `` | 複製分支名稱到剪貼簿 | | -| `` `` | 檢出 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. | +| `` `` | 檢出 | 基於目前選中的遠端分支檢出一個新的本地分支,或者將遠端分支作分離的HEAD。 | | `` n `` | 新分支 | | -| `` w `` | New worktree | | -| `` M `` | 合併到當前檢出的分支 | View options for merging the selected item into the current branch (regular merge, squash merge) | -| `` r `` | 將已檢出的分支變基至此分支 | Rebase the checked-out branch onto the selected branch. | -| `` d `` | 刪除 | Delete the remote branch from the remote. | +| `` w `` | 新建工作樹 | | +| `` M `` | 合併到當前檢出的分支 | 檢視將選中項合併到目前分支的選項(正常合併,壓縮合並) | +| `` r `` | 將已檢出的分支變基至此分支 | 將檢出的分支變基到所選的分支上。 | +| `` d `` | 刪除 | 從遠端刪除遠端分支。 | | `` u `` | 設置為遠端 | 將此分支設為當前分支之遠端 | | `` s `` | 排序規則 | | -| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. | +| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` 0 `` | Focus main view | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視提交 | | | `` / `` | 搜尋 | | diff --git a/docs/Custom_DiffRenderers.md b/docs/Custom_DiffRenderers.md index 1a6b1d5ce..509f42ebf 100644 --- a/docs/Custom_DiffRenderers.md +++ b/docs/Custom_DiffRenderers.md @@ -27,7 +27,7 @@ Fields only for `extDiff`: Fields only for `rawGit`: -- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`) +- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`), as an array of strings. Here's an example for a multi-renderer setup: @@ -40,7 +40,7 @@ git: - type: extDiff command: difft --color=always --context={{diffContext}} - type: rawGit - args: --color-words + args: [--color-words] name: color-words - type: rawGit # git's default diff name: default diff --git a/docs/Searching.md b/docs/Searching.md index 589831c55..4cba775df 100644 --- a/docs/Searching.md +++ b/docs/Searching.md @@ -4,10 +4,14 @@ Depending on the currently focused view, hitting '/' will bring up a filter or search prompt. When filtering, the contents of the view will be filtered down to only those lines which match the query string. When searching, the contents of the view are not filtered, but matching lines are highlighted and you can iterate through matches with `n`/`N`. -We intend to support filtering for the files view soon, but at the moment it uses searching. We intend to continue using search for the commits view because you typically care about the commits that come before/after a matching commit. +In the commits view we don't filter, but search; this is deliberate because you typically care about the commits that come before/after a matching commit. If you would like both filtering and searching to be enabled on a given view, please raise an issue for this. +## Menu filtering + +The keybindings (`?`) and recent repositories menus can be filtered simply by typing. The filter field appears at the bottom of the menu while you type; there is no need to press `/` or confirm the filter before navigating the results. + ## Filtering files by status You can filter the files view to only show staged/unstaged files by pressing `` in the files view. diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index 7d95e72dd..0f01dea7c 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -280,7 +280,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` w `` | New worktree | | | `` M `` | Merge in met huidige checked out branch | View options for merging the selected item into the current branch (regular merge, squash merge) | | `` r `` | Rebase branch | Rebase de uitgecheckte branch bovenop de geselecteerde branch. | -| `` d `` | Verwijderen | Delete the remote branch from the remote. | +| `` d `` | Verwijderen | Verwijder de remote branch van de remote. | | `` u `` | Instellen als upstream | Stel in als upstream van uitgecheckte branch | | `` s `` | Sort order | | | `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. | @@ -295,7 +295,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` `` | Bekijk branches | | | `` n `` | Voeg een nieuwe remote toe | | -| `` d `` | Verwijderen | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. | +| `` d `` | Verwijderen | Verwijder de geselecteerde remote. Locale branches die een branch tracken van de remote worden niet aangepast. | | `` e `` | Edit | Wijzig remote | | `` f `` | Fetch | Fetch remote | | `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. | diff --git a/docs/keybindings/Keybindings_zh-TW.md b/docs/keybindings/Keybindings_zh-TW.md index 1706a627e..0e5debfdf 100644 --- a/docs/keybindings/Keybindings_zh-TW.md +++ b/docs/keybindings/Keybindings_zh-TW.md @@ -9,28 +9,28 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 切換到最近使用的版本庫 | | | `` , K, (fn+up/shift+k) `` | 向上捲動主面板 | | | `` , J, (fn+down/shift+j) `` | 向下捲動主面板 | | -| `` @ `` | 開啟命令記錄選單 | View options for the command log e.g. show/hide the command log and focus the command log. | +| `` @ `` | 開啟命令記錄選單 | 檢視命令日誌的選項,例如顯示/隱藏命令日誌以及聚焦命令日誌。 | | `` P `` | 推送 | 推送到遠端。如果沒有設定遠端,會開啟設定視窗。 | | `` p `` | 拉取 | 從遠端同步當前分支。如果沒有設定遠端,會開啟設定視窗。 | -| `` ) `` | Increase rename similarity threshold | Increase the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | -| `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | -| `` } `` | 增加差異檢視中顯示變更周圍上下文的大小 | Increase the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | -| `` { `` | 減小差異檢視中顯示變更周圍上下文的大小 | Decrease the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | -| `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. | +| `` ) `` | 提高重新命名相似度閾值 | 提高將刪除和新增對視為重新命名所需的相似度閾值。

預設值可在設定檔中透過鍵 'git.renameSimilarityThreshold' 更改。 | +| `` ( `` | 降低重新命名相似度閾值 | 降低將刪除和新增對視為重新命名所需的相似度閾值。

預設值可在設定檔中透過鍵 'git.renameSimilarityThreshold' 更改。 | +| `` } `` | 增加差異檢視中顯示變更周圍上下文的大小 | 增加差異檢視中變更周圍顯示的上下文量。

預設值可在設定檔中透過鍵 'git.diffContextSize' 更改。 | +| `` { `` | 減小差異檢視中顯示變更周圍上下文的大小 | 減少差異檢視中變更周圍顯示的上下文量。

預設值可在設定檔中透過鍵 'git.diffContextSize' 更改。 | +| `` : `` | 執行 Shell 命令 | 調出可輸入shell命令執行的提示符。 | | `` `` | 檢視自訂補丁選項 | | -| `` m `` | 查看合併/變基選項 | View options to abort/continue/skip the current merge/rebase. | -| `` R `` | 重新整理 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. | +| `` m `` | 查看合併/變基選項 | 檢視目前合併或變基的中止、繼續、跳過選項。 | +| `` R `` | 重新整理 | 重新整理Git狀態(即在背景執行`git status`、`git branch`等命令以更新面板內容)。此操作不會執行`git fetch`。 | | `` + `` | 下一個螢幕模式(常規/半螢幕/全螢幕) | | | `` _ `` | 上一個螢幕模式 | | -| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. | -| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. | +| `` \| `` | 切換差異渲染器 | 選擇已設定的差異渲染器清單中的下一個渲染器。 | +| `` \ `` | 切換差異渲染器(反向) | 選擇已設定的差異渲染器清單中的上一個渲染器。 | | `` `` | 取消 | | | `` ? `` | 開啟選單 | | -| `` `` | 檢視篩選路徑選項 | View options for filtering the commit log, so that only commits matching the filter are shown. | -| `` W, `` | 開啟差異比較選單 | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. | +| `` `` | 檢視篩選路徑選項 | 檢視用於過濾提交日誌的選項,以便僅顯示與過濾器匹配的提交。 | +| `` W, `` | 開啟差異比較選單 | 檢視與比較兩個引用相關的選項,例如與選定的 ref 進行比較,輸入要比較的 ref,然後反轉比較方向。 | | `` q, `` | 結束 | | -| `` `` | Suspend the application | | -| `` `` | 切換是否在差異檢視中顯示空格變更 | Toggle whether or not whitespace changes are shown in the diff view.

The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. | +| `` `` | 掛起應用程式 | | +| `` `` | 切換是否在差異檢視中顯示空格變更 | 切換是否在差異檢視中顯示空白字元更改。

預設值可在設定檔中透過鍵 'git.ignoreWhitespaceInDiffView' 更改。 | | `` `` | 編輯設定檔案 | 使用外部編輯器開啟 | | `` z `` | 復原 | 將使用 reflog 確任 git 指令以復原。這不包括工作區更改;只考慮提交。 | | `` Z `` | 取消復原 | 將使用 reflog 確任 git 指令以重作。這不包括工作區更改;只考慮提交。 | @@ -44,21 +44,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` <, `` | 捲動到頂部 | | | `` >, `` | 捲動到底部 | | | `` v `` | 切換拖曳選擇 | | -| `` `` | Range select down | | -| `` `` | Range select up | | +| `` `` | 向下擴充套件選擇範圍 | | +| `` `` | 向上擴充套件選擇範圍 | | | `` / `` | 搜尋 | | | `` H `` | 向左捲動 | | | `` L `` | 向右捲動 | | | `` ] `` | 下一個索引標籤 | | | `` [ `` | 上一個索引標籤 | | -## Input prompt - -| Key | Action | Info | -|-----|--------|-------------| -| `` `` | 確認 | | -| `` `` | 關閉/取消 | | - ## 主面板 (補丁生成) | Key | Action | Info | @@ -66,12 +59,12 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | | `` v `` | 切換拖曳選擇 | | -| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | +| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 | | `` `` | 複製所選文本至剪貼簿 | | | `` o `` | 開啟檔案 | 使用預設軟體開啟 | | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | | `` `` | 向 (或從) 補丁中添加/刪除行 | | -| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. | +| `` d `` | 從提交中移除行 | 從本次提交中移除所選行。此操作會在背景執行互動式變基,因此如果後續提交也修改了這些行,您可能會遇到合併衝突。 | | `` `` | 退出自訂補丁建立器 | | | `` / `` | 搜尋 | | @@ -81,8 +74,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` (fn+up) `` | 向下捲動 | | | `` (fn+down) `` | 向上捲動 | | -| `` `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). | -| `` `` | Exit back to side panel | | +| `` `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 | +| `` `` | 退出回到側邊面板 | | | `` / `` | 搜尋 | | ## 主面板(合併) @@ -90,15 +83,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| | `` `` | 挑選程式碼片段 | | -| `` b `` | Pick both hunks | | +| `` b `` | 選取兩個區塊 | | | `` , k `` | 選擇上一段 | | | `` , j `` | 選擇下一段 | | | `` , h `` | 選擇上一個衝突 | | | `` , l `` | 選擇下一個衝突 | | -| `` z `` | 復原 | Undo last merge conflict resolution. | +| `` z `` | 復原 | 撤消上次合併衝突解決。 | | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | | `` o `` | 開啟檔案 | 使用預設軟體開啟 | -| `` M `` | View merge conflict options | View options for resolving merge conflicts. | +| `` M `` | 檢視合併衝突選項 | 檢視用於解決合併衝突的選項。 | | `` `` | 返回檔案面板 | | ## 主面板(預存) @@ -108,19 +101,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | | `` v `` | 切換拖曳選擇 | | -| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | +| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 | | `` `` | 複製所選文本至剪貼簿 | | | `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | -| `` d `` | 刪除變更 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | +| `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 | | `` o `` | 開啟檔案 | 使用預設軟體開啟 | | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | | `` `` | 返回檔案面板 | | -| `` `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). | -| `` E `` | 編輯程式碼塊 | Edit selected hunk in external editor. | +| `` `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 | +| `` E `` | 編輯程式碼塊 | 在外部編輯器中編輯選中的程式碼塊。 | | `` c `` | 提交變更 | 提交暫存區變更 | | `` w `` | 沒有預提交 hook 就提交更改 | | | `` C `` | 使用 git 編輯器提交變更 | | -| `` `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: | +| `` `` | 尋找 fixup 的基礎提交 | 找出目前變更所依據的提交,以便 amend/fixup。這樣不必逐一檢視分支中的提交來找出要 amend/fixup 的提交。請見文件: | | `` / `` | 搜尋 | | ## 功能表 @@ -135,19 +128,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| -| `` `` | Copy abbreviated commit hash to clipboard | | -| `` `` | 檢出 | Checkout the selected commit as a detached HEAD. | -| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | +| `` `` | 複製縮略提交雜湊值到剪貼簿 | | +| `` `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 | +| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 | | `` o `` | 在瀏覽器中開啟提交 | | | `` n `` | 從提交建立新分支 | | -| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | -| `` w `` | New worktree | | -| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. | -| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | +| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。

請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 | +| `` w `` | 新建工作樹 | | +| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 | +| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `` 來取消選擇。 | | `` `` | 重設選定的揀選 (複製) 提交 | | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` * `` | Select commits of current branch | | -| `` 0 `` | Focus main view | | +| `` * `` | 選擇目前分支的提交 | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視所選項目的檔案 | | | `` / `` | 搜尋 | | @@ -156,12 +149,12 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| | `` `` | 複製子模組名稱到剪貼簿 | | -| `` `` | Enter | 進入子模組 | -| `` d `` | Remove | Remove the selected submodule and its corresponding directory. | -| `` u `` | Update | 更新子模組 | +| `` `` | 進入 | 進入子模組 | +| `` d `` | 刪除 | 刪除選定的子模組及其相應的目錄。 | +| `` u `` | 更新 | 更新子模組 | | `` n `` | 新增子模組 | | | `` e `` | 更新子模組 URL | | -| `` i `` | Initialize | 初始化子模組 | +| `` i `` | 初始化 | 初始化子模組 | | `` b `` | 查看批量子模組選項 | | | `` / `` | 搜尋 | | @@ -169,27 +162,27 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| -| `` n `` | New worktree | | -| `` `` | Switch | Switch to the selected worktree. | +| `` n `` | 新建工作樹 | | +| `` `` | 切換 | 切換到選中的工作樹。 | | `` o `` | 在編輯器中開啟 | | -| `` d `` | Remove | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. | +| `` d `` | 刪除 | 刪除選定的工作樹。這將刪除工作樹的目錄以及 .git 目錄中有關工作樹的後設資料。 | | `` / `` | 搜尋 | | ## 提交 | Key | Action | Info | |-----|--------|-------------| -| `` `` | Copy abbreviated commit hash to clipboard | | +| `` `` | 複製縮略提交雜湊值到剪貼簿 | | | `` `` | 重設選定的揀選 (複製) 提交 | | | `` b `` | 查看二分選項 | | -| `` s `` | 壓縮 (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. | -| `` f `` | 修復 (Fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. | -| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. | +| `` s `` | 壓縮 (Squash) | 將已選提交壓縮到該提交之下。這些選定的提交的訊息會附加到該提交的訊息之下。 | +| `` f `` | 修復 (Fixup) | 將選定的提交合併到其下面的提交中。與壓縮類似,但所選提交的訊息將被丟棄。 | +| `` c `` | 設定修復提交資訊 | 設定修復提交的資訊選項。-C 選項表示使用此提交的資訊,而非目標提交的資訊。 | | `` r `` | 改寫提交 | 改寫選中的提交訊息 | | `` R `` | 使用編輯器改寫提交 | | -| `` d `` | 刪除提交 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. | +| `` d `` | 刪除提交 | 刪除選中的提交。這將透過變基從分支中刪除該提交,如果該提交修改的內容依賴於後續的提交,則需要解決合併衝突。 | | `` e `` | 編輯(開始互動變基) | 編輯提交 | -| `` i `` | 開始互動變基 | Start an interactive rebase for the commits on your branch. This will include all commits from the HEAD commit down to the first merge commit or main branch commit.
If you would instead like to start an interactive rebase from the selected commit, press `e`. | +| `` i `` | 開始互動變基 | 為分支上的提交啟動互動式變基。這將包括從 HEAD 提交到第一個合併提交或主分支提交的所有提交。
如果您想從所選提交啟動互動式變基,請按 `e`。 | | `` p `` | 挑選 | 挑選提交 (於變基過程中) | | `` F `` | 建立修復提交 | 為此提交建立修復提交 | | `` S `` | 壓縮上方所有「fixup」提交(自動壓縮) | 是否壓縮上方 {{.commit}} 所有「fixup」提交? | @@ -198,22 +191,22 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` V `` | 貼上提交 (揀選) | | | `` B `` | 為了變基已標注提交為基準提交 | 請為了下一次變基選擇一項基準提交;此將執行 `git rebase --onto`。 | | `` A `` | 修改 | 使用已預存的更改修正提交 | -| `` a `` | 設定/重設提交作者 | Set/Reset commit author or set co-author. | -| `` t `` | 還原 | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. | -| `` T `` | 打標籤到提交 | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. | -| `` `` | 開啟記錄選單 | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. | -| `` G `` | Open pull request in browser | | -| `` `` | 檢出 | Checkout the selected commit as a detached HEAD. | -| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | +| `` a `` | 設定/重設提交作者 | 設定或重置提交的作者,或新增其他作者。 | +| `` t `` | 還原 | 為所選提交建立還原提交,這會反向應用所選提交的更改。 | +| `` T `` | 打標籤到提交 | 建立一個新標籤指向所選提交。您可以在彈窗中輸入標籤名稱和描述(可選)。 | +| `` `` | 開啟記錄選單 | 檢視提交日誌的選項,例如更改排序順序、隱藏 git graph、顯示整個 git graph。 | +| `` G `` | 在瀏覽器中開啟拉取請求 | | +| `` `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 | +| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 | | `` o `` | 在瀏覽器中開啟提交 | | | `` n `` | 從提交建立新分支 | | -| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | -| `` w `` | New worktree | | -| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. | -| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | +| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。

請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 | +| `` w `` | 新建工作樹 | | +| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 | +| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `` 來取消選擇。 | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` * `` | Select commits of current branch | | -| `` 0 `` | Focus main view | | +| `` * `` | 選擇目前分支的提交 | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視所選項目的檔案 | | | `` / `` | 搜尋 | | @@ -231,30 +224,30 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 複製檔案名稱到剪貼簿 | | | `` y `` | 複製到剪貼簿 | | | `` c `` | 檢出 | 檢出檔案 | -| `` d `` | 捨棄 | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. | +| `` d `` | 捨棄 | 放棄對此檔案的提交變更。 | | `` o `` | 開啟檔案 | 使用預設軟體開啟 | | `` e `` | 編輯 | 使用外部編輯器開啟 | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` `` | 切換檔案是否包含在補丁中 | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. | -| `` a `` | 切換所有檔案是否包含在補丁中 | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. | -| `` `` | 輸入檔案以將選定的行添加至補丁(或切換目錄折疊) | If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory. | -| `` ` `` | 顯示檔案樹狀視圖 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | -| `` - `` | Collapse all files | Collapse all directories in the files tree | -| `` = `` | Expand all files | Expand all directories in the file tree | -| `` 0 `` | Focus main view | | +| `` `` | 切換檔案是否包含在補丁中 | 切換檔案是否包含在自定義補丁中。請參閱 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 | +| `` a `` | 切換所有檔案是否包含在補丁中 | 新增或刪除所有提交中的檔案到自定義的補丁中。請參閱 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 | +| `` `` | 輸入檔案以將選定的行添加至補丁(或切換目錄折疊) | 如果已選擇一個檔案,則Enter進入該檔案,以便您可以向自定義補丁新增/刪除單獨的行。如果選擇了目錄,則切換目錄。 | +| `` ` `` | 顯示檔案樹狀視圖 | 在平面佈局和樹佈局之間切換檔案檢視。平面佈局在單個列表中顯示所有檔案路徑,樹佈局按目錄分組檔案。

可以在設定檔中使用 'gui.showFileTree' 鍵更改預設設定。 | +| `` - `` | 摺疊全部檔案 | 摺疊檔案樹中的全部目錄 | +| `` = `` | 展開全部檔案 | 展開檔案樹中的全部目錄 | +| `` 0 `` | 聚焦主檢視 | | | `` / `` | 搜尋 | | ## 收藏 (Stash) | Key | Action | Info | |-----|--------|-------------| -| `` `` | 套用 | Apply the stash entry to your working directory. | -| `` g `` | 還原 | Apply the stash entry to your working directory and remove the stash entry. | -| `` d `` | 捨棄 | Remove the stash entry from the stash list. | -| `` n `` | 新分支 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. | -| `` w `` | New worktree | | +| `` `` | 套用 | 將貯藏項應用到您的工作目錄。 | +| `` g `` | 還原 | 將儲存項應用到工作目錄並刪除儲存項。 | +| `` d `` | 捨棄 | 從貯藏列表中刪除該貯藏項。 | +| `` n `` | 新分支 | 從選定的貯藏項建立一個新分支。這是透過 git 檢查建立貯藏項的提交,從該提交建立一個新分支,然後將貯藏項作為附加提交應用到新分支來實現的。 | +| `` w `` | 新建工作樹 | | | `` r `` | 重新命名收藏 | | -| `` 0 `` | Focus main view | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視所選項目的檔案 | | | `` / `` | 搜尋 | | @@ -262,19 +255,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| -| `` `` | Copy abbreviated commit hash to clipboard | | -| `` `` | 檢出 | Checkout the selected commit as a detached HEAD. | -| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | +| `` `` | 複製縮略提交雜湊值到剪貼簿 | | +| `` `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 | +| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 | | `` o `` | 在瀏覽器中開啟提交 | | | `` n `` | 從提交建立新分支 | | -| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | -| `` w `` | New worktree | | -| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. | -| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | +| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。

請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 | +| `` w `` | 新建工作樹 | | +| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 | +| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `` 來取消選擇。 | | `` `` | 重設選定的揀選 (複製) 提交 | | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` * `` | Select commits of current branch | | -| `` 0 `` | Focus main view | | +| `` * `` | 選擇目前分支的提交 | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視提交 | | | `` / `` | 搜尋 | | @@ -286,18 +279,18 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` i `` | 顯示 git-flow 選項 | | | `` `` | 檢出 | 檢出選定的項目。 | | `` n `` | 新分支 | | -| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | -| `` w `` | New worktree | | +| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。

請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 | +| `` w `` | 新建工作樹 | | | `` o `` | 建立拉取請求 | | | `` O `` | 建立拉取請求選項 | | -| `` G `` | Open pull request in browser | | +| `` G `` | 在瀏覽器中開啟拉取請求 | | | `` `` | 複製拉取請求的 URL 到剪貼板 | | -| `` c `` | 根據名稱檢出 | Checkout by name. In the input box you can enter '-' to switch to the previous branch. | -| `` - `` | Checkout previous branch | | -| `` F `` | 強制檢出 | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. | -| `` d `` | 刪除 | View delete options for local/remote branch. | -| `` r `` | 將已檢出的分支變基至此分支 | Rebase the checked-out branch onto the selected branch. | -| `` M `` | 合併到當前檢出的分支 | View options for merging the selected item into the current branch (regular merge, squash merge) | +| `` c `` | 根據名稱檢出 | 按名稱檢出。在輸入框中,您可以輸入'-' 來切換到最後一個分支。 | +| `` - `` | 簽出上一個分支 | | +| `` F `` | 強制檢出 | 強制檢出所選分支。這將在檢出所選分支之前放棄工作目錄中的所有本地更改。 | +| `` d `` | 刪除 | 檢視本地/遠端分支的刪除選項。 | +| `` r `` | 將已檢出的分支變基至此分支 | 將檢出的分支變基到所選的分支上。 | +| `` M `` | 合併到當前檢出的分支 | 檢視將選中項合併到目前分支的選項(正常合併,壓縮合並) | | `` f `` | 從上游快進此分支 | 從遠端快進所選的分支 | | `` T `` | 建立標籤 | | | `` s `` | 排序規則 | | @@ -305,7 +298,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` R `` | 重新命名分支 | | | `` u `` | 檢視遠端設定 | 檢視有關遠端分支的設定(例如重設至遠端) | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` 0 `` | Focus main view | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視提交 | | | `` / `` | 搜尋 | | @@ -313,15 +306,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| -| `` `` | Copy tag to clipboard | | -| `` `` | 檢出 | Checkout the selected tag as a detached HEAD. | -| `` n `` | 建立標籤 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. | -| `` w `` | New worktree | | -| `` d `` | 刪除 | View delete options for local/remote tag. | -| `` P `` | 推送標籤 | Push the selected tag to a remote. You'll be prompted to select a remote. | -| `` g `` | 重設 | View reset options (soft/mixed/hard) for resetting onto selected item. | +| `` `` | 複製標籤到剪貼簿 | | +| `` `` | 檢出 | 檢出選擇的標籤作為分離的HEAD。 | +| `` n `` | 建立標籤 | 基於目前提交建立一個新標籤。您將在彈窗中輸入標籤名稱和描述(可選)。 | +| `` w `` | 新建工作樹 | | +| `` d `` | 刪除 | 檢視本機/遠端標籤的刪除選項。 | +| `` P `` | 推送標籤 | 推送選擇的標籤到遠端。您將在彈窗中選擇一個遠端。 | +| `` g `` | 重設 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` 0 `` | Focus main view | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視提交 | | | `` / `` | 搜尋 | | @@ -330,40 +323,40 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| | `` `` | 複製檔案名稱到剪貼簿 | | -| `` `` | 切換預存 | Toggle staged for selected file. | +| `` `` | 切換預存 | 切換所選檔案的暫存狀態。 | | `` `` | 篩選檔案 (預存/未預存) | | | `` y `` | 複製到剪貼簿 | | | `` c `` | 提交變更 | 提交暫存區變更 | | `` w `` | 沒有預提交 hook 就提交更改 | | | `` A `` | 修改上次提交 | | | `` C `` | 使用 git 編輯器提交變更 | | -| `` `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: | +| `` `` | 尋找 fixup 的基礎提交 | 找出目前變更所依據的提交,以便 amend/fixup。這樣不必逐一檢視分支中的提交來找出要 amend/fixup 的提交。請見文件: | | `` e `` | 編輯 | 使用外部編輯器開啟 | | `` o `` | 開啟檔案 | 使用預設軟體開啟 | | `` i `` | 忽略或排除檔案 | | | `` r `` | 重新整理檔案 | | -| `` s `` | 收藏 | Stash all changes. For other variations of stashing, use the view stash options keybinding. | -| `` S `` | 檢視收藏選項 | View stash options (e.g. stash all, stash staged, stash unstaged). | -| `` a `` | 全部預存/取消預存 | Toggle staged/unstaged for all files in working tree. | -| `` `` | 選擇檔案中的單個程式碼塊/行,或展開/折疊目錄 | If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it. | +| `` s `` | 收藏 | 貯藏所有變更.若要使用其他貯藏變體,請使用檢視貯藏選項快捷鍵。 | +| `` S `` | 檢視收藏選項 | 檢視貯藏選項(例如:貯藏所有、貯藏已暫存變更、貯藏未暫存變更)。 | +| `` a `` | 全部預存/取消預存 | 切換工作區中所有檔案的已暫存/未暫存狀態。 | +| `` `` | 選擇檔案中的單個程式碼塊/行,或展開/折疊目錄 | 如果選中的是一個檔案,則會進入到暫存檢視,以便可以暫存單個程式碼塊/行。如果選中的是一個目錄,則會摺疊/展開這個目錄。 | | `` d `` | 捨棄 | 檢視選中變動進行捨棄復原 | | `` g `` | 檢視遠端重設選項 | | -| `` D `` | 重設 | View reset options for working tree (e.g. nuking the working tree). | -| `` ` `` | 顯示檔案樹狀視圖 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | +| `` D `` | 重設 | 檢視工作樹的重置選項(例如:清除工作樹)。 | +| `` ` `` | 顯示檔案樹狀視圖 | 在平面佈局和樹佈局之間切換檔案檢視。平面佈局在單個列表中顯示所有檔案路徑,樹佈局按目錄分組檔案。

可以在設定檔中使用 'gui.showFileTree' 鍵更改預設設定。 | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` M `` | View merge conflict options | View options for resolving merge conflicts. | +| `` M `` | 檢視合併衝突選項 | 檢視用於解決合併衝突的選項。 | | `` f `` | 擷取 | 同步遠端異動 | -| `` - `` | Collapse all files | Collapse all directories in the files tree | -| `` = `` | Expand all files | Expand all directories in the file tree | -| `` 0 `` | Focus main view | | +| `` - `` | 摺疊全部檔案 | 摺疊檔案樹中的全部目錄 | +| `` = `` | 展開全部檔案 | 展開檔案樹中的全部目錄 | +| `` 0 `` | 聚焦主檢視 | | | `` / `` | 搜尋 | | ## 次要 | Key | Action | Info | |-----|--------|-------------| -| `` `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). | -| `` `` | Exit back to side panel | | +| `` `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 | +| `` `` | 退出回到側邊面板 | | | `` / `` | 搜尋 | | ## 狀態 @@ -373,9 +366,9 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | 編輯設定檔案 | 使用外部編輯器開啟 | | `` u `` | 檢查更新 | | | `` `` | 切換到最近使用的版本庫 | | -| `` a `` | Show/cycle all branch logs | | -| `` A `` | Show/cycle all branch logs (reverse) | | -| `` 0 `` | Focus main view | | +| `` a `` | 顯示/迴圈所有分支日誌 | | +| `` A `` | 顯示/迴圈所有分支日誌(反向) | | +| `` 0 `` | 聚焦主檢視 | | ## 確認面板 @@ -385,16 +378,23 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 關閉/取消 | | | `` `` | 複製到剪貼簿 | | +## 輸入提示 + +| Key | Action | Info | +|-----|--------|-------------| +| `` `` | 確認 | | +| `` `` | 關閉/取消 | | + ## 遠端 | Key | Action | Info | |-----|--------|-------------| -| `` `` | View branches | | +| `` `` | 檢視分支 | | | `` n `` | 新增遠端 | | -| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. | +| `` d `` | 刪除 | 刪除選中的遠端。從遠端跟蹤遠端分支的任何本地分支都不會受到影響。 | | `` e `` | 編輯 | 編輯遠端 | | `` f `` | 擷取 | 擷取遠端 | -| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. | +| `` F `` | 新增復刻遠端倉庫 | 透過替換 origin URL 中的所有者來快速新增復刻遠端倉庫,並可選擇從新遠端倉庫檢出分支。 | | `` / `` | 搜尋 | | ## 遠端分支 @@ -402,16 +402,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | Key | Action | Info | |-----|--------|-------------| | `` `` | 複製分支名稱到剪貼簿 | | -| `` `` | 檢出 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. | +| `` `` | 檢出 | 基於目前選中的遠端分支檢出一個新的本地分支,或者將遠端分支作分離的HEAD。 | | `` n `` | 新分支 | | -| `` w `` | New worktree | | -| `` M `` | 合併到當前檢出的分支 | View options for merging the selected item into the current branch (regular merge, squash merge) | -| `` r `` | 將已檢出的分支變基至此分支 | Rebase the checked-out branch onto the selected branch. | -| `` d `` | 刪除 | Delete the remote branch from the remote. | +| `` w `` | 新建工作樹 | | +| `` M `` | 合併到當前檢出的分支 | 檢視將選中項合併到目前分支的選項(正常合併,壓縮合並) | +| `` r `` | 將已檢出的分支變基至此分支 | 將檢出的分支變基到所選的分支上。 | +| `` d `` | 刪除 | 從遠端刪除遠端分支。 | | `` u `` | 設置為遠端 | 將此分支設為當前分支之遠端 | | `` s `` | 排序規則 | | -| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. | +| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` 0 `` | Focus main view | | +| `` 0 `` | 聚焦主檢視 | | | `` `` | 檢視提交 | | | `` / `` | 搜尋 | | diff --git a/go.mod b/go.mod index 7f5c1b710..6612d0756 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,9 @@ go 1.25.0 // This is necessary to ignore test files when executing gofumpt. ignore ./test +// Likewise for worktrees that are nested in the main tree. +ignore ./.worktrees + require ( dario.cat/mergo v1.0.2 github.com/adrg/xdg v0.5.3 @@ -13,7 +16,7 @@ require ( github.com/cli/go-gh/v2 v2.13.0 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 github.com/creack/pty v1.1.24 - github.com/gdamore/tcell/v3 v3.4.1 + github.com/gdamore/tcell/v3 v3.4.2 github.com/go-errors/errors v1.5.1 github.com/gookit/color v1.6.1 github.com/integrii/flaggy v1.8.0 @@ -22,7 +25,7 @@ require ( github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3 github.com/kyokomi/emoji/v2 v2.2.14 - github.com/lucasb-eyer/go-colorful v1.4.0 + github.com/lucasb-eyer/go-colorful v1.4.1 github.com/mgutz/str v1.2.0 github.com/mitchellh/go-ps v1.0.0 github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe @@ -31,12 +34,12 @@ require ( github.com/samber/lo v1.53.0 github.com/sanity-io/litter v1.5.8 github.com/sasha-s/go-deadlock v0.3.9 - github.com/sirupsen/logrus v1.9.4 + github.com/sirupsen/logrus v1.10.2 github.com/spf13/afero v1.15.0 github.com/spkg/bom v1.0.1 github.com/stefanhaller/git-todo-parser v0.0.7-0.20250905083220-c50528f08304 - github.com/stretchr/testify v1.11.1 - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e + github.com/stretchr/testify v1.12.1 + github.com/xo/terminfo v1.0.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 @@ -50,11 +53,9 @@ require ( github.com/cli/safeexec v1.0.1 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.9.0 // indirect github.com/gdamore/encoding v1.0.1 // indirect github.com/go-logfmt/logfmt v0.5.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/hpcloud/tail v1.0.0 // indirect github.com/invopop/jsonschema v0.10.0 // indirect github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect @@ -63,16 +64,16 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/onsi/ginkgo v1.10.3 // indirect github.com/onsi/gomega v1.34.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - golang.org/x/mod v0.37.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/mod v0.38.0 // indirect golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.40.0 // indirect - golang.org/x/tools v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect + golang.org/x/tools v0.48.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - mvdan.cc/gofumpt v0.9.2 // indirect + mvdan.cc/gofumpt v0.11.0 // indirect ) tool mvdan.cc/gofumpt diff --git a/go.sum b/go.sum index 9d99088d6..3c4b8793b 100644 --- a/go.sum +++ b/go.sum @@ -25,22 +25,20 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= -github.com/gdamore/tcell/v3 v3.4.1 h1:22227t1EUwqxTlmCX9vw0RUE2IEPGw6oYcNan+bPe4w= -github.com/gdamore/tcell/v3 v3.4.1/go.mod h1:YWwuxZNi14VGQC5g2VGNEDRXpBraTwvVjMovRH6G6hw= +github.com/gdamore/tcell/v3 v3.4.2 h1:gGW+6z2Bz5Wl2mNwFlm9+eRmg2JQrWcKjSkL1LRfpNU= +github.com/gdamore/tcell/v3 v3.4.2/go.mod h1:Oe5U3S3jm3NzypswDNUhe+LUnF5CoFq2b4sepD++QHo= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474= +github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= @@ -75,8 +73,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyokomi/emoji/v2 v2.2.14 h1:YOF6VL52613M0Qr9v4puJDD9QQPmyyjXedDDlrGzH80= github.com/kyokomi/emoji/v2 v2.2.14/go.mod h1:1AnYl9IgmJZXKd5m1PEijyyUw85SqYsuAr8lpU/s+9s= -github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= -github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= +github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -100,12 +98,11 @@ github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeD github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe h1:vHpqOnPlnkba8iSxU4j/CvDSS9J4+F4473esQsYLGoE= github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= +github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/sahilm/fuzzy v0.1.3 h1:juByESSS32nVD81vr6tHmKmA/8zde7gE+x5CLxrzXPU= github.com/sahilm/fuzzy v0.1.3/go.mod h1:au6//VbVSqu6DFrkL2CfjlJ5iURpNCPeE+1GwY3XsT8= github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= @@ -114,8 +111,8 @@ github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rY github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w= github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= +github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spkg/bom v1.0.1 h1:tl8kQ2sufL/wDEJa9me1jnQYEpDB7LqYGNkwCVR5GLs= @@ -125,28 +122,30 @@ github.com/stefanhaller/git-todo-parser v0.0.7-0.20250905083220-c50528f08304/go. github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY= +github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -175,14 +174,14 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -196,5 +195,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= -mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= +mvdan.cc/gofumpt v0.11.0 h1:0H01XB95PnN2QgCSR9ELdZyTlJqNZ7181B0BTMh5VZc= +mvdan.cc/gofumpt v0.11.0/go.mod h1:BeT5wCsOJt6J9zT2MZIOGszjUHzFkn1/l9g6xAzqsXo= diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go index a9cee6494..ad62f9e54 100644 --- a/pkg/cheatsheet/generate.go +++ b/pkg/cheatsheet/generate.go @@ -19,12 +19,12 @@ import ( "strings" "github.com/jesseduffield/generics/maps" - "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/app" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -49,7 +49,7 @@ func CommandToRun() string { } func GetKeybindingsDir() string { - return utils.GetLazyRootDirectory() + "/docs-master/keybindings" + return utils.MustFindLazygitRootDirectory() + "/docs-master/keybindings" } func generateAtDir(cheatsheetDir string) { @@ -196,7 +196,7 @@ func getHeader(binding *types.Binding, tr *i18n.TranslationSet) header { func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection) string { var content strings.Builder - content.WriteString(fmt.Sprintf("# Lazygit %s\n", tr.Keybindings)) + fmt.Fprintf(&content, "# Lazygit %s\n", tr.Keybindings) for _, section := range bindingSections { content.WriteString(formatTitle(section.title)) diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index 8b79bd8cd..381dd641e 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -174,7 +174,7 @@ func (self *CommitLoader) MergeRebasingCommits(hashPool *utils.StringPool, commi } if workingTreeState.Rebasing { - rebasingCommits, err := self.getHydratedRebasingCommits(hashPool, addConflictedRebasingCommit) + rebasingCommits, err := self.getHydratedRebasingCommits(hashPool, commits, addConflictedRebasingCommit) if err != nil { return nil, err } @@ -251,8 +251,8 @@ func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line }) } -func (self *CommitLoader) getHydratedRebasingCommits(hashPool *utils.StringPool, addConflictingCommit bool) ([]*models.Commit, error) { - return self.getHydratedTodoCommits(hashPool, self.getRebasingCommits(hashPool, addConflictingCommit), false) +func (self *CommitLoader) getHydratedRebasingCommits(hashPool *utils.StringPool, existingCommits []*models.Commit, addConflictingCommit bool) ([]*models.Commit, error) { + return self.getHydratedTodoCommits(hashPool, self.getRebasingCommits(hashPool, addConflictingCommit), existingCommits, false) } func (self *CommitLoader) getHydratedSequencerCommits(hashPool *utils.StringPool, workingTreeState models.WorkingTreeState) ([]*models.Commit, error) { @@ -271,39 +271,56 @@ func (self *CommitLoader) getHydratedSequencerCommits(hashPool *utils.StringPool } } - return self.getHydratedTodoCommits(hashPool, commits, true) + return self.getHydratedTodoCommits(hashPool, commits, nil, true) } -func (self *CommitLoader) getHydratedTodoCommits(hashPool *utils.StringPool, todoCommits []*models.Commit, todoFileHasShortHashes bool) ([]*models.Commit, error) { +func (self *CommitLoader) getHydratedTodoCommits( + hashPool *utils.StringPool, + todoCommits []*models.Commit, + existingCommits []*models.Commit, + todoFileHasShortHashes bool, +) ([]*models.Commit, error) { if len(todoCommits) == 0 { return nil, nil } - commitHashes := lo.FilterMap(todoCommits, func(commit *models.Commit, _ int) (string, bool) { - return commit.Hash(), commit.Hash() != "" - }) - - // note that we're not filtering these as we do non-rebasing commits just because - // I suspect that will cause some damage - cmdObj := self.cmd.New( - NewGitCmd("show"). - Config("log.showSignature=false"). - Arg("--no-patch", "--oneline", "--abbrev=20", prettyFormat). - Arg(commitHashes...). - ToArgv(), - ).DontLog() - + // A refresh of only the rebasing todos should reuse the already loaded todos to avoid + // unnecessary git show calls. fullCommits := map[string]*models.Commit{} - err := cmdObj.RunAndProcessLines(func(line string) (bool, error) { - if line == "" || line[0] != '+' { - return false, nil + for _, commit := range existingCommits { + if commit.IsTODO() && commit.Hash() != "" { + // Make a copy of the commit; that's necessary to avoid mutating the original commit + // when we later reuse it in the loop at the end of this function. + fullCommits[commit.Hash()] = lo.ToPtr(*commit) } - commit := self.extractCommitFromLine(hashPool, line[1:], false) - fullCommits[commit.Hash()] = commit - return false, nil + } + + commitHashesToFetch := lo.FilterMap(todoCommits, func(commit *models.Commit, _ int) (string, bool) { + return commit.Hash(), commit.Hash() != "" && fullCommits[commit.Hash()] == nil }) - if err != nil { - return nil, err + + if len(commitHashesToFetch) > 0 { + // note that we're not filtering these as we do non-rebasing commits just because + // I suspect that will cause some damage + cmdObj := self.cmd.New( + NewGitCmd("show"). + Config("log.showSignature=false"). + Arg("--no-patch", "--oneline", "--abbrev=20", prettyFormat). + Arg(commitHashesToFetch...). + ToArgv(), + ).DontLog() + + err := cmdObj.RunAndProcessLines(func(line string) (bool, error) { + if line == "" || line[0] != '+' { + return false, nil + } + commit := self.extractCommitFromLine(hashPool, line[1:], false) + fullCommits[commit.Hash()] = commit + return false, nil + }) + if err != nil { + return nil, err + } } findFullCommit := lo.Ternary(todoFileHasShortHashes, diff --git a/pkg/commands/git_commands/commit_loader_test.go b/pkg/commands/git_commands/commit_loader_test.go index 7f9873b0b..d26119720 100644 --- a/pkg/commands/git_commands/commit_loader_test.go +++ b/pkg/commands/git_commands/commit_loader_test.go @@ -538,6 +538,110 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) { } } +func TestCommitLoaderGetHydratedTodoCommitsReusesExistingCommit(t *testing.T) { + hashPool := &utils.StringPool{} + runner := oscommands.NewFakeRunner(t) + loader := &CommitLoader{ + cmd: oscommands.NewDummyCmdObjBuilder(runner), + } + existingCommit := models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: "0123456789012345678901234567890123456789", + Name: "hydrated subject", + AuthorName: "Jane Doe", + AuthorEmail: "jane@example.com", + UnixTimestamp: 1234, + Parents: []string{"1123456789012345678901234567890123456789"}, + Status: models.StatusRebasing, + Action: todo.Pick, + }) + refreshedTodo := models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: existingCommit.Hash(), + Name: "subject from the todo file", + Status: models.StatusConflicted, + Action: todo.Fixup, + ActionFlag: "-C", + }) + + commits, err := loader.getHydratedTodoCommits( + hashPool, + []*models.Commit{refreshedTodo}, + []*models.Commit{existingCommit}, + false, + ) + + assert.NoError(t, err) + assert.Equal(t, []*models.Commit{ + models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: existingCommit.Hash(), + Name: "hydrated subject", + AuthorName: "Jane Doe", + AuthorEmail: "jane@example.com", + UnixTimestamp: 1234, + Parents: []string{"1123456789012345678901234567890123456789"}, + Status: models.StatusConflicted, + Action: todo.Fixup, + ActionFlag: "-C", + }), + }, commits) + assert.Equal(t, todo.Pick, existingCommit.Action) + assert.Equal(t, models.StatusRebasing, existingCommit.Status) + runner.CheckForMissingCalls() +} + +func TestCommitLoaderGetHydratedTodoCommitsLoadsMissingCommit(t *testing.T) { + hashPool := &utils.StringPool{} + existingHash := "0123456789012345678901234567890123456789" + missingHash := "2123456789012345678901234567890123456789" + missingCommitOutput := strings.ReplaceAll( + `+2123456789012345678901234567890123456789|1235|John Doe|john@example.com||>|tag: new|new subject`, + "|", + "\x00", + ) + runner := oscommands.NewFakeRunner(t).ExpectGitArgs( + []string{ + "-c", "log.showSignature=false", "show", "--no-patch", "--oneline", "--abbrev=20", + prettyFormat, missingHash, + }, + missingCommitOutput, + nil, + ) + loader := &CommitLoader{ + cmd: oscommands.NewDummyCmdObjBuilder(runner), + } + existingCommit := models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: existingHash, + Name: "existing subject", + Status: models.StatusRebasing, + Action: todo.Pick, + }) + refreshedTodos := []*models.Commit{ + models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: existingHash, + Status: models.StatusRebasing, + Action: todo.Pick, + }), + models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: missingHash, + Status: models.StatusRebasing, + Action: todo.Edit, + }), + } + + commits, err := loader.getHydratedTodoCommits( + hashPool, + refreshedTodos, + []*models.Commit{existingCommit}, + false, + ) + + assert.NoError(t, err) + assert.Len(t, commits, 2) + assert.Equal(t, "existing subject", commits[0].Name) + assert.Equal(t, "new subject", commits[1].Name) + assert.Equal(t, todo.Edit, commits[1].Action) + runner.CheckForMissingCalls() +} + func TestCommitLoader_setCommitStatuses(t *testing.T) { type scenario struct { testName string diff --git a/pkg/commands/git_commands/file_loader.go b/pkg/commands/git_commands/file_loader.go index 3f960cb30..747572b38 100644 --- a/pkg/commands/git_commands/file_loader.go +++ b/pkg/commands/git_commands/file_loader.go @@ -2,7 +2,6 @@ package git_commands import ( "fmt" - "path/filepath" "strconv" "strings" @@ -91,26 +90,6 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File self.setConflictMarkerSizes(files) - // Go through the files to see if any of these files are actually worktrees - // so that we can render them correctly - worktreePaths := linkedWortkreePaths(self.Fs, self.repoPaths.RepoGitDirPath()) - for _, file := range files { - for _, worktreePath := range worktreePaths { - absFilePath, err := filepath.Abs(file.Path) - if err != nil { - self.Log.Error(err) - continue - } - if absFilePath == worktreePath { - file.IsWorktree = true - // `git status` renders this worktree as a folder with a trailing slash but we'll represent it as a singular worktree - // If we include the slash, it will be rendered as a folder with a null file inside. - file.Path = strings.TrimSuffix(file.Path, "/") - break - } - } - } - return files } diff --git a/pkg/commands/git_commands/github.go b/pkg/commands/git_commands/github.go index 3a6146923..2b0568685 100644 --- a/pkg/commands/git_commands/github.go +++ b/pkg/commands/git_commands/github.go @@ -6,6 +6,8 @@ import ( "fmt" "io" "net/http" + "os" + "os/exec" "regexp" "strings" "time" @@ -160,9 +162,51 @@ func fetchPullRequestsQuery(branches []string, owner string, repo string) (strin return queryString, variables } +// GetAuthToken returns the token to authenticate against the given host with, +// or an empty string if there is none. +// +// The token has to come from gh itself rather than from an in-process lookup +// with go-gh: that reads gh's config file once per process and answers from +// that snapshot ever after, whereas gh rewrites the file whenever the active +// account changes, and keeps the active account's token either there or in the +// system keyring. Under a long-running lazygit the snapshot therefore drifts +// out of date, leaving us with a token for an account that is no longer active, +// or with no token at all. func (self *GitHubCommands) GetAuthToken(host string) string { - token, _ := auth.TokenForHost(host) - return token + ghExe := ghExecutable() + if ghExe == "" { + // Without gh installed, the environment variables and config file that + // gh would have consulted are still worth a look. + token, _ := auth.TokenFromEnvOrConfig(host) + return token + } + + cmdArgs := []string{ghExe, "auth", "token", "--hostname", host} + output, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs() + if err != nil { + // Not being logged in to this host is a normal state rather than + // something to report; the runner logs gh's stderr for the rest. + return "" + } + + return strings.TrimSpace(output) +} + +// ghExecutable returns the path of the gh binary, or an empty string if it +// isn't installed. +func ghExecutable() string { + if ghExe := os.Getenv("GH_PATH"); ghExe != "" { + return ghExe + } + + // A gh found in the current directory rather than on PATH comes back as + // exec.ErrDot, which we treat as not having found one at all. + ghExe, err := exec.LookPath("gh") + if err != nil { + return "" + } + + return ghExe } // FetchRecentPRs fetches recent pull requests using GraphQL. serviceInfo diff --git a/pkg/commands/git_commands/repo_paths.go b/pkg/commands/git_commands/repo_paths.go index 61afc943f..0473f8f8e 100644 --- a/pkg/commands/git_commands/repo_paths.go +++ b/pkg/commands/git_commands/repo_paths.go @@ -1,7 +1,6 @@ package git_commands import ( - ioFs "io/fs" "os" "path/filepath" "strings" @@ -10,7 +9,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/env" "github.com/jesseduffield/lazygit/pkg/utils" - "github.com/spf13/afero" ) type RepoPaths struct { @@ -302,41 +300,3 @@ func runGitRevParse(gitCmd *oscommands.CmdObj) (string, error) { } return strings.TrimSpace(res), nil } - -// Returns the paths of linked worktrees -func linkedWortkreePaths(fs afero.Fs, repoGitDirPath string) []string { - result := []string{} - // For each directory in this path we're going to cat the `gitdir` file and append its contents to our result - // That file points us to the `.git` file in the worktree. - worktreeGitDirsPath := filepath.Join(repoGitDirPath, "worktrees") - - // ensure the directory exists - _, err := fs.Stat(worktreeGitDirsPath) - if err != nil { - return result - } - - _ = afero.Walk(fs, worktreeGitDirsPath, func(currPath string, info ioFs.FileInfo, err error) error { - if err != nil { - return err - } - - if !info.IsDir() { - return nil - } - - gitDirPath := filepath.Join(currPath, "gitdir") - gitDirBytes, err := afero.ReadFile(fs, gitDirPath) - if err != nil { - // ignoring error - return nil - } - trimmedGitDir := strings.TrimSpace(string(gitDirBytes)) - // removing the .git part - worktreeDir := filepath.Dir(trimmedGitDir) - result = append(result, worktreeDir) - return nil - }) - - return result -} diff --git a/pkg/commands/git_commands/working_tree.go b/pkg/commands/git_commands/working_tree.go index 1f9d9653b..846b359b3 100644 --- a/pkg/commands/git_commands/working_tree.go +++ b/pkg/commands/git_commands/working_tree.go @@ -385,27 +385,22 @@ func (self *WorkingTreeCommands) Exclude(filename string) error { // WorktreeFileDiff returns the diff of a file func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool, cached bool) string { // for now we assume an error means the file was deleted - s, _ := self.WorktreeFileDiffCmdObj(file, plain, cached, nil).RunWithOutput() + s, _ := self.WorktreeFileDiffCmdObj(file, plain, cached, file.Names()).RunWithOutput() return s } -// WorktreeFileDiffCmdObj returns a command object for diffing a file or directory -// in the working tree. When pathOverrides is non-empty, those paths are used instead of -// the node's path (used to diff only filtered/visible files within a directory). -func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, pathOverrides []string) *oscommands.CmdObj { +// WorktreeFileDiffCmdObj returns a command object for diffing the given paths +// in the working tree. node is the item they belong to; all it decides is +// whether git has to compare against /dev/null, which is the case for a file +// that isn't in the index yet. +func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, paths []string) *oscommands.CmdObj { colorArg := self.diffRendererConfigManager.GetColorArg() if plain { colorArg = "never" } - prevPath := node.GetPreviousPath() noIndex := !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached && node.GetIsFile() - paths := pathOverrides - if len(paths) == 0 { - paths = []string{node.GetPath()} - } - cmdArgs := NewGitCmd("diff"). AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), !plain). Arg("--submodule"). @@ -415,7 +410,6 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain Arg("--"). ArgIf(noIndex, "/dev/null"). Arg(paths...). - ArgIf(prevPath != "", prevPath). Dir(self.repoPaths.worktreePath). ToArgv() diff --git a/pkg/commands/oscommands/pty_windows.go b/pkg/commands/oscommands/pty_windows.go index e645ae627..ff707c519 100644 --- a/pkg/commands/oscommands/pty_windows.go +++ b/pkg/commands/oscommands/pty_windows.go @@ -204,7 +204,8 @@ func (p *winPty) Close() error { // slave closes on child exit, but ConPTY keeps the pipe alive until we call // ClosePseudoConsole explicitly. Without doing that on child exit, the // scanner in pkg/tasks.NewCmdTask would block forever on the next read and -// the post-content view never gets cleared (FlushStaleCells never fires). +// the render would never reach its end of input, so the new content would +// never be swapped in. func startWaiter(proc *os.Process, p *winPty) func() error { done := make(chan struct{}) var waitErr error diff --git a/pkg/commands/patch/hunk.go b/pkg/commands/patch/hunk.go index 6d0177d05..568b312a7 100644 --- a/pkg/commands/patch/hunk.go +++ b/pkg/commands/patch/hunk.go @@ -44,7 +44,8 @@ func (self *Hunk) lineCount() int { // Returns all lines in the hunk, including the header line func (self *Hunk) allLines() []*PatchLine { - lines := []*PatchLine{{Content: self.formatHeaderLine(), Kind: HUNK_HEADER}} + lines := make([]*PatchLine, 1, 1+len(self.bodyLines)) + lines[0] = &PatchLine{Content: self.formatHeaderLine(), Kind: HUNK_HEADER} lines = append(lines, self.bodyLines...) return lines } diff --git a/pkg/config/editor_presets.go b/pkg/config/editor_presets.go index 5fcde97c5..c101236ee 100644 --- a/pkg/config/editor_presets.go +++ b/pkg/config/editor_presets.go @@ -50,13 +50,13 @@ type editPreset struct { suspend func() bool } -func returnBool(a bool) func() bool { return (func() bool { return a }) } +func returnBool(a bool) func() bool { return func() bool { return a } } // IF YOU ADD A PRESET TO THIS FUNCTION YOU MUST UPDATE THE `Supported presets` SECTION OF docs/Config.md func getPreset(shell string, osConfig *OSConfig, guessDefaultEditor func() string) *editPreset { var nvimRemoteEditTemplate, nvimRemoteEditAtLineTemplate, nvimRemoteOpenDirInEditorTemplate string // By default fish doesn't have SHELL variable set, but it does have FISH_VERSION since Nov 2012. - if (strings.HasSuffix(shell, "fish")) || (os.Getenv("FISH_VERSION") != "") { + if strings.HasSuffix(shell, "fish") || (os.Getenv("FISH_VERSION") != "") { nvimRemoteEditTemplate = `begin; if test -z "$NVIM"; nvim -- {{filename}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{filename}}; end; end` nvimRemoteEditAtLineTemplate = `begin; if test -z "$NVIM"; nvim +{{line}} -- {{filename}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{filename}}; nvim --server "$NVIM" --remote-send ":{{line}}"; end; end` nvimRemoteOpenDirInEditorTemplate = `begin; if test -z "$NVIM"; nvim -- {{dir}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{dir}}; end; end` diff --git a/pkg/config/user_config_validation.go b/pkg/config/user_config_validation.go index 81d2792ff..3f836a21b 100644 --- a/pkg/config/user_config_validation.go +++ b/pkg/config/user_config_validation.go @@ -191,6 +191,51 @@ func validateCustomCommandKey(key Keybinding) error { return nil } +// ValidCustomCommandContexts lists the names a custom command's 'context' may +// use. It mirrors context.AllContextKeys in the gui package, which this package +// can't import; a test over there keeps the two in sync. +var ValidCustomCommandContexts = []string{ + "global", + "status", + "files", + "localBranches", + "remotes", + "worktrees", + "remoteBranches", + "tags", + "commits", + "reflogCommits", + "subCommits", + "commitFiles", + "stash", + "normal", + "normalSecondary", + "staging", + "stagingSecondary", + "patchBuilding", + "patchBuildingSecondary", + "mergeConflicts", + "menu", + "confirmation", + "prompt", + "search", + "commitMessage", + "submodules", + "suggestions", + "cmdLog", +} + +func validateCustomCommandContext(context string) error { + for _, name := range strings.Split(context, ",") { + name = strings.TrimSpace(name) + if !slices.Contains(ValidCustomCommandContexts, name) { + return fmt.Errorf("Unknown context '%s' for custom command. Allowed values: %s", + name, strings.Join(ValidCustomCommandContexts, ", ")) + } + } + return nil +} + func validateCustomCommands(customCommands []CustomCommand) error { for _, customCommand := range customCommands { if err := validateCustomCommandKey(customCommand.Key); err != nil { @@ -216,6 +261,15 @@ func validateCustomCommands(customCommands []CustomCommand) error { return err } } else { + // A command in a menu may leave the context out, in which case it is + // offered whatever is focused; a top-level one may not, but that is + // only noticed when the keybindings are built. + if customCommand.Context != "" { + if err := validateCustomCommandContext(customCommand.Context); err != nil { + return err + } + } + for _, prompt := range customCommand.Prompts { if err := validateCustomCommandPrompt(prompt); err != nil { return err diff --git a/pkg/config/user_config_validation_test.go b/pkg/config/user_config_validation_test.go index 370818718..7977e3e4c 100644 --- a/pkg/config/user_config_validation_test.go +++ b/pkg/config/user_config_validation_test.go @@ -225,6 +225,43 @@ func TestUserConfigValidate_enums(t *testing.T) { {value: "invalid_value", valid: false}, }, }, + { + name: "Custom command context", + setup: func(config *UserConfig, value string) { + config.CustomCommands = []CustomCommand{ + { + Context: value, + }, + } + }, + testCases: []testCase{ + {value: "", valid: true}, + {value: "global", valid: true}, + {value: "commits", valid: true}, + {value: "commits, subCommits", valid: true}, + {value: "commits,subCommits", valid: true}, + {value: "invalid_value", valid: false}, + {value: "commits, invalid_value", valid: false}, + }, + }, + { + name: "Custom command context in a sub menu", + setup: func(config *UserConfig, value string) { + config.CustomCommands = []CustomCommand{ + { + Key: Keybinding{"X"}, + CommandMenu: []CustomCommand{ + {Key: Keybinding{"1"}, Command: "echo 'hello'", Context: value}, + }, + }, + } + }, + testCases: []testCase{ + {value: "", valid: true}, + {value: "commits", valid: true}, + {value: "invalid_value", valid: false}, + }, + }, { name: "Custom command sub menu", setup: func(config *UserConfig, _ string) { diff --git a/pkg/gocui/double_click_test.go b/pkg/gocui/double_click_test.go index b8d9f5f9c..9c73da1e9 100644 --- a/pkg/gocui/double_click_test.go +++ b/pkg/gocui/double_click_test.go @@ -13,14 +13,14 @@ func TestMouseReleaseDoesNotBreakDoubleClickDetection(t *testing.T) { g := newTestGui(t) view, _ := g.SetView("list", 0, 0, 20, 10, 0) doubleClicks := []bool{} - assert.NoError(t, g.SetViewClickBinding(&ViewMouseBinding{ + g.SetViewClickBinding(&ViewMouseBinding{ ViewName: "list", Key: MouseLeft, Handler: func(opts ViewMouseBindingOpts) error { doubleClicks = append(doubleClicks, opts.IsDoubleClick) return nil }, - })) + }) for _, event := range []GocuiEvent{ gocuiEventFromTcellEvent(tcell.NewEventMouse(view.x0+1, view.y0+1, tcell.ButtonPrimary, tcell.ModNone)), diff --git a/pkg/gocui/edit.go b/pkg/gocui/edit.go index 649379fb6..4263e0b5c 100644 --- a/pkg/gocui/edit.go +++ b/pkg/gocui/edit.go @@ -78,7 +78,7 @@ func SimpleEditor(v *View, key Key) bool { v.TextArea.GoToEndOfLine() case key.Equals(NewKeyStrMod("y", ModCtrl)): v.TextArea.Yank() - case key.Str() != "" && key.Mod() == 0: + case key.IsPrintable(): v.TextArea.TypeCharacter(key.Str()) default: return false diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index be52c8584..67fbd1aa2 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -38,6 +38,11 @@ var ( // ErrKeybindingNotHandled is returned when a keybinding is not handled, so that the key can be dispatched further ErrKeybindingNotHandled = standardErrors.New("keybinding not handled") + + // ErrLoopExited is returned by OnUIThreadAndWait when MainLoop has already + // returned. Nothing dequeues user events after that, so the callback it was + // asked to run on the main goroutine never will be. + ErrLoopExited = standardErrors.New("main loop exited") ) const ( @@ -217,6 +222,11 @@ type Gui struct { // worker goroutines, so it's atomic. uiThreadID atomic.Int64 + // focused says whether the terminal we're running in has focus, as far as + // its focus reports tell us (see IsFocused). Written by the event loop, + // readable from anywhere, so it's atomic. + focused atomic.Bool + // blockInputCount, when greater than zero, withholds keyboard input from // the handlers: key events are buffered into bufferedKeyEvents and replayed // once the count drops back to zero, while mouse clicks and hover are @@ -301,6 +311,12 @@ func NewGui(opts NewGuiOpts) (*Gui, error) { // runs during startup, before we reach MainLoop. g.uiThreadID.Store(goid.Get()) + // Assume we start out focused: a terminal that supports focus reports sends + // one for the state it is already in when we turn reporting on in MainLoop, + // and passing that on as a change would have the app react to a change that + // never happened. + g.focused.Store(true) + return g, nil } @@ -659,19 +675,15 @@ func (g *Gui) DeleteViewKeybindings(viewname string) { } // SetTabClickBinding sets a binding for a tab click event -func (g *Gui) SetTabClickBinding(viewName string, handler tabClickHandler) error { +func (g *Gui) SetTabClickBinding(viewName string, handler tabClickHandler) { g.tabClickBindings = append(g.tabClickBindings, &tabClickBinding{ viewName: viewName, handler: handler, }) - - return nil } -func (g *Gui) SetViewClickBinding(binding *ViewMouseBinding) error { +func (g *Gui) SetViewClickBinding(binding *ViewMouseBinding) { g.viewMouseBindings = append(g.viewMouseBindings, binding) - - return nil } // captureMouse routes subsequent mouse events to view until the mouse button is @@ -893,36 +905,50 @@ func (g *Gui) EndBlockingEvents() error { } // OnUIThreadAndWait runs f on the main event-loop goroutine and blocks the -// caller until f has run, returning f's error. Use it to read UI-thread-owned -// state (the model, contexts) from a worker without racing the UI thread. +// caller until f has run. Use it to read UI-thread-owned state (the model, +// contexts) from a worker without racing the UI thread. +// +// The error it returns is the wait's own, never f's: it reports that f was not +// run at all, which happens when the main loop has exited (ErrLoopExited). f +// doesn't report an error because what callers want on the UI thread — reading +// and mutating state — doesn't fail. // // It must be called from a worker goroutine, never from the UI thread itself: // the UI thread would block waiting for a callback only it can run, which // deadlocks. Callers arrange this by construction (see the refresh helper's // RefreshFromWorker); a debug-only assertion there guards against getting it // wrong. -func (g *Gui) OnUIThreadAndWait(f func() error) error { +func (g *Gui) OnUIThreadAndWait(f func()) error { return g.onUIThreadAndWait(f, false) } // Like OnUIThreadAndWait, but the enqueued work belongs to a background routine, // so it doesn't count towards the program being busy (see UpdateBackground). -func (g *Gui) OnUIThreadAndWaitBackground(f func() error) error { +func (g *Gui) OnUIThreadAndWaitBackground(f func()) error { return g.onUIThreadAndWait(f, true) } -func (g *Gui) onUIThreadAndWait(f func() error, background bool) error { +func (g *Gui) onUIThreadAndWait(f func(), background bool) error { enqueue := g.Update if background { enqueue = g.UpdateBackground } - result := make(chan error, 1) + ran := make(chan struct{}) enqueue(func(*Gui) error { - result <- f() + f() + close(ran) return nil }) - return <-result + + select { + case <-ran: + return nil + case <-g.loopExited: + // The queue we just enqueued onto is no longer being served, so waiting + // on `ran` here would mean waiting for the rest of the process's life. + return ErrLoopExited + } } // Calls a function in a goroutine. Handles panics gracefully and tracks @@ -1246,7 +1272,7 @@ func calcScrollbarRune( func calcRealScrollbarStartEnd(v *View) (bool, int, int) { height := v.InnerHeight() - fullHeight := v.ViewLinesHeight() - v.scrollMargin() + fullHeight := v.scrollbarContentHeight() - v.scrollMargin() if v.CanScrollPastBottom { fullHeight += height @@ -1428,7 +1454,7 @@ func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error { currentBgColor = v.BgColor } - if i >= currentTabStart && i <= currentTabEnd { + if i >= currentTabStart && i <= currentTabEnd && g.IsFocused() { currentFgColor = v.SelFgColor if v != g.currentView { currentFgColor &= ^AttrBold @@ -1467,7 +1493,7 @@ func (g *Gui) drawSubtitle(v *View, fgColor, bgColor Attribute) error { // drawListFooter draws the footer of a list view, showing something like '1 of 10' func (g *Gui) drawListFooter(v *View, fgColor, bgColor Attribute) error { - if len(v.lines) == 0 { + if len(v.buf.lines) == 0 { return nil } @@ -1589,6 +1615,20 @@ func (g *Gui) ForceFlushViewsContentOnly(views []*View) error { return g.flushContentOnly(views) } +// hasFocus reports whether a view is drawn as focused. Views that are embedded +// in one another (see View.ParentView) form a single unit, so they are all drawn +// as focused while any one of them is the current view. +func (g *Gui) hasFocus(v *View) bool { + return g.currentView != nil && outermostView(v) == outermostView(g.currentView) +} + +func outermostView(v *View) *View { + for v.ParentView != nil { + v = v.ParentView + } + return v +} + // draw manages the cursor and calls the draw function of a view. func (g *Gui) draw(v *View) error { if !v.Visible || v.y1 < v.y0 || v.x1 < v.x0 { @@ -1609,11 +1649,11 @@ func (g *Gui) draw(v *View) error { Screen.HideCursor() } - v.draw() + v.draw(g.IsFocused()) if v.Frame { var fgColor, bgColor, frameColor Attribute - if g.Highlight && v == g.currentView { + if g.Highlight && g.hasFocus(v) && g.IsFocused() { fgColor = g.SelFgColor bgColor = g.SelBgColor frameColor = g.SelFrameColor @@ -1717,13 +1757,13 @@ func (g *Gui) onKey(ev *GocuiEvent) error { if newY < 0 { newY = 0 newCy = -v.oy - } else if newY >= len(v.lines) { - newY = len(v.lines) - 1 + } else if newY >= len(v.buf.lines) { + newY = len(v.buf.lines) - 1 newCy = newY - v.oy } visibleLineWidth := 0 - for _, c := range v.lines[newY].cells { + for _, c := range v.buf.lines[newY].cells { visibleLineWidth += c.width } if visibleLineWidth < newX { @@ -1733,10 +1773,8 @@ func (g *Gui) onKey(ev *GocuiEvent) error { } if ev.Key.KeyName() == MouseLeft && (ev.Key.Mod()&ModMotion) == 0 && !v.Editable && g.openHyperlink != nil { - if newY >= 0 && newY <= len(v.viewLines)-1 && newX >= 0 && newX <= len(v.viewLines[newY].line)-1 { - if link := v.viewLines[newY].line[newX].hyperlink; link != "" { - return g.openHyperlink(link, v.name) - } + if link := v.hyperlinkAt(newX, newY); link != "" { + return g.openHyperlink(link, v.name) } } @@ -1955,7 +1993,7 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error { matchingParentViewKb = nil break } - if v != nil && g.matchView(v.ParentView, kb) { + if matchingParentViewKb == nil && v != nil && g.matchView(v.ParentView, kb) { matchingParentViewKb = kb } if globalKb == nil && kb.viewName == "" { @@ -1990,7 +2028,21 @@ func (g *Gui) execKeybinding(v *View, kb *keybinding) error { return nil } +// IsFocused reports whether the terminal we're running in has focus. Terminals +// that don't report focus at all leave this true for good. +func (g *Gui) IsFocused() bool { + return g.focused.Load() +} + func (g *Gui) onFocus(ev *GocuiEvent) error { + // Terminals report their focus state when we turn focus reporting on, and + // some report it again when their window is activated, so only pass on the + // reports that actually change it. + if ev.Focused == g.focused.Load() { + return nil + } + g.focused.Store(ev.Focused) + if g.focusHandler != nil { return g.focusHandler(ev.Focused) } @@ -2053,13 +2105,15 @@ func (g *Gui) isSuspended() bool { return g.suspended } -// matchView returns if the keybinding matches the current view (and the view's context) +// matchView returns if the keybinding matches the given view (and the view's context) func (g *Gui) matchView(v *View, kb *keybinding) bool { - // if the user is typing in a field, ignore char keys if v == nil { return false } - if v.Editable && kb.key.Str() != "" && kb.key.Mod() == 0 { + // If the user is typing in a field, printable keys are theirs to type, so no + // keybinding gets a look at them: not the field's own, and not those of the + // view it is embedded in either. + if field := g.currentView; field != nil && field.Editable && !field.KeybindOnEdit && kb.key.IsPrintable() { return false } if kb.viewName != v.name { diff --git a/pkg/gocui/key.go b/pkg/gocui/key.go index dd0a912a0..0eaa29b38 100644 --- a/pkg/gocui/key.go +++ b/pkg/gocui/key.go @@ -61,6 +61,12 @@ func (k Key) IsSet() bool { return k.keyName != 0 } +// IsPrintable reports whether the key stands for a character that can be typed +// into a text field. +func (k Key) IsPrintable() bool { + return k.keyName == KeyName(tcell.KeyRune) && k.str != "" && k.mod == ModNone +} + func (k Key) Equals(otherKey Key) bool { return k.keyName == otherKey.keyName && k.str == otherKey.str && k.mod == otherKey.mod } diff --git a/pkg/gocui/key_test.go b/pkg/gocui/key_test.go new file mode 100644 index 000000000..8fce3fe53 --- /dev/null +++ b/pkg/gocui/key_test.go @@ -0,0 +1,16 @@ +package gocui + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKeyIsPrintable(t *testing.T) { + assert.True(t, NewKeyRune('x').IsPrintable()) + assert.True(t, NewKeyRune('界').IsPrintable()) + assert.True(t, NewKeyRune(' ').IsPrintable()) + assert.False(t, NewKeyStrMod("x", ModCtrl).IsPrintable()) + assert.False(t, NewKeyName(KeyEnter).IsPrintable()) + assert.False(t, Key{}.IsPrintable()) +} diff --git a/pkg/gocui/mouse_capture_test.go b/pkg/gocui/mouse_capture_test.go index eea1e3f9f..f335e65c4 100644 --- a/pkg/gocui/mouse_capture_test.go +++ b/pkg/gocui/mouse_capture_test.go @@ -36,7 +36,7 @@ func TestMouseCaptureRoutesMotionAndReleaseOutsideView(t *testing.T) { }, }, } { - assert.NoError(t, g.SetViewClickBinding(binding)) + g.SetViewClickBinding(binding) } g.captureMouse(view) @@ -69,7 +69,7 @@ func TestPrimaryMouseDragStaysWithPressedView(t *testing.T) { receivedBy := "" for _, viewName := range []string{"left", "right"} { - assert.NoError(t, g.SetViewClickBinding(&ViewMouseBinding{ + g.SetViewClickBinding(&ViewMouseBinding{ ViewName: viewName, Key: MouseLeft, Modifier: ModMotion, @@ -77,7 +77,7 @@ func TestPrimaryMouseDragStaysWithPressedView(t *testing.T) { receivedBy = viewName return nil }, - })) + }) } assert.NoError(t, g.onKey(&GocuiEvent{ @@ -102,10 +102,10 @@ func TestPrimaryMouseDragDoesNotActivateTabs(t *testing.T) { view.Tabs = []string{"first", "second"} clickedTabs := []int{} - assert.NoError(t, g.SetTabClickBinding("tabs", func(tabIndex int) error { + g.SetTabClickBinding("tabs", func(tabIndex int) error { clickedTabs = append(clickedTabs, tabIndex) return nil - })) + }) assert.NoError(t, g.onKey(&GocuiEvent{ Type: eventMouse, @@ -172,7 +172,7 @@ func TestCancelMouseCaptureSuppressesRemainingGesture(t *testing.T) { _, _ = g.SetView("right", 21, 0, 41, 10, 0) receivedBy := "" for _, viewName := range []string{"left", "right"} { - assert.NoError(t, g.SetViewClickBinding(&ViewMouseBinding{ + g.SetViewClickBinding(&ViewMouseBinding{ ViewName: viewName, Key: MouseLeft, Modifier: ModMotion, @@ -180,7 +180,7 @@ func TestCancelMouseCaptureSuppressesRemainingGesture(t *testing.T) { receivedBy = viewName return nil }, - })) + }) } g.captureMouse(left) diff --git a/pkg/gocui/parent_view_test.go b/pkg/gocui/parent_view_test.go new file mode 100644 index 000000000..9d56a1c46 --- /dev/null +++ b/pkg/gocui/parent_view_test.go @@ -0,0 +1,142 @@ +package gocui + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// A view and its parent view, with the child holding the focus. +func setupParentAndChildView(t *testing.T, g *Gui) (*View, *View) { + t.Helper() + + parent, _ := g.SetView("parent", 0, 0, 20, 10, 0) + child, _ := g.SetView("child", 0, 10, 20, 12, 0) + child.ParentView = parent + _, err := g.SetCurrentView(child.Name()) + assert.NoError(t, err) + + return parent, child +} + +func TestKeybindingOfParentViewIsUsedWhenChildHasNone(t *testing.T) { + g := newTestGui(t) + parent, child := setupParentAndChildView(t, g) + + pressed := []string{} + g.SetKeybinding(parent.Name(), NewKeyName(KeyArrowDown), func(*Gui, *View) error { + pressed = append(pressed, "parent") + return nil + }) + g.SetKeybinding(child.Name(), NewKeyName(KeyEnter), func(*Gui, *View) error { + pressed = append(pressed, "child") + return nil + }) + + assert.NoError(t, g.onKey(&GocuiEvent{Type: eventKey, Key: NewKeyName(KeyArrowDown)})) + assert.NoError(t, g.onKey(&GocuiEvent{Type: eventKey, Key: NewKeyName(KeyEnter)})) + + assert.Equal(t, []string{"parent", "child"}, pressed) +} + +func TestFirstMatchingKeybindingOfParentViewWins(t *testing.T) { + g := newTestGui(t) + parent, _ := setupParentAndChildView(t, g) + + pressed := []string{} + for _, name := range []string{"first", "second"} { + g.SetKeybinding(parent.Name(), NewKeyName(KeyArrowDown), func(*Gui, *View) error { + pressed = append(pressed, name) + return nil + }) + } + + assert.NoError(t, g.onKey(&GocuiEvent{Type: eventKey, Key: NewKeyName(KeyArrowDown)})) + + assert.Equal(t, []string{"first"}, pressed) +} + +func TestEmbeddedViewsAreFocusedTogether(t *testing.T) { + g := newTestGui(t) + parent, child := setupParentAndChildView(t, g) + sibling, _ := g.SetView("sibling", 0, 12, 20, 14, 0) + sibling.ParentView = parent + unrelated, _ := g.SetView("unrelated", 30, 0, 50, 10, 0) + + assert.True(t, g.hasFocus(child)) + assert.True(t, g.hasFocus(parent)) + assert.True(t, g.hasFocus(sibling)) + assert.False(t, g.hasFocus(unrelated)) + + _, err := g.SetCurrentView(unrelated.Name()) + assert.NoError(t, err) + + assert.True(t, g.hasFocus(unrelated)) + assert.False(t, g.hasFocus(parent)) + assert.False(t, g.hasFocus(child)) +} + +func TestPrintableKeysGoToTheFieldBeingTypedIn(t *testing.T) { + for _, test := range []struct { + name string + keybindOnEdit bool + declineKeybinding bool + expectedPresses int + expectedEdits int + }{ + {name: "the field gets the key", expectedEdits: 1}, + {name: "the parent view gets the key", keybindOnEdit: true, expectedPresses: 1}, + { + name: "the field gets the key the parent view declined", + keybindOnEdit: true, + declineKeybinding: true, + expectedPresses: 1, + expectedEdits: 1, + }, + } { + t.Run(test.name, func(t *testing.T) { + g := newTestGui(t) + parent, child := setupParentAndChildView(t, g) + child.Editable = true + child.KeybindOnEdit = test.keybindOnEdit + + edits := 0 + child.Editor = EditorFunc(func(*View, Key) bool { + edits++ + return true + }) + presses := 0 + g.SetKeybinding(parent.Name(), NewKeyRune('j'), func(*Gui, *View) error { + presses++ + if test.declineKeybinding { + return ErrKeybindingNotHandled + } + return nil + }) + + assert.NoError(t, g.onKey(&GocuiEvent{Type: eventKey, Key: NewKeyRune('j')})) + + assert.Equal(t, test.expectedPresses, presses) + assert.Equal(t, test.expectedEdits, edits) + }) + } +} + +func TestUnhandledKeybindingOfParentViewFallsThroughToEditor(t *testing.T) { + g := newTestGui(t) + parent, child := setupParentAndChildView(t, g) + + edited := []Key{} + child.Editable = true + child.Editor = EditorFunc(func(_ *View, key Key) bool { + edited = append(edited, key) + return true + }) + g.SetKeybinding(parent.Name(), NewKeyName(KeyArrowDown), func(*Gui, *View) error { + return ErrKeybindingNotHandled + }) + + assert.NoError(t, g.onKey(&GocuiEvent{Type: eventKey, Key: NewKeyName(KeyArrowDown)})) + + assert.Equal(t, []Key{NewKeyName(KeyArrowDown)}, edited) +} diff --git a/pkg/gocui/search_test.go b/pkg/gocui/search_test.go new file mode 100644 index 000000000..ba20da9de --- /dev/null +++ b/pkg/gocui/search_test.go @@ -0,0 +1,58 @@ +package gocui + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +// writeLines writes the given lines to the view, as a task rendering content into it +// does: one line at a time. +func writeLines(v *View, lines ...string) { + for _, line := range lines { + fmt.Fprintf(v, "%s\n", line) + } +} + +func TestSearchStatusAfterTheMatchesChange(t *testing.T) { + v := NewView("name", 0, 0, 40, 10, OutputNormal) + writeLines(v, "match", "other", "match", "other", "match") + + v.Search("match", nil) + _ = v.gotoNextMatch() + _ = v.gotoNextMatch() + index, total := v.GetSearchStatus() + assert.Equal(t, 2, index) + assert.Equal(t, 3, total) + + // The content is re-rendered with only the first of those matches left in it. + v.Clear() + writeLines(v, "match", "other", "other") + + index, total = v.GetSearchStatus() + assert.Equal(t, 0, index) + assert.Equal(t, 1, total) +} + +func TestSearchPositionsFollowStreamedContent(t *testing.T) { + v := NewView("name", 0, 0, 40, 10, OutputNormal) + v.Search("match", nil) + + // A render arrives a line at a time, and the status describes all of it. + writeLines(v, "other", "match", "other", "match") + + _, total := v.GetSearchStatus() + assert.Equal(t, 2, total) +} + +func BenchmarkWriteToSearchedView(b *testing.B) { + for b.Loop() { + v := NewView("name", 0, 0, 100, 40, OutputNormal) + v.Search("match", nil) + for i := range 2000 { + fmt.Fprintf(v, "line %d of a diff, most of which does not match\n", i) + } + v.GetSearchStatus() + } +} diff --git a/pkg/gocui/tcell_driver.go b/pkg/gocui/tcell_driver.go index 745725993..312d6d5a2 100644 --- a/pkg/gocui/tcell_driver.go +++ b/pkg/gocui/tcell_driver.go @@ -300,15 +300,15 @@ func (g *Gui) pollEvent() GocuiEvent { if g.playRecording { select { case ev := <-g.replayedEvents.Keys: - tev = (ev).toTcellEvent() + tev = ev.toTcellEvent() task = ev.task case ev := <-g.replayedEvents.Resizes: - tev = (ev).toTcellEvent() + tev = ev.toTcellEvent() case ev := <-g.replayedEvents.MouseEvents: - tev = (ev).toTcellEvent() + tev = ev.toTcellEvent() task = ev.task case ev := <-g.replayedEvents.FocusEvents: - tev = (ev).toTcellEvent() + tev = ev.toTcellEvent() task = ev.task } } else { diff --git a/pkg/gocui/ui_thread_test.go b/pkg/gocui/ui_thread_test.go new file mode 100644 index 000000000..d76bfaf9c --- /dev/null +++ b/pkg/gocui/ui_thread_test.go @@ -0,0 +1,43 @@ +package gocui + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// errStillWaiting stands in for the result of a wait that hasn't produced one. +var errStillWaiting = errors.New("still waiting") + +// resultOrTimeout reports what a wait returned, or errStillWaiting if it hasn't +// returned by the time we give up on it. +func resultOrTimeout(result chan error) error { + select { + case err := <-result: + return err + case <-time.After(time.Second): + return errStillWaiting + } +} + +// A worker waiting for the UI thread must not be left parked there once the +// main loop has stopped: nothing will ever run its callback, and the shutdown +// that follows blocks until such workers have finished (see +// tasks.ViewBufferManager.Close). +func TestOnUIThreadAndWaitGivesUpWhenTheLoopExits(t *testing.T) { + g := newTestGui(t) + + // Closing this is what MainLoop returning does. From here on nothing + // dequeues user events, so the callback below is never going to run. + close(g.loopExited) + + result := make(chan error, 1) + go func() { + result <- g.OnUIThreadAndWait(func() {}) + }() + + err := resultOrTimeout(result) + assert.ErrorIs(t, err, ErrLoopExited) +} diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index b106eb21f..0a06d1981 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -25,17 +25,51 @@ const ( RIGHT = 8 // view is overlapping at right edge ) +// viewBuffer holds a view's content as cells, together with the cursor and +// escape-sequence decoder state used to turn incoming bytes into those cells. +// A view normally has a single buffer (the one it displays), but bundling this +// state lets a re-render build a second, off-screen buffer and swap it in +// atomically once the new content is ready, so no reader ever sees a +// half-written buffer. +type viewBuffer struct { + // the view's content: one []cell per unwrapped line + lines []lineType + + // write cursor into lines + wx, wy int + + // decodes ESC sequences as bytes are written + ei *escapeInterpreter + + // If the last character written was a newline, we don't write it but instead + // set pendingNewline to true. If more text is written, we write the newline + // then. This avoids an extra blank line at the end of the view. + pendingNewline bool +} + // A View is a window. It maintains its own internal buffer and cursor // position. type View struct { name string - x0, y0, x1, y1 int // left top right bottom - ox, oy int // view offsets - cx, cy int // cursor position - rx, ry int // Read() offsets - wx, wy int // Write() offsets - lines []lineType // All the data + x0, y0, x1, y1 int // left top right bottom + ox, oy int // view offsets + cx, cy int // cursor position + rx, ry int // Read() offsets outMode OutputMode + + // buf bundles the view's cell buffer and the cursor / escape-parser state + // used to write into it (see the viewBuffer type). It is the buffer every + // reader sees. + buf *viewBuffer + + // While non-nil, writes go here instead of buf, so an async re-render can + // build its new content without disturbing what readers (draw, clicks, + // scrolling, …) see. The task swaps it into buf once it has read enough to + // paint (SwapInOffscreenRender), so the displayed content jumps straight + // from the previous render to the new one with no half-written frame in + // between. nil during normal (non-async) writes. + offscreen *viewBuffer + // The y position of the first line of a range selection. // This is not relative to the view's origin: it is relative to the first line // of the view's content, so you can scroll the view and this value will remain @@ -74,17 +108,20 @@ type View struct { // true and viewLines to nil viewLines []viewLine - // If the last character written was a newline, we don't write it but - // instead set pendingNewline to true. If more text is written, we write the - // newline then. This is to avoid having an extra blank at the end of the view. - pendingNewline bool + // While a re-render is loading new content (see offscreen), the displayed + // buffer is only partially filled once we've swapped the off-screen render + // in: the task keeps appending lines after the first paint, up to the count + // needed for an accurate scrollbar. Sizing the scrollbar from that partial + // view-line count would make the thumb shrink and snap back as the rest + // streams in. So while a load is in progress we hold the scrollbar's height + // at this value — the height the view had when the load began — and let it + // grow only if the new content turns out taller. Zero means no load is in + // progress and the scrollbar tracks the content directly. + scrollbarHeightFloor int // writeMutex protects locks the write process writeMutex sync.Mutex - // ei is used to decode ESC sequences on Write - ei *escapeInterpreter - // Visible specifies whether the view is visible. Visible bool @@ -173,7 +210,9 @@ type View struct { // Overlaps describes which edges are overlapping with another view's edges Overlaps byte - // ParentView is the view which catches events bubbled up from the given view if there's no matching handler + // ParentView is the view which catches events bubbled up from the given view if there's no matching handler. + // Views related this way are also drawn as a single focused unit: while one of + // them is the current view, they all get the focused frame and title colors. ParentView *View searcher *searcher @@ -232,6 +271,12 @@ type searcher struct { currentSearchIndex int onSelectItem func(*View, int) renderSearchStatus func(*View, int, int) + + // Whether the content has changed since the positions were worked out, so that + // they have to be worked out again before they are read. Working them out walks + // the whole view, and content arrives a line at a time, so it happens once per + // read rather than once per line written. + positionsStale bool } func (v *View) setRenderSearchStatus(renderSearchStatus func(*View, int, int)) { @@ -248,7 +293,40 @@ func (v *View) renderSearchStatus(index int, itemCount int) { } } +// refreshSearchPositions works the search positions out again if the content has +// changed since they were last worked out. Every read of the positions goes through +// this, so that no caller has to know whether the view has been drawn since the +// content it is asking about arrived. +func (v *View) refreshSearchPositions() { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + v.refreshSearchPositionsIfNeeded() +} + +// refreshSearchPositions for a caller that already holds writeMutex. +func (v *View) refreshSearchPositionsIfNeeded() { + if v.searcher.positionsStale { + v.updateSearchPositions() + } +} + +// RefreshSearch runs the search again over content the view has just been re-rendered +// with, and shows the "x of y" status of what it finds. The view stays where it is: the +// position in the content is the user's, and the search follows it rather than moving +// it. +func (v *View) RefreshSearch() { + if !v.IsSearching() { + return + } + + v.UpdateSearchResults(v.searcher.searchString, v.searcher.modelSearchResults) + v.renderSearchStatus(v.searcher.currentSearchIndex, len(v.searcher.searchPositions)) +} + func (v *View) gotoNextMatch() error { + v.refreshSearchPositions() + if len(v.searcher.searchPositions) == 0 { return nil } @@ -268,6 +346,8 @@ func (v *View) gotoNextMatch() error { } func (v *View) gotoPreviousMatch() error { + v.refreshSearchPositions() + if len(v.searcher.searchPositions) == 0 { return nil } @@ -289,6 +369,8 @@ func (v *View) gotoPreviousMatch() error { } func (v *View) SelectSearchResult(index int) { + v.refreshSearchPositions() + itemCount := len(v.searcher.searchPositions) if itemCount == 0 { return @@ -308,6 +390,8 @@ func (v *View) SelectSearchResult(index int) { // Returns , func (v *View) GetSearchStatus() (int, int) { + v.refreshSearchPositions() + return v.searcher.currentSearchIndex, len(v.searcher.searchPositions) } @@ -381,6 +465,8 @@ func (v *View) nearestSearchPosition() int { } func (v *View) SetNearestSearchPosition() { + v.refreshSearchPositions() + if len(v.searcher.searchPositions) > 0 { newPos := v.nearestSearchPosition() if newPos != v.searcher.currentSearchIndex { @@ -402,7 +488,7 @@ func (v *View) FocusPoint(cx int, cy int, scrollIntoView bool) { if scrollIntoView { height := v.InnerHeight() - v.oy = calculateNewOrigin(cy, v.oy, lineCount, height) + v.SetOriginY(calculateNewOrigin(cy, v.oy, lineCount, height)) } v.cx = cx @@ -461,7 +547,7 @@ type SearchPosition struct { } type viewLine struct { - linesX, linesY int // coordinates relative to v.lines + linesX, linesY int // coordinates relative to v.buf.lines line []cell // Colors used to extend the bg past this wrapped segment's content. @@ -470,7 +556,7 @@ type viewLine struct { trailingFillAttributes *trailingFillAttributes } -// lineType is one of v.lines: the cells of a source line, plus optional +// lineType is one of v.buf.lines: the cells of a source line, plus optional // trailingFillAttributes recording the colors used to extend the bg // past the line's content when the writer emitted '\x1b[K'. type lineType struct { @@ -536,7 +622,7 @@ func NewView(name string, x0, y0, x1, y1 int, mode OutputMode) *View { Editor: DefaultEditor, tainted: true, outMode: mode, - ei: newEscapeInterpreter(mode), + buf: &viewBuffer{ei: newEscapeInterpreter(mode)}, searcher: &searcher{}, TextArea: &TextArea{}, rangeSelectStartY: -1, @@ -547,7 +633,7 @@ func NewView(name string, x0, y0, x1, y1 int, mode OutputMode) *View { v.SelFgColor, v.SelBgColor = ColorDefault, ColorDefault v.InactiveViewSelBgColor = ColorDefault v.TitleColor, v.FrameColor = ColorDefault, ColorDefault - v.ei.screenColMax = v.InnerWidth() + v.buf.ei.screenColMax = v.InnerWidth() return v } @@ -558,7 +644,7 @@ func NewView(name string, x0, y0, x1, y1 int, mode OutputMode) *View { // content can consult this snapshot instead of reading the view's live // dimensions (which the UI thread mutates during layout). func (v *View) SetContentWidth(width int) { - v.ei.screenColMax = width + v.buf.ei.screenColMax = width } // Dimensions returns the dimensions of the View @@ -616,7 +702,7 @@ func (v *View) Name() string { // setCharacter sets a character (grapheme cluster) at the given point relative to the view. It applies // the specified colors, taking into account if the cell must be highlighted. Also, it checks if the // position is valid. -func (v *View) setCharacter(x, y int, ch string, fgColor, bgColor Attribute) { +func (v *View) setCharacter(x, y int, ch string, fgColor, bgColor Attribute, isWindowFocused bool) { maxX, maxY := v.Size() if x < 0 || x >= maxX || y < 0 || y >= maxY { return @@ -642,7 +728,7 @@ func (v *View) setCharacter(x, y int, ch string, fgColor, bgColor Attribute) { fgColor += 8 } fgColor = fgColor | AttrBold - if v.HighlightInactive { + if v.HighlightInactive || !isWindowFocused { bgColor = (bgColor & AttrStyleBits) | v.InactiveViewSelBgColor } else { bgColor = (bgColor & AttrStyleBits) | v.SelBgColor @@ -707,15 +793,8 @@ func (v *View) CursorY() int { // implement Horizontal and Vertical scrolling with just incrementing // or decrementing ox and oy. func (v *View) SetOrigin(x, y int) { - if x < 0 { - x = 0 - } - if y < 0 { - y = 0 - } - - v.ox = x - v.oy = y + v.SetOriginX(x) + v.SetOriginY(y) } func (v *View) SetOriginX(x int) { @@ -755,16 +834,16 @@ func (v *View) SetWritePos(x, y int) { y = 0 } - v.wx = x - v.wy = y + v.buf.wx = x + v.buf.wy = y // Changing the write position makes a pending newline obsolete - v.pendingNewline = false + v.buf.pendingNewline = false } // WritePos returns the current write position of the view's internal buffer. func (v *View) WritePos() (x, y int) { - return v.wx, v.wy + return v.buf.wx, v.buf.wy } // SetReadPos sets the read position of the view's internal buffer. @@ -788,56 +867,56 @@ func (v *View) ReadPos() (x, y int) { } // makeWriteable creates empty cells if required to make position (x, y) writeable. -func (v *View) makeWriteable(x, y int) { +func (b *viewBuffer) makeWriteable(x, y int) { // TODO: make this more efficient // line `y` must be index-able (that's why `<=`) - for len(v.lines) <= y { - if cap(v.lines) > len(v.lines) { - newLen := cap(v.lines) + for len(b.lines) <= y { + if cap(b.lines) > len(b.lines) { + newLen := cap(b.lines) if newLen > y { newLen = y + 1 } - v.lines = v.lines[:newLen] + b.lines = b.lines[:newLen] } else { - v.lines = append(v.lines, lineType{}) + b.lines = append(b.lines, lineType{}) } } // cell `x` need not be index-able (that's why `<`) // append should be used by `lines[y]` user if he wants to write beyond `x` - for len(v.lines[y].cells) < x { - if cap(v.lines[y].cells) > len(v.lines[y].cells) { - newLen := cap(v.lines[y].cells) + for len(b.lines[y].cells) < x { + if cap(b.lines[y].cells) > len(b.lines[y].cells) { + newLen := cap(b.lines[y].cells) if newLen > x { newLen = x } - v.lines[y].cells = v.lines[y].cells[:newLen] + b.lines[y].cells = b.lines[y].cells[:newLen] } else { - v.lines[y].cells = append(v.lines[y].cells, cell{}) + b.lines[y].cells = append(b.lines[y].cells, cell{}) } } } -// writeCells copies []cell to (v.wx, v.wy), and advances v.wx accordingly. +// writeCells copies []cell to (b.wx, b.wy), and advances b.wx accordingly. // !!! caller MUST ensure that specified location (x, y) is writeable by calling makeWriteable -func (v *View) writeCells(cells []cell) { +func (b *viewBuffer) writeCells(cells []cell) { var newLen int // use maximum len available - line := v.lines[v.wy].cells[:cap(v.lines[v.wy].cells)] - maxCopy := len(line) - v.wx + line := b.lines[b.wy].cells[:cap(b.lines[b.wy].cells)] + maxCopy := len(line) - b.wx if maxCopy < len(cells) { - copy(line[v.wx:], cells[:maxCopy]) + copy(line[b.wx:], cells[:maxCopy]) line = append(line, cells[maxCopy:]...) newLen = len(line) } else { // maxCopy >= len(cells) - copy(line[v.wx:], cells) - newLen = v.wx + len(cells) - if newLen < len(v.lines[v.wy].cells) { - newLen = len(v.lines[v.wy].cells) + copy(line[b.wx:], cells) + newLen = b.wx + len(cells) + if newLen < len(b.lines[b.wy].cells) { + newLen = len(b.lines[b.wy].cells) } } - v.lines[v.wy].cells = line[:newLen] - v.wx += len(cells) + b.lines[b.wy].cells = line[:newLen] + b.wx += len(cells) } // Write appends a byte slice into the view's internal buffer. Because @@ -854,36 +933,54 @@ func (v *View) Write(p []byte) (n int, err error) { } func (v *View) write(p []byte) { + // An async re-render builds into the off-screen buffer (see View.offscreen) + // until it swaps in; until then the displayed buffer, and so everything + // readers see, is left untouched. + if v.offscreen != nil { + v.offscreen.write(v, p) + return + } + v.tainted = true - // write only ever touches lines from v.wy onwards, so any cached wrapping + // write only ever touches lines from v.buf.wy onwards, so any cached wrapping // below that stays valid. - v.firstDirtyLine = min(v.firstDirtyLine, v.wy) + v.firstDirtyLine = min(v.firstDirtyLine, v.buf.wy) v.clearHover() + v.buf.write(v, p) + + v.searcher.positionsStale = true +} + +// write parses p into cells and appends them to the buffer at its write cursor. +// It only touches the buffer; the View wrapper above handles display-side +// effects (tainting, hover, search). v supplies render config (Editable, colors, +// width, tab width, hyperlink auto-rendering). +func (b *viewBuffer) write(v *View, p []byte) { // Fill with empty cells, if writing outside current view buffer - v.makeWriteable(v.wx, v.wy) + b.makeWriteable(b.wx, b.wy) finishLine := func() { - v.autoRenderHyperlinksInCurrentLine() + b.autoRenderHyperlinksInCurrentLine(v) } advanceToNextLine := func() { - v.wx = 0 - v.wy++ - if v.wy >= len(v.lines) { - v.lines = append(v.lines, lineType{}) + b.wx = 0 + b.wy++ + if b.wy >= len(b.lines) { + b.lines = append(b.lines, lineType{}) } } - if v.pendingNewline { + if b.pendingNewline { advanceToNextLine() - v.ei.notifyRowAdvance() - v.pendingNewline = false + b.ei.notifyRowAdvance() + b.pendingNewline = false } until := len(p) if !v.Editable && until > 0 && p[until-1] == '\n' { - v.pendingNewline = true + b.pendingNewline = true until-- } @@ -899,26 +996,26 @@ func (v *View) write(p []byte) { case characterEquals(chr, '\n') || isCRLF(chr): finishLine() advanceToNextLine() - v.ei.notifyRowAdvance() + b.ei.notifyRowAdvance() case characterEquals(chr, '\r'): finishLine() - v.wx = 0 - v.ei.notifyColumnReset() + b.wx = 0 + b.ei.notifyColumnReset() default: - truncateLine, cells := v.parseInput(chr, width, v.wx, v.wy) - if cd, ok := v.ei.instruction.(cursorDown); ok { - v.ei.instructionRead() + truncateLine, cells := b.parseInput(v, chr, width, b.wx, b.wy) + if cd, ok := b.ei.instruction.(cursorDown); ok { + b.ei.instructionRead() for range cd.n { - v.autoRenderHyperlinksInCurrentLine() + b.autoRenderHyperlinksInCurrentLine(v) advanceToNextLine() } } if cells == nil { continue } - v.writeCells(cells) + b.writeCells(cells) if truncateLine { - v.lines[v.wy].cells = v.lines[v.wy].cells[:v.wx] + b.lines[b.wy].cells = b.lines[b.wy].cells[:b.wx] } // Soft-wrap tracking. truncateLine is true exactly when the // cells are from \x1b[K filling to end of line — ConPTY @@ -929,18 +1026,16 @@ func (v *View) write(p []byte) { for _, c := range cells { totalWidth += c.width } - v.ei.notifyCellsWritten(totalWidth) + b.ei.notifyCellsWritten(totalWidth) } } } - if v.pendingNewline { + if b.pendingNewline { finishLine() } else { - v.autoRenderHyperlinksInCurrentLine() + b.autoRenderHyperlinksInCurrentLine(v) } - - v.updateSearchPositions() } // exported functions use the mutex. Non-exported functions are for internal use @@ -983,12 +1078,12 @@ var lineEndCharacters = map[string]bool{ ")": true, } -func (v *View) autoRenderHyperlinksInCurrentLine() { +func (b *viewBuffer) autoRenderHyperlinksInCurrentLine(v *View) { if !v.AutoRenderHyperLinks { return } - line := v.lines[v.wy].cells + line := b.lines[b.wy].cells start := 0 for { linkStart := findLinkStart(line[start:]) @@ -1005,7 +1100,7 @@ func (v *View) autoRenderHyperlinksInCurrentLine() { link.WriteString(line[linkEnd].chr) } for i := linkStart; i < linkEnd; i++ { - v.lines[v.wy].cells[i].hyperlink = link.String() + b.lines[b.wy].cells[i].hyperlink = link.String() } start = linkEnd } @@ -1014,13 +1109,13 @@ func (v *View) autoRenderHyperlinksInCurrentLine() { // parseInput parses char by char the input written to the View. It returns nil // while processing ESC sequences. Otherwise, it returns a cell slice that // contains the processed data. -func (v *View) parseInput(ch []byte, width int, x int, _ int) (bool, []cell) { +func (b *viewBuffer) parseInput(v *View, ch []byte, width int, x int, _ int) (bool, []cell) { cells := []cell{} truncateLine := false - isEscape, err := v.ei.parseOne(ch) + isEscape, err := b.ei.parseOne(ch) if err != nil { - for _, chr := range v.ei.characters() { + for _, chr := range b.ei.characters() { c := cell{ fgColor: v.FgColor, bgColor: v.BgColor, @@ -1029,28 +1124,28 @@ func (v *View) parseInput(ch []byte, width int, x int, _ int) (bool, []cell) { } cells = append(cells, c) } - v.ei.reset() + b.ei.reset() } else { repeatCount := 1 - if _, ok := v.ei.instruction.(eraseInLineFromCursor); ok { + if _, ok := b.ei.instruction.(eraseInLineFromCursor); ok { // Discard any old content past the cursor and record the // fill colors so draw() paints the trailing area with them. // This extends the bg to the right edge in both the // content-fits and content-wraps cases — for the latter, // the metadata is what reaches every wrapped segment past // the last word. - v.ei.instructionRead() + b.ei.instructionRead() truncateLine = true - v.lines[v.wy].trailingFillAttributes = &trailingFillAttributes{ - fg: v.ei.curFgColor, - bg: v.ei.curBgColor, + b.lines[b.wy].trailingFillAttributes = &trailingFillAttributes{ + fg: b.ei.curFgColor, + bg: b.ei.curBgColor, } return truncateLine, []cell{} - } else if cf, ok := v.ei.instruction.(cursorForward); ok { + } else if cf, ok := b.ei.instruction.(cursorForward); ok { // emit `n` space cells under the parser-tracked SGR — used // to materialize ConPTY's compressed runs of spaces (which // it emits as ECH+CUF instead of literal whitespace). - v.ei.instructionRead() + b.ei.instructionRead() repeatCount = cf.n ch = []byte{' '} width = 1 @@ -1068,9 +1163,9 @@ func (v *View) parseInput(ch []byte, width int, x int, _ int) (bool, []cell) { repeatCount = tabWidth - (x % tabWidth) } c := cell{ - fgColor: v.ei.curFgColor, - bgColor: v.ei.curBgColor, - hyperlink: v.ei.hyperlink.String(), + fgColor: b.ei.curFgColor, + bgColor: b.ei.curBgColor, + hyperlink: b.ei.hyperlink.String(), chr: string(ch), width: width, } @@ -1098,9 +1193,9 @@ func (v *View) Read(p []byte) (n int, err error) { } v.readBuffer = nil } - for v.ry < len(v.lines) { - for v.rx < len(v.lines[v.ry].cells) { - s := v.lines[v.ry].cells[v.rx].chr + for v.ry < len(v.buf.lines) { + for v.rx < len(v.buf.lines[v.ry].cells) { + s := v.buf.lines[v.ry].cells[v.rx].chr count := len(s) copy(p[offset:], s) v.rx++ @@ -1122,8 +1217,17 @@ func (v *View) Read(p []byte) (n int, err error) { // only use this if the calling function has a lock on writeMutex func (v *View) clear() { v.rewind() - v.lines = nil + v.buf.lines = nil v.clearViewLines() + // Abandon any in-progress off-screen render: a synchronous SetContent/Clear + // is taking over the displayed buffer, so writes must go there, not into a + // stale off-screen buffer left by a stopped task. + v.offscreen = nil + // Likewise release any held scrollbar height: the new content is defined + // synchronously (e.g. a string render superseding a still-loading diff), so + // there's no async growth left to smooth over and the scrollbar should track + // the new content directly. + v.scrollbarHeightFloor = 0 } // Clear empties the view's internal buffer. @@ -1164,10 +1268,10 @@ func (v *View) CopyContent(from *View) { // This is a shallow clone -- the per-row cell data is immutable once written // and stays shared, so the cost is proportional to the number of rows, not // their contents. - v.lines = slices.Clone(from.lines) + v.buf.lines = slices.Clone(from.buf.lines) v.viewLines = slices.Clone(from.viewLines) - v.ox = from.ox - v.oy = from.oy + v.SetOriginX(from.ox) + v.SetOriginY(from.oy) v.cx = from.cx v.cy = from.cy } @@ -1187,23 +1291,88 @@ func (v *View) Reset() { defer v.writeMutex.Unlock() v.rewind() - v.lines = nil + v.buf.lines = nil + // As in clear(): abandon any in-progress off-screen render so writes after a + // reset go to the displayed buffer. + v.offscreen = nil } -// This is for when we've done a restart for the sake of avoiding a flicker and -// we've reached the end of the new content to display: we need to clear the remaining -// content from the previous round. We do this by setting v.viewLines to nil so that -// we just render the new content from v.lines directly -func (v *View) FlushStaleCells() { +// BeginOffscreenRender starts building a re-render into an off-screen buffer. +// Until SwapInOffscreenRender promotes it, writes go to that buffer and the +// displayed buffer — what every reader sees — is left as it was. This is how an +// async re-render avoids exposing a half-written buffer: it accumulates +// off-screen and swaps in once it has read enough to paint. +func (v *View) BeginOffscreenRender() { v.writeMutex.Lock() defer v.writeMutex.Unlock() - v.clearViewLines() + ei := newEscapeInterpreter(v.outMode) + // The screen width content is wrapped at is render configuration set by + // SetContentWidth, not per-buffer state, so the off-screen buffer's parser + // needs it too — otherwise it counts no soft wraps and cursor-positioning + // escapes land on the wrong rows. + ei.screenColMax = v.buf.ei.screenColMax + v.offscreen = &viewBuffer{ei: ei} +} + +// SwapInOffscreenRender promotes the off-screen buffer (see BeginOffscreenRender) +// to the displayed buffer in one step, so the view jumps straight from the +// previous render to the new one with no half-written frame. Writes after this +// append to the now-displayed buffer directly. It is a no-op if no off-screen +// render is in progress, so it is safe to call more than once (e.g. again at EOF +// after an earlier paint already swapped). +func (v *View) SwapInOffscreenRender() { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + if v.offscreen == nil { + return + } + v.buf = v.offscreen + v.offscreen = nil + v.tainted = true + v.clearHover() +} + +// FreezeScrollbarHeight records the view's current content height so the +// scrollbar keeps that size while a re-render loads, instead of shrinking and +// snapping back as the partially-loaded content streams in past the first paint +// (see scrollbarHeightFloor). Call it when a load begins, while the view still +// shows the previous render; UnfreezeScrollbarHeight clears it when the load +// ends. +func (v *View) FreezeScrollbarHeight() { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + v.refreshViewLinesIfNeeded() + v.scrollbarHeightFloor = len(v.viewLines) +} + +// UnfreezeScrollbarHeight clears the height held by FreezeScrollbarHeight, so +// the scrollbar tracks the view's content directly again. Call it when a load +// ends. +func (v *View) UnfreezeScrollbarHeight() { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + v.scrollbarHeightFloor = 0 +} + +// scrollbarContentHeight is the view-line height the scrollbar is sized from. +// While a re-render is loading it is held at the height the view had when the +// load began (see FreezeScrollbarHeight), so the thumb doesn't shrink and jump +// as partially-loaded content streams in. +func (v *View) scrollbarContentHeight() int { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + v.refreshViewLinesIfNeeded() + return max(len(v.viewLines), v.scrollbarHeightFloor) } func (v *View) rewind() { - v.ei.reset() - v.ei.resetScreenCursor() + v.buf.ei.reset() + v.buf.ei.resetScreenCursor() v.SetReadPos(0, 0) v.SetWritePos(0, 0) @@ -1231,6 +1400,8 @@ func stringToGraphemes(s string) []string { } func (v *View) updateSearchPositions() { + v.searcher.positionsStale = false + if v.searcher.searchString != "" { var normalizeRune func(s string) string var normalizedSearchStr string @@ -1275,14 +1446,14 @@ func (v *View) updateSearchPositions() { for _, result := range v.searcher.modelSearchResults { // This code only works when v.Wrap is false. - if result.Y >= len(v.lines) { + if result.Y >= len(v.buf.lines) { break } // If a view line exists for this line index: - if v.lines[result.Y].cells != nil { + if v.buf.lines[result.Y].cells != nil { // search this view line for the search string - positions := searchPositionsForLine(v.lines[result.Y].cells, result.Y) + positions := searchPositionsForLine(v.buf.lines[result.Y].cells, result.Y) if len(positions) > 0 { // If we found any occurrences, add them v.searcher.searchPositions = append(v.searcher.searchPositions, positions...) @@ -1309,6 +1480,11 @@ func (v *View) updateSearchPositions() { } } } + + // The content may hold fewer matches than it did, so the current one is brought + // back into range: readers index the positions by it. + v.searcher.currentSearchIndex = min(v.searcher.currentSearchIndex, + max(0, len(v.searcher.searchPositions)-1)) } // IsTainted tells us if the view is tainted @@ -1319,7 +1495,7 @@ func (v *View) IsTainted() bool { } // draw re-draws the view's contents. -func (v *View) draw() { +func (v *View) draw(isWindowFocused bool) { v.writeMutex.Lock() defer v.writeMutex.Unlock() @@ -1335,14 +1511,15 @@ func (v *View) draw() { if maxX == 0 { return } - v.ox = 0 + v.SetOriginX(0) } v.refreshViewLinesIfNeeded() + v.refreshSearchPositionsIfNeeded() visibleViewLinesHeight := v.viewLineLengthIgnoringTrailingBlankLines() if v.Autoscroll && visibleViewLinesHeight > maxY { - v.oy = visibleViewLinesHeight - maxY + v.SetOriginY(visibleViewLinesHeight - maxY) } if len(v.viewLines) == 0 { @@ -1409,7 +1586,7 @@ func (v *View) draw() { fgColor |= AttrUnderline } - v.setCharacter(x, y, c.chr, fgColor, bgColor) + v.setCharacter(x, y, c.chr, fgColor, bgColor, isWindowFocused) x += c.width cellIdx++ @@ -1429,7 +1606,7 @@ func (v *View) refreshViewLinesIfNeeded() { } lineIdx := 0 - lines := v.lines + lines := v.buf.lines for i := range lines { line := &lines[i] @@ -1475,6 +1652,13 @@ func (v *View) refreshViewLinesIfNeeded() { } v.firstDirtyLine = len(lines) + // Truncate any entries left over from a previous, longer render. An async + // re-render builds its content off-screen and swaps it in whole (see + // View.offscreen), so the buffer this rebuilds from is always a complete + // render — there is no half-loaded shorter buffer whose tail we'd need to + // keep showing to avoid a flicker, and a leftover tail would just be stale + // lines mapping to the wrong buffer rows. + v.viewLines = v.viewLines[:lineIdx] v.tainted = false } @@ -1553,8 +1737,8 @@ func (v *View) BufferLines() []string { v.writeMutex.Lock() defer v.writeMutex.Unlock() - lines := make([]string, len(v.lines)) - for i, l := range v.lines { + lines := make([]string, len(v.buf.lines)) + for i, l := range v.buf.lines { lines[i] = l.cells.String() } return lines @@ -1566,7 +1750,7 @@ func (v *View) Buffer() string { v.writeMutex.Lock() defer v.writeMutex.Unlock() - return linesToString(v.lines) + return linesToString(v.buf.lines) } // ViewBufferLines returns the lines in the view's internal @@ -1586,7 +1770,7 @@ func (v *View) ViewBufferLines() []string { // LinesHeight is the count of view lines (i.e. lines excluding wrapping) func (v *View) LinesHeight() int { - return len(v.lines) + return len(v.buf.lines) } // ViewLinesHeight is the count of view lines (i.e. lines including wrapping) @@ -1617,11 +1801,11 @@ func (v *View) Line(y int) (string, bool) { return "", false } - if y < 0 || y >= len(v.lines) { + if y < 0 || y >= len(v.buf.lines) { return "", false } - return v.lines[y].cells.String(), true + return v.buf.lines[y].cells.String(), true } // Word returns a string with the word of the view's internal buffer @@ -1632,11 +1816,11 @@ func (v *View) Word(x, y int) (string, bool) { return "", false } - if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y].cells) { + if x < 0 || y < 0 || y >= len(v.buf.lines) || x >= len(v.buf.lines[y].cells) { return "", false } - str := v.lines[y].cells.String() + str := v.buf.lines[y].cells.String() nl := strings.LastIndexFunc(str[:x], indexFunc) if nl == -1 { @@ -1662,12 +1846,12 @@ func indexFunc(r rune) bool { // SetHighlight toggles highlighting of separate lines, for custom lists // or multiple selection in views. func (v *View) SetHighlight(y int, on bool) { - if y < 0 || y >= len(v.lines) { + if y < 0 || y >= len(v.buf.lines) { return } - cells := make([]cell, 0, len(v.lines[y].cells)) - for _, c := range v.lines[y].cells { + cells := make([]cell, 0, len(v.buf.lines[y].cells)) + for _, c := range v.buf.lines[y].cells { if on { c.bgColor = v.SelBgColor c.fgColor = v.SelFgColor @@ -1679,7 +1863,7 @@ func (v *View) SetHighlight(y int, on bool) { } v.tainted = true v.firstDirtyLine = min(v.firstDirtyLine, y) - v.lines[y].cells = cells + v.buf.lines[y].cells = cells v.clearHover() } @@ -1791,7 +1975,7 @@ func (v *View) SelectedLine() string { v.writeMutex.Lock() defer v.writeMutex.Unlock() - if len(v.lines) == 0 { + if len(v.buf.lines) == 0 { return "" } @@ -1803,7 +1987,7 @@ func (v *View) SelectedLines() []string { v.writeMutex.Lock() defer v.writeMutex.Unlock() - if len(v.lines) == 0 { + if len(v.buf.lines) == 0 { return nil } @@ -1818,7 +2002,7 @@ func (v *View) SelectedLines() []string { } func (v *View) lineContentAtIdx(idx int) string { - return v.lines[idx].cells.String() + return v.buf.lines[idx].cells.String() } func (v *View) SelectedPoint() (int, int) { @@ -1891,8 +2075,8 @@ func (v *View) ClearTextArea() { func (v *View) overwriteLines(y int, content string) { // break by newline, then for each line, write it, then add that erase command - v.wx = 0 - v.wy = y + v.buf.wx = 0 + v.buf.wy = y v.clearViewLines() lines := strings.ReplaceAll(content, "\n", "\x1b[K\n") @@ -1904,7 +2088,7 @@ func (v *View) overwriteLines(y int, content string) { v.writeString(lines) } -// only call this function if you don't care where v.wx and v.wy end up +// only call this function if you don't care where v.buf.wx and v.buf.wy end up func (v *View) OverwriteLines(y int, content string) { v.writeMutex.Lock() defer v.writeMutex.Unlock() @@ -1912,7 +2096,7 @@ func (v *View) OverwriteLines(y int, content string) { v.overwriteLines(y, content) } -// only call this function if you don't care where v.wx and v.wy end up +// only call this function if you don't care where v.buf.wx and v.buf.wy end up func (v *View) OverwriteLinesAndClearEverythingElse(lineCount int, y int, content string) { v.writeMutex.Lock() defer v.writeMutex.Unlock() @@ -1922,25 +2106,27 @@ func (v *View) OverwriteLinesAndClearEverythingElse(lineCount int, y int, conten v.overwriteLines(y, content) for i := range y { - v.lines[i] = lineType{} + v.buf.lines[i] = lineType{} } - for i := v.wy + 1; i < len(v.lines); i += 1 { - v.lines[i] = lineType{} + for i := v.buf.wy + 1; i < len(v.buf.lines); i += 1 { + v.buf.lines[i] = lineType{} } } func (v *View) setContentLineCount(lineCount int) { if lineCount > 0 { - v.makeWriteable(0, lineCount-1) + v.buf.makeWriteable(0, lineCount-1) } - v.lines = v.lines[:lineCount] + v.buf.lines = v.buf.lines[:lineCount] } // If the current search result is no longer visible after a scroll up, select the last search // result that is visible in the view, if any, or the first one that is below the view if none is // visible. func (v *View) selectVisibleSearchResultAfterScrollUp() { + v.refreshSearchPositions() + if !v.Highlight && len(v.searcher.searchPositions) != 0 { windowBottom := v.oy + v.InnerHeight() if v.searcher.searchPositions[v.searcher.currentSearchIndex].Y >= windowBottom { @@ -1964,6 +2150,8 @@ func (v *View) selectVisibleSearchResultAfterScrollUp() { // result that is visible in the view, if any, or the last one that is above the view if none is // visible. func (v *View) selectVisibleSearchResultAfterScrollDown() { + v.refreshSearchPositions() + if !v.Highlight && len(v.searcher.searchPositions) != 0 { if v.searcher.searchPositions[v.searcher.currentSearchIndex].Y < v.oy { newSearchIndex := v.searcher.currentSearchIndex @@ -1989,7 +2177,7 @@ func (v *View) ScrollUp(amount int) { } if amount != 0 { - v.oy -= amount + v.SetOriginY(v.oy - amount) v.cy += amount v.clearHover() @@ -2001,7 +2189,7 @@ func (v *View) ScrollUp(amount int) { func (v *View) ScrollDown(amount int) { adjustedAmount := v.adjustDownwardScrollAmount(amount) if adjustedAmount > 0 { - v.oy += adjustedAmount + v.SetOriginY(v.oy + adjustedAmount) v.cy -= adjustedAmount v.clearHover() @@ -2015,7 +2203,7 @@ func (v *View) ScrollLeft(amount int) { newOx = 0 } if newOx != v.ox { - v.ox = newOx + v.SetOriginX(newOx) v.clearHover() } @@ -2023,7 +2211,7 @@ func (v *View) ScrollLeft(amount int) { // not applying any limits to this func (v *View) ScrollRight(amount int) { - v.ox += amount + v.SetOriginX(v.ox + amount) v.clearHover() } @@ -2068,7 +2256,7 @@ func (v *View) scrollMargin() int { // Returns true if the view contains a line containing the given text with the given // foreground color func (v *View) ContainsColoredText(fgColor string, text string) bool { - for _, line := range v.lines { + for _, line := range v.buf.lines { if containsColoredTextInLine(fgColor, text, line.cells) { return true } @@ -2105,6 +2293,9 @@ func (v *View) onMouseMove(x int, y int) { return } + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + // newCx and newCy are relative to the view port, i.e. to the visible area of the view newCx := x - v.x0 - 1 newCy := y - v.y0 - 1 @@ -2123,6 +2314,19 @@ func (v *View) onMouseMove(x int, y int) { } } +// hyperlinkAt returns the hyperlink at the given position of the view's +// content, or an empty string if there is none. +func (v *View) hyperlinkAt(x, y int) string { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + if y < 0 || y >= len(v.viewLines) || x < 0 || x >= len(v.viewLines[y].line) { + return "" + } + + return v.viewLines[y].line[x].hyperlink +} + func (v *View) findHyperlinkAt(x, y int) *SearchPosition { linkStr := v.viewLines[y].line[x].hyperlink if linkStr == "" { diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index f7e229f1c..2ee5eb4b8 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -11,6 +11,7 @@ import ( "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" "github.com/rivo/uniseg" + "github.com/samber/lo" "github.com/stretchr/testify/assert" ) @@ -101,15 +102,13 @@ func TestWriteString(t *testing.T) { for _, test := range tests { v := NewView("name", 0, 0, 10, 10, OutputNormal) for _, l := range test.existingLines { - v.lines = append(v.lines, lineType{cells: stringToCells(l)}) + v.buf.lines = append(v.buf.lines, lineType{cells: stringToCells(l)}) } for _, s := range test.stringsToWrite { v.writeString(s) } - var resultingLines [][]string - for _, l := range v.lines { - resultingLines = append(resultingLines, cellsToStrings(l.cells)) - } + resultingLines := lo.Map(v.buf.lines, + func(l lineType, _ int) []string { return cellsToStrings(l.cells) }) assert.Equal(t, test.expectedLines, resultingLines) } } @@ -144,19 +143,115 @@ func TestAutoRenderingHyperlinks(t *testing.T) { v.writeString("htt") // No hyperlinks are generated for incomplete URLs - assert.Equal(t, "", v.lines[0].cells[0].hyperlink) + assert.Equal(t, "", v.buf.lines[0].cells[0].hyperlink) // Writing more characters to the same line makes the link complete (even // though we didn't see a newline yet) v.writeString("ps://example.com") - assert.Equal(t, "https://example.com", v.lines[0].cells[0].hyperlink) + assert.Equal(t, "https://example.com", v.buf.lines[0].cells[0].hyperlink) v.Clear() // Valid but incomplete URL v.writeString("https://exa") - assert.Equal(t, "https://exa", v.lines[0].cells[0].hyperlink) + assert.Equal(t, "https://exa", v.buf.lines[0].cells[0].hyperlink) // Writing more characters to the same fixes the link v.writeString("mple.com") - assert.Equal(t, "https://example.com", v.lines[0].cells[0].hyperlink) + assert.Equal(t, "https://example.com", v.buf.lines[0].cells[0].hyperlink) +} + +// An async re-render builds into an off-screen buffer and swaps it in once it +// has enough to paint, so readers keep seeing the previous render — coherent and +// consistent — until the new content appears in one step. See View.offscreen. +func TestOffscreenRender(t *testing.T) { + v := NewView("name", 0, 0, 80, 10, OutputNormal) + + v.writeString("a\nb\nc") + assert.Equal(t, []string{"a", "b", "c"}, v.ViewBufferLines()) + + // Render new, longer content off-screen. + v.BeginOffscreenRender() + v.writeString("w\nx\ny\nz") + + // The displayed buffer is untouched: readers still see the previous render. + assert.Equal(t, []string{"a", "b", "c"}, v.ViewBufferLines()) + + // Swapping in reveals the new content in one step. + v.SwapInOffscreenRender() + assert.Equal(t, []string{"w", "x", "y", "z"}, v.ViewBufferLines()) + + // A further write now appends to the displayed buffer directly. + v.writeString("\nmore") + assert.Equal(t, []string{"w", "x", "y", "z", "more"}, v.ViewBufferLines()) +} + +// When a render produces fewer view lines than the previous one, +// refreshViewLinesIfNeeded must truncate viewLines to the new content rather +// than leaving the previous render's entries in the tail: with the off-screen +// render there is no half-loaded buffer whose tail we'd want to keep showing, +// and a leftover tail is just stale lines describing content that is gone. +func TestViewLinesTruncatedByShorterRender(t *testing.T) { + v := NewView("name", 0, 0, 10, 10, OutputNormal) // InnerWidth is 9 + v.Wrap = true + + // Two lines of 27 characters each wrap into 3 view lines apiece. + v.writeString(strings.Repeat("a", 27) + "\n" + strings.Repeat("b", 27)) + assert.Equal(t, 6, v.ViewLinesHeight()) + + // Re-render with three short, unwrapped lines: only 3 view lines remain. + v.BeginOffscreenRender() + v.writeString("aaa\nbbb\nccc") + v.SwapInOffscreenRender() + assert.Equal(t, 3, v.ViewLinesHeight()) + assert.Equal(t, []string{"aaa", "bbb", "ccc"}, v.ViewBufferLines()) +} + +// While an async re-render loads, it swaps in only a partially-filled buffer at +// its first paint and keeps appending lines afterwards. The scrollbar must keep +// using the pre-load height until the load ends, so the thumb doesn't shrink and +// snap back as the rest streams in. See View.scrollbarHeightFloor. +func TestScrollbarHeightHeldWhileLoading(t *testing.T) { + v := NewView("name", 0, 0, 80, 12, OutputNormal) + + // Initial render: 100 lines, scrolled well down. + v.writeString(strings.Repeat("x\n", 100)) + v.SetOrigin(0, 80) + assert.Equal(t, 100, v.scrollbarContentHeight()) + + // A re-render begins while the previous render is still shown: hold the + // scrollbar height at the current value. + v.FreezeScrollbarHeight() + + // The off-screen render swaps in only a screenful at its first paint. + v.BeginOffscreenRender() + v.writeString(strings.Repeat("y\n", 30)) + v.SwapInOffscreenRender() + + // The displayed buffer is now short, but the scrollbar height stays held, so + // the thumb keeps its position instead of jumping. + assert.Equal(t, 30, v.ViewLinesHeight()) + assert.Equal(t, 100, v.scrollbarContentHeight()) + + // The rest of the content streams in. + v.writeString(strings.Repeat("y\n", 70)) + assert.Equal(t, 100, v.scrollbarContentHeight()) + + // Once the load ends, the scrollbar tracks the real content directly again. + v.UnfreezeScrollbarHeight() + assert.Equal(t, 100, v.scrollbarContentHeight()) +} + +// If a synchronous render (e.g. a string render) supersedes a still-loading diff +// before it reaches its end, the held scrollbar height must be released, so the +// scrollbar reflects the new content rather than the abandoned load's height. +func TestScrollbarHeightReleasedWhenContentReplaced(t *testing.T) { + v := NewView("name", 0, 0, 80, 12, OutputNormal) + + v.writeString(strings.Repeat("x\n", 100)) + v.FreezeScrollbarHeight() + assert.Equal(t, 100, v.scrollbarContentHeight()) + + // A synchronous render replaces the content before the (notional) load ends. + v.SetContent("just a few\nshort lines\nhere") + assert.Equal(t, 3, v.scrollbarContentHeight()) } func TestContainsColoredText(t *testing.T) { @@ -233,7 +328,7 @@ func TestContainsColoredText(t *testing.T) { for j, cells := range test.lines { lines[j] = lineType{cells: cells} } - v := &View{lines: lines} + v := &View{buf: &viewBuffer{lines: lines}} assert.Equal(t, test.expected, v.ContainsColoredText(test.fgColorStr, test.text), "Test %d failed", i) } } @@ -248,8 +343,8 @@ func TestWriteCursorPositionEscape(t *testing.T) { // "a", then "skip to row 3" (i.e. one blank row), then "b". v.writeString("a\r\n\x1b[3;1Hb\r\n") - got := make([][]string, 0, len(v.lines)) - for _, l := range v.lines { + got := make([][]string, 0, len(v.buf.lines)) + for _, l := range v.buf.lines { got = append(got, cellsToStrings(l.cells)) } @@ -269,8 +364,8 @@ func TestWriteCursorPositionEscapeAcrossWrites(t *testing.T) { // ConPTY is on row 3 here; CUP to row 5 should skip exactly one row. v.writeString("c\x1b[5;1Hd\n") - got := make([][]string, 0, len(v.lines)) - for _, l := range v.lines { + got := make([][]string, 0, len(v.buf.lines)) + for _, l := range v.buf.lines { got = append(got, cellsToStrings(l.cells)) } assert.Equal(t, [][]string{ @@ -282,6 +377,31 @@ func TestWriteCursorPositionEscapeAcrossWrites(t *testing.T) { }, got) } +func TestWriteCursorPositionEscapeInOffscreenRender(t *testing.T) { + // Soft-wrap counting has to work in an off-screen render too: the content + // width the parser counts wraps against is set by SetContentWidth before the + // render starts, so the off-screen buffer's parser has to pick it up. If it + // doesn't, no wraps are counted and the CUP below is evaluated against a + // stale row, overshooting into an extra blank line. + v := NewView("name", 0, 0, 30, 30, OutputNormal) + v.SetContentWidth(5) + + v.BeginOffscreenRender() + // Seven characters soft-wrap once on a 5-column screen, putting ConPTY on + // row 2; CUP to row 3 should then skip no rows at all. + v.writeString("aaaaaaa\x1b[3;1Hb\n") + v.SwapInOffscreenRender() + + got := make([][]string, 0, len(v.buf.lines)) + for _, l := range v.buf.lines { + got = append(got, cellsToStrings(l.cells)) + } + assert.Equal(t, [][]string{ + {"a", "a", "a", "a", "a", "a", "a"}, + {"b"}, + }, got) +} + func TestWriteCursorForwardEscape(t *testing.T) { // ConPTY compresses runs of default-colored spaces into ECH (\x1b[NX, // "clear N cells, cursor stationary") + CUF (\x1b[NC, "cursor forward @@ -292,8 +412,8 @@ func TestWriteCursorForwardEscape(t *testing.T) { // "a" + ECH 5 + CUF 5 + "b" — visually "a b". v.writeString("a\x1b[5X\x1b[5Cb\n") - got := make([][]string, 0, len(v.lines)) - for _, l := range v.lines { + got := make([][]string, 0, len(v.buf.lines)) + for _, l := range v.buf.lines { got = append(got, cellsToStrings(l.cells)) } @@ -312,8 +432,8 @@ func TestWriteCursorPositionEscapeWithSoftWraps(t *testing.T) { v.writeString("abcdefghij\n") v.writeString("\x1b[4;1Hxyz\n") - got := make([][]string, 0, len(v.lines)) - for _, l := range v.lines { + got := make([][]string, 0, len(v.buf.lines)) + for _, l := range v.buf.lines { got = append(got, cellsToStrings(l.cells)) } assert.Equal(t, [][]string{ @@ -344,11 +464,7 @@ func cellsToString(cells []cell) string { } func cellsToStrings(cells []cell) []string { - s := []string{} - for _, c := range cells { - s = append(s, c.chr) - } - return s + return lo.Map(cells, func(c cell, _ int) string { return c.chr }) } func TestLineWrap(t *testing.T) { @@ -534,7 +650,7 @@ func TestNewlineTerminatedLineClearsTrailingBg(t *testing.T) { // renders with bg=red. The trailing area past "foo" must NOT extend // the red bg because '\n' marks the line as cleanly terminated. v.writeString("\x1b[7m\x1b[31mfoo\x1b[0m\n") - v.draw() + v.draw(true) // First row: cells 1..3 are "foo" (render with red bg via reverse), // cells 4..10 are trailing and should be plain default. @@ -560,7 +676,7 @@ func TestUnterminatedReverseLineDoesNotExtend(t *testing.T) { // Reverse + red fg, "foo", no termination. The trailing cells past // "foo" should be plain default, NOT a continuation of the red bg. v.writeString("\x1b[7m\x1b[31mfoo") - v.draw() + v.draw(true) // Cells 4..10 are trailing and should be default with no reverse. for x := 4; x <= 10; x++ { @@ -583,7 +699,7 @@ func TestShortFilledLineExtendsBgWithoutWrap(t *testing.T) { // \x1b[41m sets bg=red. "hi" fits within InnerWidth=10; \x1b[K should // fill the remaining 8 cells with red. v.writeString("\x1b[41mhi\x1b[K\x1b[0m\n") - v.draw() + v.draw(true) // All ten cells at (1..10, 1) should have red bg. for x := 1; x <= 10; x++ { @@ -611,7 +727,7 @@ func TestWrappedFilledLineExtendsBgToEdge(t *testing.T) { // segments — "aaa bbb" / "ccc ddd" / "eee". Each row's trailing area // must pick up the red fill from \x1b[K. v.writeString("\x1b[41m" + "aaa bbb ccc ddd eee" + "\x1b[0m\x1b[41m\x1b[K\x1b[0m\n") - v.draw() + v.draw(true) // All three wrapped rows should have the red fill background across // the full InnerWidth, including the trailing cells past each row's @@ -645,7 +761,7 @@ func TestMulticolorWrappedFillUsesLastCellOfEachSegment(t *testing.T) { // last cell red) and segment 2 is "ccc" (green, last cell green). // \x1b[K records the green bg on the source line. v.writeString("\x1b[41maaa bbb\x1b[42m ccc\x1b[K\x1b[0m\n") - v.draw() + v.draw(true) // Row 1's content ends with a red cell at x=7, so trailing columns // 8..10 should pick up red rather than the \x1b[K's green. diff --git a/pkg/gui/background.go b/pkg/gui/background.go index 180b2d445..6a4c529a1 100644 --- a/pkg/gui/background.go +++ b/pkg/gui/background.go @@ -119,13 +119,12 @@ func (self *BackgroundRoutineMgr) startBackgroundFetch() { var appStatusHelper *helpers.AppStatusHelper var branchesHelper *helpers.BranchesHelper var fetchGeneration int - if err := self.gui.g.OnUIThreadAndWaitBackground(func() error { + if err := self.gui.g.OnUIThreadAndWaitBackground(func() { git = self.gui.git appStatusHelper = self.gui.helpers.AppStatus branchesHelper = self.gui.helpers.BranchesHelper fetchGeneration = self.gui.c.State().GetRepoGeneration() self.gui.State.LastBackgroundFetchTime = time.Now() - return nil }); err != nil { return err } @@ -184,10 +183,9 @@ func (self *BackgroundRoutineMgr) checkForExternalChanges() { // reading them from this background goroutine would race the reassignment. var git *commands.GitCommand var refreshHelper *helpers.RefreshHelper - if err := self.gui.g.OnUIThreadAndWaitBackground(func() error { + if err := self.gui.g.OnUIThreadAndWaitBackground(func() { git = self.gui.git refreshHelper = self.gui.helpers.Refresh - return nil }); err != nil { return } diff --git a/pkg/gui/context.go b/pkg/gui/context.go index 1adfec35c..d83b144f2 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -3,6 +3,7 @@ package gui import ( "sync" + "github.com/jesseduffield/generics/set" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" @@ -179,11 +180,8 @@ func (self *ContextMgr) Activate(c types.Context, opts types.OnFocusOpts) { self.gui.helpers.Window.SetWindowContext(c) self.gui.helpers.Window.MoveToTopOfWindow(c) - oldView := self.gui.c.GocuiGui().CurrentView() - if oldView != nil && oldView.Name() != viewName { - oldView.HighlightInactive = true - } - if _, err := self.gui.c.GocuiGui().SetCurrentView(viewName); err != nil { + inputViewName := c.GetInputViewName() + if _, err := self.gui.c.GocuiGui().SetCurrentView(inputViewName); err != nil { panic(err) } @@ -198,9 +196,37 @@ func (self *ContextMgr) Activate(c types.Context, opts types.OnFocusOpts) { self.gui.c.GocuiGui().Cursor = v.Editable && v.Mask == "" + self.updateSelectionHighlights() + c.HandleFocus(opts) } +// updateSelectionHighlights re-derives which views draw a selection, and which of +// them draw theirs as the active one: a view shows a selection while its context is +// on the stack and has something to select, and the context the user is in shows the +// active selection while the ones behind it show inactive ones. +// +// Both of those can change, so this is called wherever they do: from Activate, which +// every change to the stack goes through; after a refresh, since that is when the +// contents of a list change; and from whoever tells a context that its content has +// gained or lost something to select. +func (self *ContextMgr) updateSelectionHighlights() { + self.RLock() + defer self.RUnlock() + + onStack := set.NewFromSlice(lo.Map(self.ContextStack, + func(c types.Context, _ int) types.ContextKey { return c.GetKey() })) + currentKey := self.currentContextWithoutLock().GetKey() + + for _, c := range self.allContexts.Flatten() { + // The global context has no view of its own. + if view := c.GetView(); view != nil { + view.Highlight = onStack.Includes(c.GetKey()) && c.HasSelectableContent() + view.HighlightInactive = c.GetKey() != currentKey + } + } +} + func (self *ContextMgr) Current() types.Context { self.RLock() defer self.RUnlock() diff --git a/pkg/gui/context/base_context.go b/pkg/gui/context/base_context.go index 7584b5a12..b5fbf76ce 100644 --- a/pkg/gui/context/base_context.go +++ b/pkg/gui/context/base_context.go @@ -28,7 +28,7 @@ type BaseContext struct { hasControlledBounds bool needsRerenderOnWidthChange types.NeedsRerenderOnWidthChangeLevel needsRerenderOnHeightChange bool - highlightOnFocus bool + hasSelectableContent bool *ParentContextMgr } @@ -49,7 +49,7 @@ type NewBaseContextOpts struct { Focusable bool Transient bool HasUncontrolledBounds bool // negating for the sake of making false the default - HighlightOnFocus bool + HasSelectableContent bool NeedsRerenderOnWidthChange types.NeedsRerenderOnWidthChangeLevel NeedsRerenderOnHeightChange bool @@ -70,7 +70,7 @@ func NewBaseContext(opts NewBaseContextOpts) *BaseContext { focusable: opts.Focusable, transient: opts.Transient, hasControlledBounds: hasControlledBounds, - highlightOnFocus: opts.HighlightOnFocus, + hasSelectableContent: opts.HasSelectableContent, needsRerenderOnWidthChange: opts.NeedsRerenderOnWidthChange, needsRerenderOnHeightChange: opts.NeedsRerenderOnHeightChange, ParentContextMgr: &ParentContextMgr{}, @@ -102,6 +102,10 @@ func (self *BaseContext) GetViewName() string { return self.view.Name() } +func (self *BaseContext) GetInputViewName() string { + return self.GetViewName() +} + func (self *BaseContext) GetView() *gocui.View { return self.view } @@ -114,12 +118,16 @@ func (self *BaseContext) GetKind() types.ContextKind { return self.kind } +func (self *BaseContext) HasSelectableContent() bool { + return self.hasSelectableContent +} + func (self *BaseContext) GetKey() types.ContextKey { return self.key } func (self *BaseContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { - bindings := []*types.Binding{} + bindings := make([]*types.Binding, 0, len(self.keybindingsFns)) for i := range self.keybindingsFns { // the first binding in the bindings array takes precedence but we want the // last keybindingsFn to take precedence to we add them in reverse @@ -216,7 +224,7 @@ func (self *BaseContext) AddOnQuitFn(fn func()) { } func (self *BaseContext) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding { - bindings := []*gocui.ViewMouseBinding{} + bindings := make([]*gocui.ViewMouseBinding, 0, len(self.mouseKeybindingsFns)) for i := range self.mouseKeybindingsFns { // the first binding in the bindings array takes precedence but we want the // last keybindingsFn to take precedence to we add them in reverse diff --git a/pkg/gui/context/context_test.go b/pkg/gui/context/context_test.go new file mode 100644 index 000000000..53f83c6ea --- /dev/null +++ b/pkg/gui/context/context_test.go @@ -0,0 +1,22 @@ +package context + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" +) + +// The config package validates a custom command's context against its own copy of +// these names, being unable to import this package. A name in one list but not the +// other would be either a context that validation rejects although you can bind to +// it, or one it accepts although binding to it exits lazygit. +func TestValidCustomCommandContextsMatchesAllContextKeys(t *testing.T) { + keys := lo.Map(AllContextKeys, func(key types.ContextKey, _ int) string { + return string(key) + }) + + assert.Equal(t, keys, config.ValidCustomCommandContexts) +} diff --git a/pkg/gui/context/filtered_list_view_model.go b/pkg/gui/context/filtered_list_view_model.go index ce2f8ac36..2c2841964 100644 --- a/pkg/gui/context/filtered_list_view_model.go +++ b/pkg/gui/context/filtered_list_view_model.go @@ -1,7 +1,5 @@ package context -import "github.com/jesseduffield/lazygit/pkg/i18n" - type FilteredListViewModel[T HasID] struct { *FilteredList[T] *ListViewModel[T] @@ -35,8 +33,3 @@ func (self *FilteredListViewModel[T]) ClearFilter() { self.SetSelection(unfilteredIndex) } - -// Default implementation of most filterable contexts. Can be overridden if needed. -func (self *FilteredListViewModel[T]) FilterPrefix(tr *i18n.TranslationSet) string { - return tr.FilterPrefix -} diff --git a/pkg/gui/context/list_context_trait.go b/pkg/gui/context/list_context_trait.go index 597fc99df..2e4ae7267 100644 --- a/pkg/gui/context/list_context_trait.go +++ b/pkg/gui/context/list_context_trait.go @@ -28,10 +28,19 @@ type ListContextTrait struct { // true if we're inside the OnSearchSelect call; in that case we don't want to update the search // result index. inOnSearchSelect bool + + // If set, this renders the "x of y" footer instead of the default, which puts + // it on the bottom border of the list's own view. A list that is part of a + // composite panel can use this to put it somewhere else; see MenuContext. + renderFooter func(footer string) } func (self *ListContextTrait) IsListContext() {} +func (self *ListContextTrait) HasSelectableContent() bool { + return self.list.Len() > 0 +} + func (self *ListContextTrait) FocusLine(scrollIntoView bool) { self.Context.FocusLine(scrollIntoView) @@ -81,7 +90,13 @@ func (self *ListContextTrait) refreshViewport() { } func (self *ListContextTrait) setFooter() { - self.GetViewTrait().SetFooter(formatListFooter(self.list.GetSelectedLineIdx(), self.list.Len())) + footer := formatListFooter(self.list.GetSelectedLineIdx(), self.list.Len()) + if self.renderFooter != nil { + self.renderFooter(footer) + return + } + + self.GetViewTrait().SetFooter(footer) } func formatListFooter(selectedLineIdx int, length int) string { @@ -89,9 +104,7 @@ func formatListFooter(selectedLineIdx int, length int) string { } func (self *ListContextTrait) HandleFocus(opts types.OnFocusOpts) { - self.FocusLine(opts.ScrollSelectionIntoView) - - self.GetViewTrait().SetHighlight(self.list.Len() > 0) + self.FocusLine(!opts.KeepScrollPosition) self.Context.HandleFocus(opts) } diff --git a/pkg/gui/context/main_context.go b/pkg/gui/context/main_context.go index c8b6edade..692c6dd5c 100644 --- a/pkg/gui/context/main_context.go +++ b/pkg/gui/context/main_context.go @@ -21,12 +21,12 @@ func NewMainContext( ctx := &MainContext{ SimpleContext: NewSimpleContext( NewBaseContext(NewBaseContextOpts{ - Kind: types.MAIN_CONTEXT, - View: view, - WindowName: windowName, - Key: key, - Focusable: true, - HighlightOnFocus: false, + Kind: types.MAIN_CONTEXT, + View: view, + WindowName: windowName, + Key: key, + Focusable: true, + HasSelectableContent: false, })), SearchTrait: NewSearchTrait(c), } diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 8129aa420..55a3e5bfa 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -8,7 +8,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -45,6 +44,13 @@ func NewMenuContext( getColumnAlignments: func() []utils.Alignment { return viewModel.columnAlignment }, getNonModelItems: viewModel.GetNonModelItems, }, + // While the filter row is showing, its top border covers the menu's bottom + // border, so the footer has to be rendered on the row instead. + renderFooter: func(footer string) { + onFilterRow := viewModel.FilterStarted() + c.Views().Menu.Footer = lo.Ternary(onFilterRow, "", footer) + c.Views().MenuFilterFrame.Footer = lo.Ternary(onFilterRow, footer, "") + }, c: c, }, } @@ -58,6 +64,8 @@ type MenuViewModel struct { columnAlignment []utils.Alignment allowFilteringKeybindings bool keybindingsTakePrecedence bool + filterAsYouType bool + filterStarted bool onCancel func() error *FilteredListViewModel[*types.MenuItem] } @@ -128,10 +136,37 @@ func (self *MenuViewModel) SetAllowFilteringKeybindings(allow bool) { self.allowFilteringKeybindings = allow } +func (self *MenuViewModel) AllowFilteringKeybindings() bool { + return self.allowFilteringKeybindings +} + func (self *MenuViewModel) SetKeybindingsTakePrecedence(value bool) { self.keybindingsTakePrecedence = value } +// Whether this menu has a filter row that filters the items as the user types, +// instead of being filtered through the search prompt. +func (self *MenuViewModel) SetFilterAsYouType(value bool) { + self.filterAsYouType = value + self.SetFilterStarted(false) +} + +func (self *MenuViewModel) FilterAsYouType() bool { + return self.filterAsYouType +} + +// Whether the user has started to filter, which is when the filter row appears. +func (self *MenuViewModel) SetFilterStarted(value bool) { + self.filterStarted = value + // As long as there is nothing to type into, printable keys keep driving the + // menu, so that the configured navigation keys work like in any other menu. + self.c.Views().MenuFilter.KeybindOnEdit = !value +} + +func (self *MenuViewModel) FilterStarted() bool { + return self.filterStarted +} + // TODO: move into presentation package func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string { menuItems := self.FilteredListViewModel.GetItems() @@ -209,6 +244,16 @@ func (self *MenuViewModel) GetNonModelItems() []*NonModelItem { func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { basicBindings := self.ListContextTrait.GetKeybindings(opts) + + if self.filterAsYouType { + // A menu item's keys are shown as a reminder of what they do outside the + // menu, but pressing one types it into the filter rather than executing the + // item, so we don't bind them at all. That leaves the bindings that drive + // the menu itself, and the printable ones among those give way to the filter + // as soon as there is something to type into (see View.KeybindOnEdit). + return basicBindings + } + menuItemsWithKeys := lo.Filter(self.menuItems, func(item *types.MenuItem, _ int) bool { return len(item.Keys) > 0 }) @@ -267,10 +312,13 @@ func (self *MenuContext) RangeSelectEnabled() bool { return false } -func (self *MenuContext) FilterPrefix(tr *i18n.TranslationSet) string { - if self.allowFilteringKeybindings { - return tr.FilterPrefixMenu +// A menu that filters as you type points the keyboard at its filter input, so +// that whatever the user types ends up there. Keys that the input doesn't take +// still reach the menu, because the input view is embedded in the menu view. +func (self *MenuContext) GetInputViewName() string { + if self.filterAsYouType { + return self.c.Views().MenuFilter.Name() } - return self.FilteredListViewModel.FilterPrefix(tr) + return self.GetViewName() } diff --git a/pkg/gui/context/merge_conflicts_context.go b/pkg/gui/context/merge_conflicts_context.go index 2ab446c06..dd1060288 100644 --- a/pkg/gui/context/merge_conflicts_context.go +++ b/pkg/gui/context/merge_conflicts_context.go @@ -35,12 +35,12 @@ func NewMergeConflictsContext( viewModel: viewModel, Context: NewSimpleContext( NewBaseContext(NewBaseContextOpts{ - Kind: types.MAIN_CONTEXT, - View: c.Views().MergeConflicts, - WindowName: "main", - Key: MERGE_CONFLICTS_CONTEXT_KEY, - Focusable: true, - HighlightOnFocus: true, + Kind: types.MAIN_CONTEXT, + View: c.Views().MergeConflicts, + WindowName: "main", + Key: MERGE_CONFLICTS_CONTEXT_KEY, + Focusable: true, + HasSelectableContent: true, }), ), c: c, diff --git a/pkg/gui/context/patch_explorer_context.go b/pkg/gui/context/patch_explorer_context.go index 334c2e374..434de6e58 100644 --- a/pkg/gui/context/patch_explorer_context.go +++ b/pkg/gui/context/patch_explorer_context.go @@ -47,7 +47,7 @@ func NewPatchExplorerContext( Key: key, Kind: types.MAIN_CONTEXT, Focusable: true, - HighlightOnFocus: true, + HasSelectableContent: true, NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES, })), SearchTrait: NewSearchTrait(c), diff --git a/pkg/gui/context/simple_context.go b/pkg/gui/context/simple_context.go index 83d201f3a..2de4199e2 100644 --- a/pkg/gui/context/simple_context.go +++ b/pkg/gui/context/simple_context.go @@ -33,21 +33,16 @@ func NewDisplayContext(key types.ContextKey, view *gocui.View, windowName string } func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) { - if self.highlightOnFocus { - self.GetViewTrait().SetHighlight(true) - } - for _, fn := range self.onFocusFns { fn(opts) } - if self.onRenderToMainFn != nil { + if self.onRenderToMainFn != nil && !opts.SkipMainViewUpdate { self.onRenderToMainFn() } } func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) { - self.GetViewTrait().SetHighlight(false) self.view.SetOriginX(0) for _, fn := range self.onFocusLostFns { fn(opts) diff --git a/pkg/gui/context/view_trait.go b/pkg/gui/context/view_trait.go index 8e12e083f..9fc078e61 100644 --- a/pkg/gui/context/view_trait.go +++ b/pkg/gui/context/view_trait.go @@ -43,11 +43,6 @@ func (self *ViewTrait) SetContent(content string) { self.view.SetContent(content) } -func (self *ViewTrait) SetHighlight(highlight bool) { - self.view.Highlight = highlight - self.view.HighlightInactive = false -} - func (self *ViewTrait) SetFooter(value string) { self.view.Footer = value } diff --git a/pkg/gui/context/worktrees_context.go b/pkg/gui/context/worktrees_context.go index 3e45f2d45..690fa6d4b 100644 --- a/pkg/gui/context/worktrees_context.go +++ b/pkg/gui/context/worktrees_context.go @@ -16,8 +16,8 @@ var _ types.IListContext = (*WorktreesContext)(nil) func NewWorktreesContext(c *ContextCommon) *WorktreesContext { viewModel := NewFilteredListViewModel( func() []*models.Worktree { return c.Model().Worktrees }, - func(Worktree *models.Worktree) []string { - return []string{Worktree.Name} + func(worktree *models.Worktree) []string { + return []string{worktree.Name, worktree.Branch} }, ) diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index a7eb35919..f21fb607f 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -138,6 +138,9 @@ func (gui *Gui) resetHelpersAndControllers() { common := controllers.NewControllerCommon(helperCommon, gui) + listControllerFactory := controllers.NewListControllerFactory(common) + menuListController := listControllerFactory.Create(gui.State.Contexts.Menu) + syncController := controllers.NewSyncController( common, ) @@ -156,7 +159,7 @@ func (gui *Gui) resetHelpersAndControllers() { remoteBranchesController := controllers.NewRemoteBranchesController(common) - menuController := controllers.NewMenuController(common) + menuController := controllers.NewMenuController(common, menuListController) localCommitsController := controllers.NewLocalCommitsController(common, syncController.HandlePull) tagsController := controllers.NewTagsController(common) filesController := controllers.NewFilesController( @@ -359,6 +362,7 @@ func (gui *Gui) resetHelpersAndControllers() { controllers.AttachControllers(gui.State.Contexts.Menu, menuController, + menuListController, ) controllers.AttachControllers(gui.State.Contexts.CommitMessage, @@ -412,8 +416,11 @@ func (gui *Gui) resetHelpersAndControllers() { ) // this must come last so that we've got our click handlers defined against the context - listControllerFactory := controllers.NewListControllerFactory(common) for _, context := range gui.c.Context().AllList() { + if context == gui.State.Contexts.Menu { + // already attached above, next to the menu controller that delegates to it + continue + } controllers.AttachControllers(context, listControllerFactory.Create(context)) } } diff --git a/pkg/gui/controllers/basic_commits_controller.go b/pkg/gui/controllers/basic_commits_controller.go index 410335712..f698f6cf0 100644 --- a/pkg/gui/controllers/basic_commits_controller.go +++ b/pkg/gui/controllers/basic_commits_controller.go @@ -157,6 +157,18 @@ func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) e } } + commitTagsItem := &types.MenuItem{ + Label: self.c.Tr.CommitTags, + OnPress: func() error { + return self.copyCommitTagsToClipboard(commit) + }, + Keys: menuKey('t'), + } + + if len(commit.Tags) == 0 { + commitTagsItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CommitHasNoTags} + } + items := []*types.MenuItem{ { Label: self.c.Tr.CommitHash, @@ -207,22 +219,9 @@ func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) e }, Keys: menuKey('a'), }, + commitTagsItem, } - commitTagsItem := types.MenuItem{ - Label: self.c.Tr.CommitTags, - OnPress: func() error { - return self.copyCommitTagsToClipboard(commit) - }, - Keys: menuKey('t'), - } - - if len(commit.Tags) == 0 { - commitTagsItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CommitHasNoTags} - } - - items = append(items, &commitTagsItem) - return self.c.Menu(types.CreateMenuOptions{ Title: self.c.Tr.Actions.CopyCommitAttributeToClipboard, Items: items, diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 748355f67..4aa46a28c 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -616,24 +616,9 @@ func (self *CommitFilesController) GetOnClickFocusedMainView() func(mainViewName } } -// pathsForDiff returns the file paths to use for a diff command. When a text -// filter is active and the node is a directory, only the visible (filtered) -// file paths are returned so the diff reflects what the user sees. func (self *CommitFilesController) pathsForDiff(node *filetree.CommitFileNode) []string { - if !node.IsFile() && self.context().IsFiltering() { - var paths []string - _ = node.ForEachFile(func(file *models.CommitFile) error { - // For a rename we need to pass both paths so that git detects it as - // a rename rather than an unrelated delete and add. - paths = append(paths, file.Names()...) - return nil - }) - return paths - } - if file := node.GetFile(); file != nil { - return file.Names() - } - return []string{node.GetPath()} + return diffPathsForNode( + node.Raw(), self.context().GetRoot().Raw(), self.c.Model().CommitFiles, self.context().IsFiltering()) } // NOTE: these functions are identical to those in files_controller.go (except for types) and diff --git a/pkg/gui/controllers/diff_paths.go b/pkg/gui/controllers/diff_paths.go new file mode 100644 index 000000000..e9c12606f --- /dev/null +++ b/pkg/gui/controllers/diff_paths.go @@ -0,0 +1,132 @@ +package controllers + +import ( + "path" + "strings" + + "github.com/jesseduffield/lazygit/pkg/gui/filetree" + "github.com/samber/lo" +) + +// Both models.File and models.CommitFile satisfy this. Names returns the file's +// path, plus the path it was renamed from if it is a rename. +type fileWithNames[T any] interface { + *T + GetPath() string + GetPreviousPath() string + Names() []string +} + +// diffPathsForNode returns the paths to limit a diff command to for showing the +// changes of the given node. files are all the files that the diff contains, +// while root is the root of the tree the node belongs to, which holds only the +// files matching the text filter when there is one. +func diffPathsForNode[T any, PT fileWithNames[T]](node *filetree.Node[T], root *filetree.Node[T], files []*T, isFiltering bool) []string { + if file := node.GetFile(); file != nil { + return PT(file).Names() + } + + dir := node.GetPath() + + if isFiltering { + // Passing the directory would bring back the files that the filter hides, + // so we spell out the ones it leaves. + var paths []string + for _, file := range filesInDir[T, PT](filesInTree(root), dir) { + paths = append(paths, PT(file).Names()...) + } + return paths + } + + // The directory covers everything below it, but git only pairs up the two + // ends of a rename if both are in the pathspec, and one end can well be + // outside the directory. Without that end we would get an addition or a + // deletion where the diff has a rename. + var outsidePaths []string + for _, f := range filesInDir[T, PT](files, dir) { + file := PT(f) + if p := file.GetPath(); !isInDir(p, dir) { + outsidePaths = append(outsidePaths, p) + } + if p := file.GetPreviousPath(); p != "" && !isInDir(p, dir) { + outsidePaths = append(outsidePaths, p) + } + } + + return dropContainedPaths(append([]string{dir}, collapseToDirs[T, PT](outsidePaths, files, dir)...)) +} + +// dropContainedPaths removes the paths that another one of them contains, since +// a pathspec that matches a directory matches everything below it anyway. +func dropContainedPaths(paths []string) []string { + return lo.Filter(paths, func(p string, _ int) bool { + return !lo.SomeBy(paths, func(other string) bool { + return other != p && isInDir(p, other) + }) + }) +} + +// collapseToDirs replaces each of the given paths with the highest directory +// that can stand in for it, so that moving a whole directory elsewhere costs a +// single pathspec rather than one per file. There is a limit to how long a +// command line may get, and a commit can move a great many files at once. +func collapseToDirs[T any, PT fileWithNames[T]](paths []string, files []*T, dir string) []string { + if len(paths) == 0 { + return nil + } + + // A directory can stand in for the paths under it as long as everything it + // contains ends up in the diff anyway, which is to say as long as all of it + // is in the directory we are diffing too. + canStandIn := make(map[string]bool) + standsIn := func(candidate string) bool { + if result, ok := canStandIn[candidate]; ok { + return result + } + + result := lo.EveryBy(files, func(file *T) bool { + return !fileIsInDir[T, PT](file, candidate) || fileIsInDir[T, PT](file, dir) + }) + canStandIn[candidate] = result + return result + } + + return lo.Uniq(lo.Map(paths, func(p string, _ int) string { + // A directory that can't stand in for the path rules out its parents + // too, since they contain everything it contains. We stop short of the + // repository root: it would leave the command with nothing to say about + // the directory whose diff we are showing. + for candidate := path.Dir(p); candidate != "." && standsIn(candidate); candidate = path.Dir(candidate) { + p = candidate + } + return p + })) +} + +func filesInTree[T any](root *filetree.Node[T]) []*T { + files := []*T{} + _ = root.ForEachFile(func(file *T) error { + files = append(files, file) + return nil + }) + return files +} + +// filesInDir returns the files that the given directory contains, either at +// their current or at their previous path. +func filesInDir[T any, PT fileWithNames[T]](files []*T, dir string) []*T { + return lo.Filter(files, func(file *T, _ int) bool { + return fileIsInDir[T, PT](file, dir) + }) +} + +func fileIsInDir[T any, PT fileWithNames[T]](f *T, dir string) bool { + file := PT(f) + previousPath := file.GetPreviousPath() + return isInDir(file.GetPath(), dir) || (previousPath != "" && isInDir(previousPath, dir)) +} + +func isInDir(path string, dir string) bool { + // "." is the root item, which contains every file + return dir == "." || strings.HasPrefix(path, dir+"/") +} diff --git a/pkg/gui/controllers/diff_paths_test.go b/pkg/gui/controllers/diff_paths_test.go new file mode 100644 index 000000000..549ed4f31 --- /dev/null +++ b/pkg/gui/controllers/diff_paths_test.go @@ -0,0 +1,113 @@ +package controllers + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/filetree" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" +) + +func TestDiffPathsForNode(t *testing.T) { + files := []*models.CommitFile{ + {Path: "dir/file1", PreviousPath: "file1", ChangeStatus: "R"}, + {Path: "dir/file2-renamed", PreviousPath: "dir/file2", ChangeStatus: "R"}, + {Path: "dir/sub/file3", ChangeStatus: "M"}, + {Path: "file4", PreviousPath: "dir/sub/file4", ChangeStatus: "R"}, + {Path: "file5", ChangeStatus: "M"}, + } + + scenarios := []struct { + testName string + files []*models.CommitFile // defaults to the files above + selectedPath string + isFiltering bool + expectedPaths []string + }{ + { + testName: "file", + selectedPath: "dir/sub/file3", + expectedPaths: []string{"dir/sub/file3"}, + }, + { + testName: "renamed file", + selectedPath: "dir/file1", + expectedPaths: []string{"dir/file1", "file1"}, + }, + { + testName: "directory: pass the other end of each rename that crosses its boundary", + selectedPath: "dir", + // dir/file2-renamed was renamed within the directory, so both of its + // paths are covered by it already + expectedPaths: []string{"dir", "file1", "file4"}, + }, + { + testName: "directory without renames crossing its boundary", + selectedPath: "dir/sub", + expectedPaths: []string{"dir/sub", "file4"}, + }, + { + testName: "root", + selectedPath: ".", + expectedPaths: []string{"."}, + }, + { + testName: "a whole directory moved into the selected one collapses to that directory", + files: []*models.CommitFile{ + {Path: "dir/a", PreviousPath: "src/a", ChangeStatus: "R"}, + {Path: "dir/b", PreviousPath: "src/nested/b", ChangeStatus: "R"}, + {Path: "dir/c", PreviousPath: "src/nested/c", ChangeStatus: "R"}, + {Path: "unrelated", ChangeStatus: "M"}, + }, + selectedPath: "dir", + expectedPaths: []string{"dir", "src"}, + }, + { + testName: "a directory that stands in for the selected one as well", + files: []*models.CommitFile{ + {Path: "a/b/c", PreviousPath: "a/c", ChangeStatus: "R"}, + {Path: "a/b/d", ChangeStatus: "M"}, + {Path: "unrelated", ChangeStatus: "M"}, + }, + selectedPath: "a/b", + expectedPaths: []string{"a"}, + }, + { + testName: "a directory with changes of its own doesn't collapse", + files: []*models.CommitFile{ + {Path: "dir/a", PreviousPath: "src/a", ChangeStatus: "R"}, + {Path: "dir/b", PreviousPath: "src/nested/b", ChangeStatus: "R"}, + {Path: "src/nested/c", ChangeStatus: "M"}, + }, + selectedPath: "dir", + // src/nested is left out of it, so that only src/a stays behind + expectedPaths: []string{"dir", "src/a", "src/nested/b"}, + }, + { + testName: "directory while filtering", + selectedPath: "dir", + isFiltering: true, + expectedPaths: []string{ + "dir/file1", "file1", + "dir/file2-renamed", "dir/file2", + "dir/sub/file3", + "file4", "dir/sub/file4", + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + files := lo.Ternary(s.files != nil, s.files, files) + cmp := filetree.NodeSortComparator[models.CommitFile]("mixed", true) + root := filetree.BuildTreeFromCommitFiles(files, true, cmp) + node, found := lo.Find(root.Flatten(filetree.NewCollapsedPaths()), func(node *filetree.Node[models.CommitFile]) bool { + return node.GetPath() == s.selectedPath + }) + assert.True(t, found, "no node for path %s", s.selectedPath) + + assert.Equal(t, s.expectedPaths, diffPathsForNode(node, root, files, s.isFiltering)) + }) + } +} diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 41f9197fe..e61e1409c 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -130,10 +130,11 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types OpensMenu: true, }, { - Keys: opts.GetKeys(opts.Config.Files.ToggleStagedAll), - Handler: self.toggleStagedAll, - Description: self.c.Tr.ToggleStagedAll, - Tooltip: self.c.Tr.ToggleStagedAllTooltip, + Keys: opts.GetKeys(opts.Config.Files.ToggleStagedAll), + Handler: self.toggleStagedAll, + GetDisabledReason: self.require(self.anyFilesDisplayed), + Description: self.c.Tr.ToggleStagedAll, + Tooltip: self.c.Tr.ToggleStagedAllTooltip, }, { Keys: opts.GetKeys(opts.Config.Universal.GoInto), @@ -368,8 +369,8 @@ func (self *FilesController) renderWorkingTreeDiff(node *filetree.FileNode) { split := self.c.UserConfig().Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges()) mainShowsStaged := !split && node.GetHasStagedChanges() - pathOverrides := self.pathOverridesForDiff(node) - cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, pathOverrides) + paths := self.pathsForDiff(node) + cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, paths) title := self.c.Tr.UnstagedChanges if mainShowsStaged { title = self.c.Tr.StagedChanges @@ -384,7 +385,7 @@ func (self *FilesController) renderWorkingTreeDiff(node *filetree.FileNode) { } if split { - cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, pathOverrides) + cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, paths) title := self.c.Tr.StagedChanges if mainShowsStaged { @@ -642,19 +643,9 @@ func (self *FilesController) press(nodes []*filetree.FileNode) error { return nil } -// pathOverridesForDiff returns file paths to override the node's path in diff -// commands when a text filter is active and the node is a directory. This -// ensures the diff only shows filtered/visible files. -func (self *FilesController) pathOverridesForDiff(node *filetree.FileNode) []string { - if !node.IsFile() && self.context().IsFiltering() { - var paths []string - _ = node.ForEachFile(func(file *models.File) error { - paths = append(paths, file.Path) - return nil - }) - return paths - } - return nil +func (self *FilesController) pathsForDiff(node *filetree.FileNode) []string { + return diffPathsForNode( + node.Raw(), self.context().GetRoot().Raw(), self.c.Model().Files, self.context().IsFiltering()) } // unstageFilteredFiles unstages only the visible (filtered) files from the @@ -916,6 +907,17 @@ func (self *FilesController) openSubmoduleConflictMenu(file *models.File) error }) } +// The stage-all command acts on the file tree as it is displayed, so there has +// to be something in it. This is also the case before the first files refresh +// has come in, when there is no tree at all yet. +func (self *FilesController) anyFilesDisplayed() *types.DisabledReason { + if self.context().FileTreeViewModel.Len() == 0 { + return &types.DisabledReason{Text: self.c.Tr.NoChangedFiles} + } + + return nil +} + func (self *FilesController) toggleStagedAll() error { if err := self.toggleStagedAllWithLock(); err != nil { return err diff --git a/pkg/gui/controllers/filter_controller.go b/pkg/gui/controllers/filter_controller.go index 358fb8ed5..830a5bbc6 100644 --- a/pkg/gui/controllers/filter_controller.go +++ b/pkg/gui/controllers/filter_controller.go @@ -33,7 +33,17 @@ func (self *FilterController) Context() types.Context { return self.context } +// A context that filters as the user types has an input field of its own, so it +// has no use for the filter prompt. +type contextThatFiltersAsYouType interface { + FilterAsYouType() bool +} + func (self *FilterController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + if context, ok := self.context.(contextThatFiltersAsYouType); ok && context.FilterAsYouType() { + return nil + } + return []*types.Binding{ { Keys: opts.GetKeys(opts.Config.Universal.StartSearch), @@ -44,5 +54,6 @@ func (self *FilterController) GetKeybindings(opts types.KeybindingsOpts) []*type } func (self *FilterController) OpenFilterPrompt() error { - return self.c.Helpers().Search.OpenFilterPrompt(self.context) + self.c.Helpers().Search.OpenFilterPrompt(self.context) + return nil } diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index beffeb5e2..f525c1d8a 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -9,6 +9,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" ) type ConfirmationHelper struct { @@ -323,19 +324,70 @@ func (self *ConfirmationHelper) ResizeCurrentPopupPanels() { } } +// The rows that a filter row adds to a menu popup: one for the input, and one +// for its bottom border. Its top border is the menu's bottom border. +const menuFilterRowHeight = 2 + +// The prompts for the filter row, from the most to the least informative. The +// keybindings menu can also filter by keybinding, which is worth spelling out +// when there is room for it. +func (self *ConfirmationHelper) menuFilterPromptCandidates() []string { + if self.c.Contexts().Menu.AllowFilteringKeybindings() { + return []string{self.c.Tr.FilterPrefixMenu, self.c.Tr.FilterPrefix} + } + + return []string{self.c.Tr.FilterPrefix} +} + +// Returns the first prompt that still leaves room to type in, or no prompt at +// all if the row is too narrow even for the shortest one. +func menuFilterPrompt(candidates []string, contentWidth int) string { + const minimumInputWidth = 4 + + for _, candidate := range candidates { + if utils.StringWidth(candidate)+minimumInputWidth <= contentWidth { + return candidate + } + } + + return "" +} + func (self *ConfirmationHelper) resizeMenu(parentPopupContext types.Context) { + menuContext := self.c.Contexts().Menu // we want the unfiltered length here so that if we're filtering we don't // resize the window - itemCount := self.c.Contexts().Menu.UnfilteredLen() + itemCount := menuContext.UnfilteredLen() offset := 3 panelWidth := self.getPopupPanelWidth(90) contentWidth := panelWidth - 2 // minus 2 for the frame promptLinesCount := self.layoutMenuPrompt(contentWidth) - x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(contentWidth, itemCount+offset+promptLinesCount, parentPopupContext) - menuBottom := y1 - offset + // The row is reserved for the whole time the menu is open, even though it only + // becomes visible once the user starts typing, so that revealing it doesn't + // move the menu. + filterRowHeight := lo.Ternary(menuContext.FilterAsYouType(), menuFilterRowHeight, 0) + x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight( + contentWidth, itemCount+offset+promptLinesCount+filterRowHeight, parentPopupContext) + menuBottom := y1 - offset - filterRowHeight _, _ = self.c.GocuiGui().SetView(self.c.Views().Menu.Name(), x0, y0, x1, menuBottom, 0) tooltipTop := menuBottom + 1 + if menuContext.FilterAsYouType() { + filterRowBottom := menuBottom + filterRowHeight + // The row hangs off the bottom of the menu, sharing its bottom border. + _, _ = self.c.GocuiGui().SetView(self.c.Views().MenuFilterFrame.Name(), x0, menuBottom, x1, filterRowBottom, 0) + + prompt := menuFilterPrompt(self.menuFilterPromptCandidates(), contentWidth) + self.c.Views().MenuFilterFrame.SetContent(prompt) + // A view's content starts one column inside its bounds, so the input field + // starts one column to the left of where its text is to appear. + inputLeft := x0 + utils.StringWidth(prompt) + _, _ = self.c.GocuiGui().SetView(self.c.Views().MenuFilter.Name(), inputLeft, menuBottom, x1, filterRowBottom, 0) + + if menuContext.FilterStarted() { + tooltipTop = filterRowBottom + 1 + } + } tooltip := "" selectedItem := self.c.Contexts().Menu.GetSelected() if selectedItem != nil { diff --git a/pkg/gui/controllers/helpers/confirmation_helper_test.go b/pkg/gui/controllers/helpers/confirmation_helper_test.go new file mode 100644 index 000000000..fc62722d0 --- /dev/null +++ b/pkg/gui/controllers/helpers/confirmation_helper_test.go @@ -0,0 +1,31 @@ +package helpers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMenuFilterPrompt(t *testing.T) { + longPrompt := "Filter ('@' for keybindings): " + shortPrompt := "Filter: " + + tests := []struct { + name string + candidates []string + contentWidth int + expected string + }{ + {name: "room for four characters", candidates: []string{shortPrompt}, contentWidth: 12, expected: shortPrompt}, + {name: "room for three characters", candidates: []string{shortPrompt}, contentWidth: 11, expected: ""}, + {name: "prefers the first candidate", candidates: []string{longPrompt, shortPrompt}, contentWidth: 34, expected: longPrompt}, + {name: "falls back to the next one", candidates: []string{longPrompt, shortPrompt}, contentWidth: 33, expected: shortPrompt}, + {name: "measures display width", candidates: []string{"篩選: "}, contentWidth: 9, expected: ""}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, menuFilterPrompt(test.candidates, test.contentWidth)) + }) + } +} diff --git a/pkg/gui/controllers/helpers/fixup_helper.go b/pkg/gui/controllers/helpers/fixup_helper.go index e8fa43f2d..e998c2ad1 100644 --- a/pkg/gui/controllers/helpers/fixup_helper.go +++ b/pkg/gui/controllers/helpers/fixup_helper.go @@ -141,7 +141,6 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error { } self.c.Contexts().LocalCommits.SetSelection(index) - self.c.Contexts().LocalCommits.FocusLine(true) self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{}) return nil }, diff --git a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go index 7c7ab3e9a..8f27efa08 100644 --- a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go +++ b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go @@ -188,9 +188,8 @@ func (self *MergeAndRebaseHelper) hasExecTodos(calledFromWorker bool) bool { } result := false - _ = self.c.GocuiGui().OnUIThreadAndWait(func() error { + _ = self.c.GocuiGui().OnUIThreadAndWait(func() { result = check() - return nil }) return result } diff --git a/pkg/gui/controllers/helpers/mode_helper.go b/pkg/gui/controllers/helpers/mode_helper.go index 6e5139744..68f7ea149 100644 --- a/pkg/gui/controllers/helpers/mode_helper.go +++ b/pkg/gui/controllers/helpers/mode_helper.go @@ -270,11 +270,6 @@ func (self *ModeHelper) changeFiltering(setFilter func(), selectCommit func()) e selectCommit() self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits) - // The list we just selected in has nothing to do with the one - // that was showing, so wherever it was scrolled to says nothing - // about where the selection now is. PostRefreshUpdate leaves the - // scroll position alone, so ask for it separately. - self.c.Contexts().LocalCommits.FocusLine(true) return nil }, }) diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 53505ba6c..730ed9a24 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -111,6 +111,17 @@ type refreshEnv struct { // persist its refreshed stat cache. backgroundRoutine bool + // Whether the views this refresh updates must keep the scroll position they + // have. Focusing a list scrolls its selection into view, which is what a + // user action should do — but a refresh that no user action is behind must + // leave the viewport wherever the user last scrolled it to. That's the case + // for the unattended background routines, and for the refreshes that merely + // reload state (see RefreshOptions.DontBlockRepoSwitch). + keepScrollPosition bool + + // Whether refreshing a side context should leave the main view unchanged. + skipMainViewUpdate bool + // the repo generation captured when the refresh started generation int @@ -220,13 +231,17 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr // against the repo it started in, and the generation guard drops its // writes. env := refreshEnv{ - background: options.Background || options.DontBlockRepoSwitch, - backgroundRoutine: options.Background, + background: options.Background || options.DontBlockRepoSwitch, + backgroundRoutine: options.Background, + keepScrollPosition: options.Background || options.DontBlockRepoSwitch, + skipMainViewUpdate: options.SkipMainViewUpdate, } - self.captureOnUIThread(calledFromWorker, env.background, func() { + if !self.captureOnUIThread(calledFromWorker, env.background, func() { env.generation = self.c.State().GetRepoGeneration() env.git = self.c.Git() - }) + }) { + return + } if options.BatchUIUpdates { env.batch = &refreshBounceBatch{} } @@ -262,6 +277,10 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr // - merge conflicts are part of what the files refresh produces // - pull requests are fetched for the tracking branches against the // remotes, so refresh both alongside to fetch against fresh data + // - commits and branches always go together: changing commits changes + // the branches' upstream/downstream counts, and changing branches + // (e.g. checking one out) changes the commits we show. This one comes + // last, so that it also covers the branches the rules above add. if scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) { scopeSet.Add(types.COMMITS, types.BRANCHES) } @@ -274,6 +293,9 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr if scopeSet.Includes(types.PULL_REQUESTS) { scopeSet.Add(types.BRANCHES, types.REMOTES) } + if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) { + scopeSet.Add(types.COMMITS, types.BRANCHES) + } // Capture the refs snapshot now, before we start reading git's state // below, rather than after. This is important to guard against the race @@ -300,6 +322,23 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr }) } + // The branches view shows worktrees against branches, so a branches render + // that happens before the refreshed worktrees have landed in the model shows + // stale ones, and rendering again once they land makes the view flicker. + // Refresh the worktrees first, then, and let the branches refresh wait for + // them: waitForWorktrees returns once the worktrees model write is queued, + // so the branches write that follows is queued behind it and the view + // renders once, with both. + worktreesWg := sync.WaitGroup{} + waitForWorktrees := func() { worktreesWg.Wait() } + if scopeSet.Includes(types.WORKTREES) { + worktreesWg.Add(1) + refresh("worktrees", func() { + defer worktreesWg.Done() + self.refreshWorktrees(env, scopeSet.Includes(types.BRANCHES)) + }) + } + branchesAndRemotesWg := sync.WaitGroup{} // The pull-request fetch (below) needs the just-loaded branches and // remotes. Their model writes are bounced onto the UI thread, so the @@ -309,32 +348,49 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr // branchesAndRemotesWg gives the fetch the happens-before to read them. var loadedBranches []*models.Branch var loadedRemotes []*models.Remote - includeWorktreesWithBranches := false - if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) { - // whenever we change commits, we should update branches because the upstream/downstream - // counts can change. Whenever we change branches we should also change commits - // e.g. in the case of switching branches. - // Capture the commits, reflog and branches refresh inputs (model, - // contexts, modes) on the UI thread, before the git work is dispatched - // to a worker, so the workers compute from an immutable snapshot - // instead of reading state the UI thread concurrently mutates. + if scopeSet.Includes(types.COMMITS) { + // Capture the refresh's inputs (model, contexts, modes) on the UI + // thread, before the git work is dispatched to a worker, so the worker + // computes from an immutable snapshot instead of reading state the UI + // thread concurrently mutates. Every scope below does the same. var capturedCommits capturedCommitState - var capturedReflog capturedReflogState - var capturedBranches capturedBranchState - self.captureOnUIThread(calledFromWorker, env.background, func() { + if !self.captureOnUIThread(calledFromWorker, env.background, func() { capturedCommits = self.captureCommitsState() - capturedReflog = self.captureReflogState() - capturedBranches = self.captureBranchState() - }) + }) { + return + } refresh("commits and commit files", func() { self.refreshCommitsAndCommitFiles(capturedCommits, options.CommitSelection, env) }) + } else if scopeSet.Includes(types.REBASE_COMMITS) { + // the commits refresh above loads the rebase commits as well, so we only + // need this one when the rebase commits are all that was asked for + var rebaseHashPool *utils.StringPool + var rebaseCommits []*models.Commit + if !self.captureOnUIThread(calledFromWorker, env.background, func() { + rebaseHashPool, rebaseCommits = self.captureRebaseCommitState() + }) { + return + } + refresh("rebase commits", func() { _ = self.refreshRebaseCommits(rebaseHashPool, rebaseCommits, env) }) + } + + if scopeSet.Includes(types.BRANCHES) { + // The reflog is refreshed here rather than in a scope of its own, + // because sorting the branches by recency needs it to be loaded first. + var capturedReflog capturedReflogState + var capturedBranches capturedBranchState + if !self.captureOnUIThread(calledFromWorker, env.background, func() { + capturedReflog = self.captureReflogState() + capturedBranches = self.captureBranchState() + }) { + return + } - includeWorktreesWithBranches = scopeSet.Includes(types.WORKTREES) if self.c.UserConfig().Git.LocalBranchSortOrder == "recency" { branchesAndRemotesWg.Add(1) refresh("reflog and branches", func() { - loadedBranches = self.refreshReflogAndBranches(capturedReflog, capturedBranches, includeWorktreesWithBranches, options.BranchSelection, options.SelectTopReflogCommit, env) + loadedBranches = self.refreshReflogAndBranches(capturedReflog, capturedBranches, waitForWorktrees, options.BranchSelection, options.SelectTopReflogCommit, env) branchesAndRemotesWg.Done() }) } else { @@ -343,47 +399,44 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr // Not a recency sort, so branches doesn't depend on the reflog // being fresh; it runs concurrently with the reflog refresh // below and uses the reflog we captured up front, as it always has. - loadedBranches = self.refreshBranches(capturedBranches, includeWorktreesWithBranches, options.BranchSelection, true, capturedReflog.reflogCommits, env) + loadedBranches = self.refreshBranches(capturedBranches, waitForWorktrees, options.BranchSelection, true, capturedReflog.reflogCommits, env) branchesAndRemotesWg.Done() }) refresh("reflog", func() { _, _ = self.refreshReflogCommits(capturedReflog, env, options.SelectTopReflogCommit) }) } - } else if scopeSet.Includes(types.REBASE_COMMITS) { - // the above block handles rebase commits so we only need to call this one - // if we've asked specifically for rebase commits and not those other things - var rebaseHashPool *utils.StringPool - var rebaseCommits []*models.Commit - self.captureOnUIThread(calledFromWorker, env.background, func() { - rebaseHashPool, rebaseCommits = self.captureRebaseCommitState() - }) - refresh("rebase commits", func() { _ = self.refreshRebaseCommits(rebaseHashPool, rebaseCommits, env) }) } if scopeSet.Includes(types.SUB_COMMITS) { var capturedSubCommits capturedSubCommitState - self.captureOnUIThread(calledFromWorker, env.background, func() { + if !self.captureOnUIThread(calledFromWorker, env.background, func() { capturedSubCommits = self.captureSubCommitState() - }) + }) { + return + } refresh("sub commits", func() { _ = self.refreshSubCommitsWithLimit(capturedSubCommits, env) }) } // reason we're not doing this if the COMMITS type is included is that if the COMMITS type _is_ included we will refresh the commit files context anyway if scopeSet.Includes(types.COMMIT_FILES) && !scopeSet.Includes(types.COMMITS) { var capturedCommitFiles capturedCommitFilesState - self.captureOnUIThread(calledFromWorker, env.background, func() { + if !self.captureOnUIThread(calledFromWorker, env.background, func() { capturedCommitFiles = self.captureCommitFilesState() - }) + }) { + return + } refresh("commit files", func() { _ = self.refreshCommitFilesContext(capturedCommitFiles, env) }) } fileWg := sync.WaitGroup{} if scopeSet.Includes(types.FILES) { var capturedFiles capturedFilesState - self.captureOnUIThread(calledFromWorker, env.background, func() { + if !self.captureOnUIThread(calledFromWorker, env.background, func() { capturedFiles = self.captureFilesState() - }) + }) { + return + } fileWg.Add(1) refresh("files", func() { _ = self.refreshFilesAndSubmodules(capturedFiles, env) @@ -393,9 +446,11 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr if scopeSet.Includes(types.STASH) { var stashFilterPath string - self.captureOnUIThread(calledFromWorker, env.background, func() { + if !self.captureOnUIThread(calledFromWorker, env.background, func() { stashFilterPath = self.c.Modes().Filtering.GetPath() - }) + }) { + return + } refresh("stash", func() { self.refreshStashEntries(stashFilterPath, env) }) } @@ -408,9 +463,11 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr // needs it to keep the remote-branches selection valid, and reading // the Remotes context off the UI thread races its render. var prevSelectedRemote *models.Remote - self.captureOnUIThread(calledFromWorker, env.background, func() { + if !self.captureOnUIThread(calledFromWorker, env.background, func() { prevSelectedRemote = self.c.Contexts().Remotes.GetSelected() - }) + }) { + return + } branchesAndRemotesWg.Add(1) refresh("remotes", func() { loadedRemotes, _ = self.refreshRemotes(prevSelectedRemote, env) @@ -443,10 +500,6 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr }) } - if scopeSet.Includes(types.WORKTREES) && !includeWorktreesWithBranches { - refresh("worktrees", func() { self.refreshWorktrees(env) }) - } - if scopeSet.Includes(types.STAGING) { refresh("staging", func() { fileWg.Wait() @@ -673,17 +726,19 @@ func (self *RefreshHelper) captureBranchState() capturedBranchState { } } -func (self *RefreshHelper) refreshReflogAndBranches(capturedReflog capturedReflogState, capturedBranches capturedBranchState, refreshWorktrees bool, branchSelection types.BranchSelectionBehavior, selectTopReflogCommit bool, env refreshEnv) []*models.Branch { +func (self *RefreshHelper) refreshReflogAndBranches(capturedReflog capturedReflogState, capturedBranches capturedBranchState, waitForWorktrees func(), branchSelection types.BranchSelectionBehavior, selectTopReflogCommit bool, env refreshEnv) []*models.Branch { switch self.c.State().GetRepoState().GetStartupStage() { case types.INITIAL: // Return the immediate (non-recency) load's branches; the recency-sorted // reload below runs on its own worker after we return. Both hold the same // set of branches, which is all the caller (the PR fetch) needs. - branches := self.refreshBranches(capturedBranches, refreshWorktrees, branchSelection, false, capturedReflog.reflogCommits, env) + branches := self.refreshBranches(capturedBranches, waitForWorktrees, branchSelection, false, capturedReflog.reflogCommits, env) self.onWorker(env.background, func(_ gocui.Task) error { reflogCommits, _ := self.refreshReflogCommits(capturedReflog, env, false) - self.refreshBranches(capturedBranches, false, types.SelectCheckedOutBranch, true, reflogCommits, env) + // The load above already waited for the worktrees, so this one has + // nothing left to wait for. + self.refreshBranches(capturedBranches, func() {}, types.SelectCheckedOutBranch, true, reflogCommits, env) self.c.State().GetRepoState().SetStartupStage(types.COMPLETE) return nil }) @@ -692,7 +747,7 @@ func (self *RefreshHelper) refreshReflogAndBranches(capturedReflog capturedReflo case types.COMPLETE: reflogCommits, _ := self.refreshReflogCommits(capturedReflog, env, selectTopReflogCommit) - return self.refreshBranches(capturedBranches, refreshWorktrees, branchSelection, true, reflogCommits, env) + return self.refreshBranches(capturedBranches, waitForWorktrees, branchSelection, true, reflogCommits, env) } return nil @@ -810,9 +865,11 @@ func (self *RefreshHelper) refreshCommitsWithLimit(captured capturedCommitState, self.onUIThreadUnlessRepoChanged(env, func() { var selectionRange *localCommitSelectionRange + var newConflictedCommitIdx *int if commitSelection == types.KeepCommitSelectionByHash { selectedIdx, rangeStartIdx, rangeSelectMode := self.c.Contexts().LocalCommits.GetSelectionRangeAndMode() selectionRange = captureLocalCommitSelectionRange(self.c.Model().Commits, selectedIdx, rangeStartIdx, rangeSelectMode) + newConflictedCommitIdx = findNewConflictedCommit(self.c.Model().Commits, commits) } self.c.Model().BisectInfo = bisectInfo @@ -826,33 +883,23 @@ func (self *RefreshHelper) refreshCommitsWithLimit(captured capturedCommitState, self.c.Model().CheckedOutBranch = "" } - scrollSelectionIntoView := false switch commitSelection { case types.SelectHeadCommit: if headCommitIdx := models.HeadCommitIdx(commits); headCommitIdx >= 0 { self.c.Contexts().LocalCommits.SetSelection(headCommitIdx) - scrollSelectionIntoView = true } case types.KeepCommitSelectionByHash: - if selectionRange != nil { - selectedIdx, rangeStartIdx, didMove, found := findLocalCommitSelectionRange(commits, selectionRange) + if newConflictedCommitIdx != nil { + self.c.Contexts().LocalCommits.SetSelection(*newConflictedCommitIdx) + } else if selectionRange != nil { + selectedIdx, rangeStartIdx, found := findLocalCommitSelectionRange(commits, selectionRange) if found { self.c.Contexts().LocalCommits.SetSelectionRangeAndMode(selectedIdx, rangeStartIdx, selectionRange.mode) - scrollSelectionIntoView = didMove } } case types.KeepCommitSelectionIndex: // The caller set the selection index deliberately; leave it untouched. } - - if scrollSelectionIntoView { - // Enqueued from within this bounce so it runs after refreshView's - // render below (which was enqueued first), matching the previous - // ordering where FocusLine ran after the view was re-rendered. - self.onUIThreadUnlessRepoChanged(env, func() { - self.c.Contexts().LocalCommits.FocusLine(true) - }) - } }) self.refreshView(self.c.Contexts().LocalCommits, env) @@ -864,8 +911,6 @@ type localCommitSelectionRange struct { selectedIsTODO bool rangeStartHash string rangeStartIsTODO bool - selectedIdx int - rangeStartIdx int mode traits.RangeSelectMode } @@ -884,8 +929,6 @@ func captureLocalCommitSelectionRange( selectedIsTODO: commits[selectedIdx].IsTODO(), rangeStartHash: commits[rangeStartIdx].Hash(), rangeStartIsTODO: commits[rangeStartIdx].IsTODO(), - selectedIdx: selectedIdx, - rangeStartIdx: rangeStartIdx, mode: mode, } } @@ -893,17 +936,16 @@ func captureLocalCommitSelectionRange( func findLocalCommitSelectionRange( commits []*models.Commit, selectionRange *localCommitSelectionRange, -) (int, int, bool, bool) { +) (int, int, bool) { selectedIdx, foundSelected := findCommitByHashPreferringTODOStatus( commits, selectionRange.selectedHash, selectionRange.selectedIsTODO) rangeStartIdx, foundRangeStart := findCommitByHashPreferringTODOStatus( commits, selectionRange.rangeStartHash, selectionRange.rangeStartIsTODO) if !foundSelected || !foundRangeStart { - return 0, 0, false, false + return 0, 0, false } - didMove := selectedIdx != selectionRange.selectedIdx || rangeStartIdx != selectionRange.rangeStartIdx - return selectedIdx, rangeStartIdx, didMove, true + return selectedIdx, rangeStartIdx, true } // findCommitByHashPreferringTODOStatus finds the commit with the given hash. @@ -934,6 +976,24 @@ func hasRestorableCommitHash(commits []*models.Commit, idx int) bool { return idx >= 0 && idx < len(commits) && commits[idx].Hash() != "" } +// Returns the index of the conflicted commit in the new commits slice, if there is one and it has a +// different hash than the one before had (or there wasn't one before). Otherwise returns nil. +func findNewConflictedCommit(previousCommits []*models.Commit, commits []*models.Commit) *int { + previousConflictedCommit, _ := lo.Find(previousCommits, func(commit *models.Commit) bool { + return commit.Status == models.StatusConflicted + }) + + newConflictedCommit, idx, hasConflict := lo.FindIndexOf(commits, func(commit *models.Commit) bool { + return commit.Status == models.StatusConflicted + }) + + if hasConflict && (previousConflictedCommit == nil || previousConflictedCommit.Hash() != newConflictedCommit.Hash()) { + return &idx + } + + return nil +} + // capturedSubCommitState holds the sub-commits refresh's model/context/mode // inputs, gathered on the UI thread (see captureSubCommitState) before the git // work is dispatched to a worker. @@ -1074,7 +1134,7 @@ func (self *RefreshHelper) refreshStateSubmoduleConfigs(env refreshEnv) ([]*mode // self.refreshStatus is called at the end of this because that's when we can // be sure there is a State.Model.Branches array to pick the current branch from -func (self *RefreshHelper) refreshBranches(captured capturedBranchState, refreshWorktrees bool, branchSelection types.BranchSelectionBehavior, loadBehindCounts bool, reflogCommits []*models.Commit, env refreshEnv) []*models.Branch { +func (self *RefreshHelper) refreshBranches(captured capturedBranchState, waitForWorktrees func(), branchSelection types.BranchSelectionBehavior, loadBehindCounts bool, reflogCommits []*models.Commit, env refreshEnv) []*models.Branch { loadSeq := self.branchLoadSeq.Add(1) branches, err := env.git.Loaders.BranchLoader.Load( @@ -1108,10 +1168,9 @@ func (self *RefreshHelper) refreshBranches(captured capturedBranchState, refresh self.c.Log.Error(err) } - var worktrees []*models.Worktree - if refreshWorktrees { - worktrees = self.loadWorktrees(env) - } + // Render only once the refreshed worktrees are in the model; the branches + // view shows them against the branches (see performRefresh). + waitForWorktrees() self.onUIThreadUnlessRepoChanged(env, func() { // Drop this write if a branch load that started later has already applied @@ -1134,11 +1193,6 @@ func (self *RefreshHelper) refreshBranches(captured capturedBranchState, refresh // the branches we just wrote, on the UI thread. self.rebuildPullRequestsMap() - if refreshWorktrees { - self.c.Model().Worktrees = worktrees - self.refreshView(self.c.Contexts().Worktrees, env) - } - // Setting the selection here, in the same bounce that writes the list, // keeps it on the UI thread and keeps the list and selection updating in // the same frame. @@ -1154,10 +1208,8 @@ func (self *RefreshHelper) refreshBranches(captured capturedBranchState, refresh } } case types.SelectCheckedOutBranch: - // The checked-out branch is always at the top of the list. Setting - // the selection doesn't scroll the view, so also reset the origin. + // The checked-out branch is always at the top of the list. self.c.Contexts().Branches.SetSelectedLineIdx(0) - self.c.Contexts().Branches.GetView().SetOriginY(0) } // Need to re-render the commits view because the visualization of local @@ -1248,21 +1300,20 @@ func (self *RefreshHelper) onUIThread(background bool, f func() error) { // waiting for a callback that only it can run), and capturing inline also // guarantees the snapshot reflects the state at the moment Refresh was called, // before the calling handler regains control and can mutate it. -func (self *RefreshHelper) captureOnUIThread(calledFromWorker bool, background bool, fn func()) { +// +// It returns false when fn didn't run because the app is shutting down, in +// which case the caller must abandon the refresh rather than compute from a +// snapshot that was never taken. +func (self *RefreshHelper) captureOnUIThread(calledFromWorker bool, background bool, fn func()) bool { if !calledFromWorker { fn() - return + return true } - wrapped := func() error { - fn() - return nil - } if background { - _ = self.c.GocuiGui().OnUIThreadAndWaitBackground(wrapped) - } else { - _ = self.c.GocuiGui().OnUIThreadAndWait(wrapped) + return self.c.GocuiGui().OnUIThreadAndWaitBackground(fn) == nil } + return self.c.GocuiGui().OnUIThreadAndWait(fn) == nil } // capturedFilesState holds the files refresh's context/model inputs, gathered @@ -1329,12 +1380,9 @@ func (self *RefreshHelper) refreshStateFiles(captured capturedFilesState, env re Background: env.backgroundRoutine, }) - conflictFileCount := 0 - for _, file := range files { - if file.HasMergeConflicts { - conflictFileCount++ - } - } + conflictedPaths := lo.FilterMap(files, func(file *models.File, _ int) (string, bool) { + return file.Path, file.HasMergeConflicts + }) repoState := self.c.State().GetRepoState() workingTreeState := env.git.Status.WorkingTreeState() @@ -1344,7 +1392,7 @@ func (self *RefreshHelper) refreshStateFiles(captured capturedFilesState, env re repoState.SetMergeOrRebaseStartedInLazygit(false) } - if workingTreeState.Any() && conflictFileCount == 0 { + if workingTreeState.Any() && len(conflictedPaths) == 0 { if prevConflictFileCount > 0 && repoState.GetMergeOrRebaseStartedInLazygit() { // The conflicts of an operation we started have just been resolved // (e.g. in the user's editor). Offer to continue it. We only do this @@ -1382,24 +1430,61 @@ func (self *RefreshHelper) refreshStateFiles(captured capturedFilesState, env re self.onUIThreadUnlessRepoChanged(env, func() { // only taking over the filter if it hasn't already been set by the user. - if conflictFileCount > 0 && prevConflictFileCount == 0 { + if len(conflictedPaths) > 0 && prevConflictFileCount == 0 { if fileTreeViewModel.GetStatusFilter() == filetree.DisplayAll { fileTreeViewModel.SetStatusFilter(filetree.DisplayConflicted) self.c.Contexts().Files.GetView().Subtitle = self.c.Tr.FilterLabelConflictingFiles } - } else if conflictFileCount == 0 && fileTreeViewModel.GetStatusFilter() == filetree.DisplayConflicted { - fileTreeViewModel.SetStatusFilter(filetree.DisplayAll) + } else if len(conflictedPaths) == 0 && fileTreeViewModel.GetStatusFilter() == filetree.DisplayConflicted { + fileTreeViewModel.SetStatusFilterPreservingSelection(filetree.DisplayAll) self.c.Contexts().Files.GetView().Subtitle = "" } + if fileTreeViewModel.GetStatusFilter() == filetree.DisplayConflicted { + fileTreeViewModel.RememberConflictedPaths(conflictedPaths) + } + self.c.Model().Submodules = submoduleConfigs self.c.Model().Files = files + markWorktreeFiles(files, self.c.Model().Worktrees, env.git.RepoPaths.WorktreePath()) fileTreeViewModel.SetTree() }) return nil } +// markWorktreeFiles marks the files that are linked worktrees of this repo, so +// that the files view can render them as such. `git status` reports a worktree +// as an untracked directory, i.e. with a trailing slash, which we take off: +// keeping it would build a directory node with a nameless file inside it. +// +// It must run on the UI thread, as it works on the model. Both models it needs +// are written by refreshes of their own, so it is called after either of them +// lands; it reports whether it changed anything. +func markWorktreeFiles(files []*models.File, worktrees []*models.Worktree, worktreePath string) bool { + changed := false + + for _, file := range files { + absPath := filepath.Join(worktreePath, file.Path) + isWorktree := lo.SomeBy(worktrees, func(worktree *models.Worktree) bool { + return worktree.Path == absPath + }) + + if isWorktree != file.IsWorktree { + file.IsWorktree = isWorktree + changed = true + } + if isWorktree { + if trimmed := strings.TrimSuffix(file.Path, "/"); trimmed != file.Path { + file.Path = trimmed + changed = true + } + } + } + + return changed +} + // the reflogs panel is the only panel where we cache data, in that we only // load entries that have been created since we last ran the call. This means // we need to be more careful with how we use this, and to ensure we're emptying @@ -1449,11 +1534,9 @@ func (self *RefreshHelper) refreshReflogCommits(captured capturedReflogState, en self.c.Model().ReflogCommits = reflogCommits self.c.Model().FilteredReflogCommits = filteredReflogCommits // Setting the selection here, in the same bounce that writes the list, - // keeps it on the UI thread and atomic with the list update. Setting the - // selection doesn't scroll the view, so also reset the origin. + // keeps it on the UI thread and atomic with the list update. if selectTopEntry { self.c.Contexts().ReflogCommits.SetSelectedLineIdx(0) - self.c.Contexts().ReflogCommits.GetView().SetOriginY(0) } }) @@ -1503,16 +1586,27 @@ func (self *RefreshHelper) loadWorktrees(env refreshEnv) []*models.Worktree { return worktrees } -func (self *RefreshHelper) refreshWorktrees(env refreshEnv) { +func (self *RefreshHelper) refreshWorktrees(env refreshEnv, branchesAreRefreshing bool) { worktrees := self.loadWorktrees(env) self.onUIThreadUnlessRepoChanged(env, func() { self.c.Model().Worktrees = worktrees + + // A worktree inside our working tree is one of the files, so the files + // view has to be told about the ones we just loaded (see + // markWorktreeFiles). Rebuild the tree because a file's path can change. + if markWorktreeFiles(self.c.Model().Files, worktrees, env.git.RepoPaths.WorktreePath()) { + self.c.Contexts().Files.FileTreeViewModel.SetTree() + self.refreshView(self.c.Contexts().Files, env) + } }) - // need to refresh branches because the branches view shows worktrees against - // branches - self.refreshView(self.c.Contexts().Branches, env) + // The branches view shows worktrees against branches, so it needs to be + // rendered again as well. When the branches are being refreshed too, they + // render after waiting for the write above, so leave it to them. + if !branchesAreRefreshing { + self.refreshView(self.c.Contexts().Branches, env) + } self.refreshView(self.c.Contexts().Worktrees, env) } @@ -1580,7 +1674,10 @@ func (self *RefreshHelper) refreshView(context types.Context, env refreshEnv) { // the filtered list model is up to date for rendering. self.searchHelper.ReApplyFilter(context) - self.c.PostRefreshUpdate(context) + self.c.PostRefreshUpdateWithOptions(context, types.OnFocusOpts{ + KeepScrollPosition: env.keepScrollPosition, + SkipMainViewUpdate: env.skipMainViewUpdate, + }) self.c.AfterLayout(func() error { // Re-applying the search must be done after re-rendering the view though, @@ -1756,7 +1853,11 @@ func (self *RefreshHelper) setGithubPullRequests(baseInfo *githubRemoteInfo, bra // the branches and remotes as they are on the UI thread, after their // own refreshes' bounces have applied. self.rebuildPullRequestsMap() - self.c.PostRefreshUpdate(self.c.Contexts().Branches) + // This lands whenever the network call happens to return, and only + // changes how the branches are rendered, not which one is selected, so + // it has no business moving the viewport. + self.c.PostRefreshUpdateWithOptions(self.c.Contexts().Branches, + types.OnFocusOpts{KeepScrollPosition: true}) }) } diff --git a/pkg/gui/controllers/helpers/refresh_helper_test.go b/pkg/gui/controllers/helpers/refresh_helper_test.go index 3a5f6ea82..5e58c56a3 100644 --- a/pkg/gui/controllers/helpers/refresh_helper_test.go +++ b/pkg/gui/controllers/helpers/refresh_helper_test.go @@ -1,6 +1,7 @@ package helpers import ( + "path/filepath" "testing" "github.com/jesseduffield/lazygit/pkg/commands/hosting_service" @@ -28,8 +29,6 @@ func TestCaptureLocalCommitSelectionRange(t *testing.T) { expected: &localCommitSelectionRange{ selectedHash: "b", rangeStartHash: "a", - selectedIdx: 1, - rangeStartIdx: 0, mode: traits.RangeSelectModeSticky, }, }, @@ -74,15 +73,12 @@ func TestFindLocalCommitSelectionRange(t *testing.T) { type expectation struct { selectedIdx int rangeStartIdx int - moved bool found bool } selectionRange := localCommitSelectionRange{ selectedHash: "b", rangeStartHash: "c", - selectedIdx: 1, - rangeStartIdx: 2, mode: traits.RangeSelectModeSticky, } @@ -97,7 +93,6 @@ func TestFindLocalCommitSelectionRange(t *testing.T) { expected: expectation{ selectedIdx: 2, rangeStartIdx: 3, - moved: true, found: true, }, }, @@ -126,7 +121,6 @@ func TestFindLocalCommitSelectionRange(t *testing.T) { expected: expectation{ selectedIdx: 2, rangeStartIdx: 3, - moved: true, found: true, }, }, @@ -139,7 +133,6 @@ func TestFindLocalCommitSelectionRange(t *testing.T) { expected: expectation{ selectedIdx: 0, rangeStartIdx: 1, - moved: true, found: true, }, }, @@ -147,11 +140,10 @@ func TestFindLocalCommitSelectionRange(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - selectedIdx, rangeStartIdx, moved, found := findLocalCommitSelectionRange(testCase.commits, &selectionRange) + selectedIdx, rangeStartIdx, found := findLocalCommitSelectionRange(testCase.commits, &selectionRange) actual := expectation{ selectedIdx: selectedIdx, rangeStartIdx: rangeStartIdx, - moved: moved, found: found, } @@ -160,6 +152,62 @@ func TestFindLocalCommitSelectionRange(t *testing.T) { } } +func TestFindNewConflictedCommit(t *testing.T) { + testCases := []struct { + name string + previousCommits []*models.Commit + commits []*models.Commit + expectedIdx *int + }{ + { + name: "finds a newly conflicted commit", + previousCommits: makeCommits("a", "b"), + commits: []*models.Commit{ + makeCommits("a")[0], + makeConflictedCommit("b"), + }, + expectedIdx: lo.ToPtr(1), + }, + { + name: "finds a different conflicted commit", + previousCommits: []*models.Commit{ + makeConflictedCommit("a"), + }, + commits: []*models.Commit{ + makeConflictedCommit("b"), + }, + expectedIdx: lo.ToPtr(0), + }, + { + name: "ignores the same conflicted commit", + previousCommits: []*models.Commit{ + makeConflictedCommit("a"), + }, + commits: []*models.Commit{ + makeConflictedCommit("a"), + }, + expectedIdx: nil, + }, + { + name: "reports not found when there is no conflict", + previousCommits: makeCommits("a"), + commits: makeCommits("a", "b"), + expectedIdx: nil, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + idx := findNewConflictedCommit(testCase.previousCommits, testCase.commits) + + assert.Equal(t, testCase.expectedIdx != nil, idx != nil) + if idx != nil { + assert.Equal(t, *testCase.expectedIdx, *idx) + } + }) + } +} + func TestGetGithubBaseRemote(t *testing.T) { cases := []struct { name string @@ -252,6 +300,46 @@ func TestGetAuthenticatedGithubRemotes(t *testing.T) { }, callsByHost) } +func TestMarkWorktreeFiles(t *testing.T) { + worktreePath := filepath.Join("/", "path", "to", "repo") + worktrees := []*models.Worktree{ + {Path: worktreePath}, + {Path: filepath.Join(worktreePath, "worktree1")}, + {Path: filepath.Join(worktreePath, "dir", "worktree2")}, + {Path: filepath.Join("/", "path", "to", "worktree3")}, + } + + t.Run("marks the files that are worktrees, and takes their slash off", func(t *testing.T) { + files := []*models.File{ + {Path: "file"}, + {Path: "worktree1/"}, + {Path: "dir/worktree2/"}, + {Path: "dir/"}, + } + + assert.True(t, markWorktreeFiles(files, worktrees, worktreePath)) + assert.Equal(t, []*models.File{ + {Path: "file"}, + {Path: "worktree1", IsWorktree: true}, + {Path: "dir/worktree2", IsWorktree: true}, + {Path: "dir/"}, + }, files) + }) + + t.Run("reports no change when there is nothing to mark", func(t *testing.T) { + files := []*models.File{{Path: "file"}, {Path: "dir/"}} + + assert.False(t, markWorktreeFiles(files, worktrees, worktreePath)) + }) + + t.Run("unmarks a file whose worktree is gone", func(t *testing.T) { + files := []*models.File{{Path: "worktree1", IsWorktree: true}} + + assert.True(t, markWorktreeFiles(files, nil, worktreePath)) + assert.Equal(t, []*models.File{{Path: "worktree1"}}, files) + }) +} + func makeGithubRemoteInfoList(names ...string) []githubRemoteInfo { return lo.Map(names, func(name string, _ int) githubRemoteInfo { return makeGithubRemoteInfo(name, name) @@ -288,3 +376,7 @@ func makeTodoCommit(action todo.TodoCommand) *models.Commit { func makeTodoCommitWithHash(hash string, action todo.TodoCommand) *models.Commit { return models.NewCommit(&utils.StringPool{}, models.NewCommitOpts{Hash: hash, Action: action}) } + +func makeConflictedCommit(hash string) *models.Commit { + return models.NewCommit(&utils.StringPool{}, models.NewCommitOpts{Hash: hash, Status: models.StatusConflicted}) +} diff --git a/pkg/gui/controllers/helpers/repos_helper.go b/pkg/gui/controllers/helpers/repos_helper.go index d3b6bfe29..c8a33bcbe 100644 --- a/pkg/gui/controllers/helpers/repos_helper.go +++ b/pkg/gui/controllers/helpers/repos_helper.go @@ -153,7 +153,11 @@ func (self *ReposHelper) CreateRecentReposMenu() error { } }) - return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.RecentRepos, Items: menuItems}) + return self.c.Menu(types.CreateMenuOptions{ + Title: self.c.Tr.RecentRepos, + Items: menuItems, + FilterAsYouType: true, + }) } // SwitchToParentRepo switches back to the repo the current submodule was diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go index bc0c938f9..96c4c35ba 100644 --- a/pkg/gui/controllers/helpers/search_helper.go +++ b/pkg/gui/controllers/helpers/search_helper.go @@ -29,14 +29,14 @@ func NewSearchHelper( } } -func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) error { +func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) { state := self.searchState() state.PrevSearchIndex = -1 state.Context = context - self.searchPrefixView().SetContent(context.FilterPrefix(self.c.Tr)) + self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix) promptView := self.promptView() promptView.ClearTextArea() self.OnPromptContentChanged("") @@ -44,10 +44,10 @@ func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) err self.c.Context().Push(self.c.Contexts().Search, types.OnFocusOpts{}) - return self.c.ResetKeybindings() + self.c.ResetKeybindings() } -func (self *SearchHelper) OpenSearchPrompt(context types.ISearchableContext) error { +func (self *SearchHelper) OpenSearchPrompt(context types.ISearchableContext) { state := self.searchState() state.PrevSearchIndex = -1 @@ -61,7 +61,7 @@ func (self *SearchHelper) OpenSearchPrompt(context types.ISearchableContext) err self.c.Context().Push(self.c.Contexts().Search, types.OnFocusOpts{}) - return self.c.ResetKeybindings() + self.c.ResetKeybindings() } func (self *SearchHelper) DisplayFilterStatus(context types.IFilterableContext) { @@ -70,7 +70,7 @@ func (self *SearchHelper) DisplayFilterStatus(context types.IFilterableContext) state.Context = context searchString := context.GetFilter() - self.searchPrefixView().SetContent(context.FilterPrefix(self.c.Tr)) + self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix) promptView := self.promptView() keybindingConfig := self.c.UserConfig().Keybinding @@ -103,10 +103,11 @@ func (self *SearchHelper) promptContent() string { return self.c.Contexts().Search.GetView().TextArea.GetContent() } -func (self *SearchHelper) Confirm() error { +func (self *SearchHelper) Confirm() { state := self.searchState() if self.promptContent() == "" { - return self.CancelPrompt() + self.CancelPrompt() + return } switch state.SearchType() { @@ -118,7 +119,7 @@ func (self *SearchHelper) Confirm() error { self.c.Context().Pop() } - return self.c.ResetKeybindings() + self.c.ResetKeybindings() } func (self *SearchHelper) ConfirmFilter() { @@ -175,12 +176,12 @@ func modelSearchResults(context types.ISearchableContext) []gocui.SearchPosition return context.ModelSearchResults(normalizedSearchStr, caseSensitive) } -func (self *SearchHelper) CancelPrompt() error { +func (self *SearchHelper) CancelPrompt() { self.Cancel() self.c.Context().Pop() - return self.c.ResetKeybindings() + self.c.ResetKeybindings() } func (self *SearchHelper) ScrollHistory(scrollIncrement int) { @@ -224,10 +225,7 @@ func (self *SearchHelper) OnPromptContentChanged(searchString string) { state := self.searchState() switch context := state.Context.(type) { case types.IFilterableContext: - context.SetSelection(0) - context.GetView().SetOriginY(0) - context.SetFilter(searchString, self.c.UserConfig().Gui.UseFuzzySearch()) - self.c.PostRefreshUpdate(context) + self.ApplyFilter(context, searchString) case types.ISearchableContext: // do nothing default: @@ -235,12 +233,21 @@ func (self *SearchHelper) OnPromptContentChanged(searchString string) { } } +func (self *SearchHelper) ApplyFilter(context types.IFilterableContext, filter string) { + context.SetSelection(0) + context.SetFilter(filter, self.c.UserConfig().Gui.UseFuzzySearch()) + self.c.PostRefreshUpdate(context) +} + func (self *SearchHelper) ReApplyFilter(context types.Context) { filterableContext, ok := context.(types.IFilterableContext) if ok { state := self.searchState() if context == state.Context && self.c.Context().Current().GetKey() == self.c.Contexts().Search.GetKey() { filterableContext.SetSelection(0) + // This runs as part of a refresh, and a refresh that no user action + // is behind keeps the scroll position, which would leave the view + // scrolled somewhere the filtered list no longer has anything at. filterableContext.GetView().SetOriginY(0) } filterableContext.ReApplyFilter(self.c.UserConfig().Gui.UseFuzzySearch()) diff --git a/pkg/gui/controllers/helpers/sub_commits_helper.go b/pkg/gui/controllers/helpers/sub_commits_helper.go index 7bd928826..09f32d1a9 100644 --- a/pkg/gui/controllers/helpers/sub_commits_helper.go +++ b/pkg/gui/controllers/helpers/sub_commits_helper.go @@ -66,7 +66,6 @@ func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error { subCommitsContext.GetView().TitlePrefix = opts.Context.GetView().TitlePrefix self.c.PostRefreshUpdate(self.c.Contexts().SubCommits) - subCommitsContext.FocusLine(true) self.c.Context().Push(self.c.Contexts().SubCommits, types.OnFocusOpts{}) return nil diff --git a/pkg/gui/controllers/helpers/window_arrangement_helper.go b/pkg/gui/controllers/helpers/window_arrangement_helper.go index 90a651809..5379e9c09 100644 --- a/pkg/gui/controllers/helpers/window_arrangement_helper.go +++ b/pkg/gui/controllers/helpers/window_arrangement_helper.go @@ -87,8 +87,8 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, repoState := self.c.State().GetRepoState() var searchPrefix string - if filterableContext, ok := repoState.GetSearchState().Context.(types.IFilterableContext); ok { - searchPrefix = filterableContext.FilterPrefix(self.c.Tr) + if _, ok := repoState.GetSearchState().Context.(types.IFilterableContext); ok { + searchPrefix = self.c.Tr.FilterPrefix } else { searchPrefix = self.c.Tr.SearchPrefix } diff --git a/pkg/gui/controllers/list_controller.go b/pkg/gui/controllers/list_controller.go index 8136c6aa9..c073e5141 100644 --- a/pkg/gui/controllers/list_controller.go +++ b/pkg/gui/controllers/list_controller.go @@ -136,7 +136,7 @@ func (self *ListController) handleLineChangeAux(f func(int), change int) error { self.context.SetNeedRerenderVisibleLines() } - self.context.HandleFocus(types.OnFocusOpts{ScrollSelectionIntoView: true}) + self.context.HandleFocus(types.OnFocusOpts{}) } else { // If the selection did not change (because, for example, we are at the top of the list and // press up), we still want to ensure that the selection is visible. This is useful after @@ -205,9 +205,10 @@ func (self *ListController) handlePageChange(delta int) error { // must tell it explicitly to rerender. self.context.SetNeedRerenderVisibleLines() - // Since we are maintaining the scroll position ourselves above, there's no point in passing - // ScrollSelectionIntoView=true here. - self.context.HandleFocus(types.OnFocusOpts{}) + // This function scrolls the view itself, keeping the selection at the edge of + // the viewport rather than in its middle, so the scroll position is ours to + // maintain, not the focus mechanism's. + self.context.HandleFocus(types.OnFocusOpts{KeepScrollPosition: true}) return nil } @@ -280,7 +281,10 @@ func (self *ListController) selectRangeThroughViewIndex(viewIndex int) { newSelectedLineIdx := self.context.ViewIndexToModelIndex(viewIndex) list.ExpandNonStickyRange(newSelectedLineIdx - list.GetSelectedLineIdx()) - self.context.HandleFocus(types.OnFocusOpts{}) + // The pointer can be outside the viewport, in which case so is the end of + // the range; the drag autoscroller takes care of following it, one line at a + // time, for as long as the pointer stays there. + self.context.HandleFocus(types.OnFocusOpts{KeepScrollPosition: true}) } func (self *ListController) handleDragAutoscroll(viewIndex int) bool { diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 642263d12..1e1a01427 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -191,7 +191,8 @@ func (self *LocalCommitsController) handleCommitDrag(opts gocui.ViewMouseBinding self.commitDrag.hasMoved = true if self.updateCommitDragInsertion(opts.Y) { - self.c.PostRefreshUpdate(self.context()) + self.c.PostRefreshUpdateWithOptions(self.context(), + types.OnFocusOpts{KeepScrollPosition: true}) } originY := self.context().GetView().OriginY() self.dragAutoscroller.Update(opts.Y - originY) @@ -344,7 +345,9 @@ func (self *LocalCommitsController) startMovingCommitsIndicator(insertionIndex i func (self *LocalCommitsController) stopMovingCommitsIndicator() { self.stopMovingCommitsIndicatorTicker() self.context().ClearDropInsertionIndex() - self.c.PostRefreshUpdate(self.context()) + self.c.PostRefreshUpdateWithOptions( + self.context(), types.OnFocusOpts{SkipMainViewUpdate: true}, + ) } func (self *LocalCommitsController) stopMovingCommitsIndicatorTicker() { @@ -1170,16 +1173,21 @@ func (self *LocalCommitsController) move( if err := self.c.Git().Rebase.MoveTodos(selectedCommits, offset); err != nil { return err } - self.context().MoveSelection(offset) - self.context().HandleFocus(types.OnFocusOpts{ScrollSelectionIntoView: true}) // Block input until the refresh has landed: a quick second press must // read the moved todo from the refreshed model, not grab whatever the // advanced selection index points at in the stale one. self.c.RefreshBlockingInput(types.RefreshOptions{ - Scope: []types.RefreshableView{types.REBASE_COMMITS}, - CommitSelection: types.KeepCommitSelectionIndex, - Then: onComplete, + Scope: []types.RefreshableView{types.REBASE_COMMITS}, + SkipMainViewUpdate: true, + Then: func() error { + self.context().MoveSelection(offset) + self.context().FocusLine(true) + if onComplete != nil { + return onComplete() + } + return nil + }, }) return nil } @@ -1204,7 +1212,7 @@ func (self *LocalCommitsController) move( Then: func() error { if err == nil { self.context().MoveSelection(offset) - self.context().HandleFocus(types.OnFocusOpts{ScrollSelectionIntoView: true}) + self.context().HandleFocus(types.OnFocusOpts{}) } if onComplete != nil { return onComplete() @@ -1676,7 +1684,8 @@ func (self *LocalCommitsController) openSearch() error { self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}) } - return self.c.Helpers().Search.OpenSearchPrompt(self.context()) + self.c.Helpers().Search.OpenSearchPrompt(self.context()) + return nil } func (self *LocalCommitsController) handleOpenLogMenu() error { diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index 6eb6c86e3..5bde8c5ff 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -109,7 +109,8 @@ func (self *MainViewController) openSearch() error { if manager := self.c.GetViewBufferManagerForView(self.context.GetView()); manager != nil { manager.ReadToEnd(func() { self.c.OnUIThread(func() error { - return self.c.Helpers().Search.OpenSearchPrompt(self.context) + self.c.Helpers().Search.OpenSearchPrompt(self.context) + return nil }) }) } diff --git a/pkg/gui/controllers/menu_controller.go b/pkg/gui/controllers/menu_controller.go index 283c2bbdf..1bbb7b27f 100644 --- a/pkg/gui/controllers/menu_controller.go +++ b/pkg/gui/controllers/menu_controller.go @@ -1,20 +1,26 @@ package controllers import ( + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) type MenuController struct { baseController *ListControllerTrait[*types.MenuItem] c *ControllerCommon + // for delegating navigation to, see physicalKeyBindings + listController *ListController } var _ types.IController = &MenuController{} func NewMenuController( c *ControllerCommon, + listController *ListController, ) *MenuController { return &MenuController{ baseController: baseController{}, @@ -24,12 +30,13 @@ func NewMenuController( c.Contexts().Menu.GetSelected, c.Contexts().Menu.GetSelectedItems, ), - c: c, + c: c, + listController: listController, } } // NOTE: if you add a new keybinding here, you'll also need to add it to -// `reservedKeys` in `pkg/gui/context/menu_context.go` +// `essentialKeys` in `pkg/gui/menu_panel.go`, so that menu items can't shadow it func (self *MenuController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { @@ -52,6 +59,51 @@ func (self *MenuController) GetKeybindings(opts types.KeybindingsOpts) []*types. }, } + if self.context().FilterAsYouType() { + bindings = append(bindings, self.physicalKeyBindings(opts)...) + } + + return bindings +} + +// In a menu that filters as you type, the keys configured for driving the menu +// may all be printable, and printable keys become filter text once the user +// starts typing. These keys can't, so binding them on top guarantees that the +// menu stays usable no matter how the keybindings are configured. +func (self *MenuController) physicalKeyBindings(opts types.KeybindingsOpts) []*types.Binding { + candidates := []struct { + key gocui.Key + configured config.Keybinding + binding *types.Binding + }{ + { + key: gocui.NewKeyName(gocui.KeyEnter), + configured: opts.Config.Universal.ConfirmMenu, + binding: &types.Binding{ + Handler: self.withItem(self.press), + GetDisabledReason: self.require(self.singleItemSelected()), + }, + }, + {gocui.NewKeyName(gocui.KeyEsc), opts.Config.Universal.Return, &types.Binding{Handler: self.close}}, + {gocui.NewKeyName(gocui.KeyArrowUp), opts.Config.Universal.PrevItem, &types.Binding{Handler: self.listController.HandlePrevLine}}, + {gocui.NewKeyName(gocui.KeyArrowDown), opts.Config.Universal.NextItem, &types.Binding{Handler: self.listController.HandleNextLine}}, + {gocui.NewKeyName(gocui.KeyPgup), opts.Config.Universal.PrevPage, &types.Binding{Handler: self.listController.HandlePrevPage}}, + {gocui.NewKeyName(gocui.KeyPgdn), opts.Config.Universal.NextPage, &types.Binding{Handler: self.listController.HandleNextPage}}, + {gocui.NewKeyName(gocui.KeyHome), opts.Config.Universal.GotoTop, &types.Binding{Handler: self.listController.HandleGotoTop}}, + {gocui.NewKeyName(gocui.KeyEnd), opts.Config.Universal.GotoBottom, &types.Binding{Handler: self.listController.HandleGotoBottom}}, + } + + bindings := []*types.Binding{} + for _, candidate := range candidates { + if lo.Contains(opts.GetKeys(candidate.configured), candidate.key) { + // this key is the configured one, so it drives the menu already + continue + } + + candidate.binding.Keys = []gocui.Key{candidate.key} + bindings = append(bindings, candidate.binding) + } + return bindings } @@ -73,6 +125,11 @@ func (self *MenuController) press(selectedItem *types.MenuItem) error { } func (self *MenuController) close() error { + if self.context().FilterStarted() { + self.stopFiltering() + return nil + } + if self.context().IsFiltering() { self.c.Helpers().Search.Cancel() return nil @@ -81,6 +138,17 @@ func (self *MenuController) close() error { return self.context().OnMenuPress(nil) } +// Hides the filter row again and puts the menu back the way it was, keeping the +// item that was selected. It takes another escape to close the menu. +func (self *MenuController) stopFiltering() { + self.c.Views().MenuFilter.ClearTextArea() + self.c.Views().MenuFilter.RenderTextArea() + + self.context().SetFilterStarted(false) + self.context().ClearFilter() + self.c.PostRefreshUpdate(self.context()) +} + func (self *MenuController) context() *context.MenuContext { return self.c.Contexts().Menu } diff --git a/pkg/gui/controllers/options_menu_action.go b/pkg/gui/controllers/options_menu_action.go index c92fdd589..be2899632 100644 --- a/pkg/gui/controllers/options_menu_action.go +++ b/pkg/gui/controllers/options_menu_action.go @@ -64,6 +64,7 @@ func (self *OptionsMenuAction) Call() error { ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft}, AllowFilteringKeybindings: true, KeepConflictingKeybindings: true, + FilterAsYouType: true, }) } diff --git a/pkg/gui/controllers/patch_building_controller.go b/pkg/gui/controllers/patch_building_controller.go index f3e26e303..e1405463a 100644 --- a/pkg/gui/controllers/patch_building_controller.go +++ b/pkg/gui/controllers/patch_building_controller.go @@ -230,9 +230,8 @@ func (self *PatchBuildingController) discardSelectionFromCommit() error { err := self.c.Git().Patch.DeletePatchesFromCommit(commits, commitIndex) // Escape pops the patch-building context, so run it on the UI thread // before the refresh below. - _ = self.c.GocuiGui().OnUIThreadAndWait(func() error { + _ = self.c.GocuiGui().OnUIThreadAndWait(func() { self.c.Helpers().PatchBuilding.Escape() - return nil }) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions( err, types.RefreshOptions{}) diff --git a/pkg/gui/controllers/search_controller.go b/pkg/gui/controllers/search_controller.go index f1d5efe2a..f84539646 100644 --- a/pkg/gui/controllers/search_controller.go +++ b/pkg/gui/controllers/search_controller.go @@ -37,12 +37,13 @@ func (self *SearchController) GetKeybindings(opts types.KeybindingsOpts) []*type return []*types.Binding{ { Keys: opts.GetKeys(opts.Config.Universal.StartSearch), - Handler: self.OpenSearchPrompt, + Handler: self.openSearchPrompt, Description: self.c.Tr.StartSearch, }, } } -func (self *SearchController) OpenSearchPrompt() error { - return self.c.Helpers().Search.OpenSearchPrompt(self.context) +func (self *SearchController) openSearchPrompt() error { + self.c.Helpers().Search.OpenSearchPrompt(self.context) + return nil } diff --git a/pkg/gui/controllers/search_prompt_controller.go b/pkg/gui/controllers/search_prompt_controller.go index 1ce02abf0..4e6de85dc 100644 --- a/pkg/gui/controllers/search_prompt_controller.go +++ b/pkg/gui/controllers/search_prompt_controller.go @@ -51,11 +51,13 @@ func (self *SearchPromptController) context() types.Context { } func (self *SearchPromptController) confirm() error { - return self.c.Helpers().Search.Confirm() + self.c.Helpers().Search.Confirm() + return nil } func (self *SearchPromptController) cancel() error { - return self.c.Helpers().Search.CancelPrompt() + self.c.Helpers().Search.CancelPrompt() + return nil } func (self *SearchPromptController) prevHistory() error { diff --git a/pkg/gui/controllers/stash_controller.go b/pkg/gui/controllers/stash_controller.go index 03011e421..7730587f4 100644 --- a/pkg/gui/controllers/stash_controller.go +++ b/pkg/gui/controllers/stash_controller.go @@ -251,7 +251,6 @@ func (self *StashController) handleRenameStashEntry(stashEntry *models.StashEntr return err } self.context().SetSelection(0) // Select the renamed stash - self.context().FocusLine(true) // Renaming re-creates the stash at the top, shifting the other // entries' indices; block input so that a quick next action sees // the refreshed list rather than the stale indices. diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go index a2dd22ed3..82ca509ca 100644 --- a/pkg/gui/controllers/submodules_controller.go +++ b/pkg/gui/controllers/submodules_controller.go @@ -123,7 +123,7 @@ func (self *SubmodulesController) GetOnRenderToMain() func() { if file == nil { task = types.NewRenderStringTask(prefix) } else { - cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, nil) + cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, file.Names()) task = types.NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix) } } diff --git a/pkg/gui/controllers/suggestions_controller.go b/pkg/gui/controllers/suggestions_controller.go index 0553050e5..18ee594b2 100644 --- a/pkg/gui/controllers/suggestions_controller.go +++ b/pkg/gui/controllers/suggestions_controller.go @@ -85,7 +85,6 @@ func (self *SuggestionsController) GetMouseKeybindings(opts types.KeybindingsOpt func (self *SuggestionsController) switchToPrompt() error { self.c.Views().Suggestions.Subtitle = "" - self.c.Views().Suggestions.Highlight = false self.c.Context().Replace(self.c.Contexts().Prompt) return nil } diff --git a/pkg/gui/controllers/toggle_whitespace_action.go b/pkg/gui/controllers/toggle_whitespace_action.go index a1ac0c8da..67bb59d86 100644 --- a/pkg/gui/controllers/toggle_whitespace_action.go +++ b/pkg/gui/controllers/toggle_whitespace_action.go @@ -27,6 +27,6 @@ func (self *ToggleWhitespaceAction) Call() error { self.c.UserConfig().Git.IgnoreWhitespaceInDiffView = !self.c.UserConfig().Git.IgnoreWhitespaceInDiffView - self.c.Context().CurrentSide().HandleFocus(types.OnFocusOpts{}) + self.c.Context().CurrentSide().HandleRenderToMain() return nil } diff --git a/pkg/gui/editors.go b/pkg/gui/editors.go index 37eacf416..7c658265b 100644 --- a/pkg/gui/editors.go +++ b/pkg/gui/editors.go @@ -49,6 +49,32 @@ func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key) bool { return matched } +func (gui *Gui) menuFilterEditor(v *gocui.View, key gocui.Key) bool { + contentBefore := v.TextArea.GetContent() + + matched := gui.handleEditorKeypress(v, key, false) + if !matched { + // Give the global keybindings a chance at the key, e.g. so that ctrl-c + // still quits while a menu is open. + return false + } + + v.RenderTextArea() + + content := v.TextArea.GetContent() + if content == contentBefore { + // The key just moved the cursor around within the filter; refiltering would + // throw away the menu's selection for nothing. + return true + } + + menuContext := gui.State.Contexts.Menu + menuContext.SetFilterStarted(true) + gui.helpers.Search.ApplyFilter(menuContext, content) + + return true +} + func (gui *Gui) searchEditor(v *gocui.View, key gocui.Key) bool { matched := gui.handleEditorKeypress(v, key, false) v.RenderTextArea() diff --git a/pkg/gui/filetree/commit_file_tree_view_model.go b/pkg/gui/filetree/commit_file_tree_view_model.go index a58f7d93e..a59bcb01a 100644 --- a/pkg/gui/filetree/commit_file_tree_view_model.go +++ b/pkg/gui/filetree/commit_file_tree_view_model.go @@ -7,7 +7,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/gui/context/traits" "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -263,10 +262,6 @@ func (self *CommitFileTreeViewModel) IsFiltering() bool { // used for type switch func (self *CommitFileTreeViewModel) IsFilterableContext() {} -func (self *CommitFileTreeViewModel) FilterPrefix(tr *i18n.TranslationSet) string { - return tr.FilterPrefix -} - func (self *CommitFileTreeViewModel) GetSearchHistory() *utils.HistoryBuffer[string] { return self.searchHistory } diff --git a/pkg/gui/filetree/file_tree.go b/pkg/gui/filetree/file_tree.go index 2d3cec514..6c8eff72e 100644 --- a/pkg/gui/filetree/file_tree.go +++ b/pkg/gui/filetree/file_tree.go @@ -3,6 +3,7 @@ package filetree import ( "fmt" + "github.com/jesseduffield/generics/set" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -42,6 +43,7 @@ type IFileTree interface { FilterFiles(test func(*models.File) bool) []*models.File SetStatusFilter(filter FileTreeDisplayFilter) + RememberConflictedPaths(paths []string) ForceShowUntracked() bool Get(index int) *FileNode GetFile(path string) *models.File @@ -54,25 +56,31 @@ type IFileTree interface { } type FileTree struct { - getFiles func() []*models.File - tree *Node[models.File] - showTree bool - common *common.Common - filter FileTreeDisplayFilter - collapsedPaths *CollapsedPaths - textFilter string - useFuzzySearch bool + getFiles func() []*models.File + tree *Node[models.File] + showTree bool + common *common.Common + filter FileTreeDisplayFilter + // Paths of the files that had conflicts while the current filter has been + // active. The DisplayConflicted filter keeps showing them after their + // conflicts have been resolved, so that their diffs can be reviewed while + // the remaining files are still being worked on. + conflictedPaths *set.Set[string] + collapsedPaths *CollapsedPaths + textFilter string + useFuzzySearch bool } var _ IFileTree = &FileTree{} func NewFileTree(getFiles func() []*models.File, common *common.Common, showTree bool) *FileTree { return &FileTree{ - getFiles: getFiles, - common: common, - showTree: showTree, - filter: DisplayAll, - collapsedPaths: NewCollapsedPaths(), + getFiles: getFiles, + common: common, + showTree: showTree, + filter: DisplayAll, + conflictedPaths: set.New[string](), + collapsedPaths: NewCollapsedPaths(), } } @@ -100,7 +108,9 @@ func (self *FileTree) getFilesForDisplay() []*models.File { case DisplayUntracked: files = self.FilterFiles(func(file *models.File) bool { return !(file.Tracked || file.HasStagedChanges) }) case DisplayConflicted: - files = self.FilterFiles(func(file *models.File) bool { return file.HasMergeConflicts }) + files = self.FilterFiles(func(file *models.File) bool { + return file.HasMergeConflicts || self.conflictedPaths.Includes(file.Path) + }) default: panic(fmt.Sprintf("Unexpected files display filter: %d", self.filter)) } @@ -122,9 +132,16 @@ func (self *FileTree) FilterFiles(test func(*models.File) bool) []*models.File { func (self *FileTree) SetStatusFilter(filter FileTreeDisplayFilter) { self.filter = filter + self.conflictedPaths = set.New[string]() self.SetTree() } +// RememberConflictedPaths records which files have conflicts right now, so that +// the DisplayConflicted filter keeps showing them once they are resolved. +func (self *FileTree) RememberConflictedPaths(paths []string) { + self.conflictedPaths.Add(paths...) +} + func (self *FileTree) ToggleShowTree() { self.showTree = !self.showTree self.SetTree() diff --git a/pkg/gui/filetree/file_tree_test.go b/pkg/gui/filetree/file_tree_test.go index 1c7960a6e..3058e8db9 100644 --- a/pkg/gui/filetree/file_tree_test.go +++ b/pkg/gui/filetree/file_tree_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/jesseduffield/generics/set" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/config" @@ -12,10 +13,11 @@ import ( func TestFilterAction(t *testing.T) { scenarios := []struct { - name string - filter FileTreeDisplayFilter - files []*models.File - expected []*models.File + name string + filter FileTreeDisplayFilter + conflictedPaths []string + files []*models.File + expected []*models.File }{ { name: "filter files with unstaged changes", @@ -84,11 +86,29 @@ func TestFilterAction(t *testing.T) { {Path: "file1", ShortStatus: "UU", HasMergeConflicts: true, HasInlineMergeConflicts: true}, }, }, + { + name: "keep showing conflicted files whose conflicts have been resolved", + filter: DisplayConflicted, + conflictedPaths: []string{"dir2/dir2/file4", "file1"}, + files: []*models.File{ + {Path: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true}, + {Path: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true}, + {Path: "file1", ShortStatus: "UU", HasMergeConflicts: true, HasInlineMergeConflicts: true}, + }, + expected: []*models.File{ + {Path: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true}, + {Path: "file1", ShortStatus: "UU", HasMergeConflicts: true, HasInlineMergeConflicts: true}, + }, + }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - mngr := &FileTree{getFiles: func() []*models.File { return s.files }, filter: s.filter} + mngr := &FileTree{ + getFiles: func() []*models.File { return s.files }, + filter: s.filter, + conflictedPaths: set.NewFromSlice(s.conflictedPaths), + } result := mngr.getFilesForDisplay() assert.EqualValues(t, s.expected, result) }) diff --git a/pkg/gui/filetree/file_tree_view_model.go b/pkg/gui/filetree/file_tree_view_model.go index aabbbce7f..68829b444 100644 --- a/pkg/gui/filetree/file_tree_view_model.go +++ b/pkg/gui/filetree/file_tree_view_model.go @@ -7,7 +7,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/gui/context/traits" "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -167,6 +166,31 @@ func (self *FileTreeViewModel) SetStatusFilter(filter FileTreeDisplayFilter) { self.IListCursor.SetSelection(0) } +func (self *FileTreeViewModel) SetStatusFilterPreservingSelection(filter FileTreeDisplayFilter) { + self.preserveSelection(func() { + self.SetStatusFilter(filter) + }) +} + +func (self *FileTreeViewModel) preserveSelection(f func()) { + selectedNode := self.GetSelected() + var selectedPath string + if selectedNode != nil { + selectedPath = selectedNode.GetInternalPath() + } + + f() + + if selectedPath != "" { + self.ExpandToPath(selectedPath) + if idx, found := self.GetIndexForPath(selectedPath); found { + self.SetSelection(idx) + return + } + } + self.ClampSelection() +} + // If we're going from flat to tree we want to select the same file. // If we're going from tree to flat and we have a file selected we want to select that. // If instead we've selected a directory we need to select the first file in that directory. @@ -233,22 +257,9 @@ func (self *FileTreeViewModel) GetFilter() string { } func (self *FileTreeViewModel) ClearFilter() { - selectedNode := self.GetSelected() - var selectedPath string - if selectedNode != nil { - selectedPath = selectedNode.GetInternalPath() - } - - self.IFileTree.SetTextFilter("", false) - - if selectedPath != "" { - self.ExpandToPath(selectedPath) - if idx, found := self.GetIndexForPath(selectedPath); found { - self.SetSelection(idx) - return - } - } - self.ClampSelection() + self.preserveSelection(func() { + self.IFileTree.SetTextFilter("", false) + }) } func (self *FileTreeViewModel) ReApplyFilter(useFuzzySearch bool) { @@ -262,10 +273,6 @@ func (self *FileTreeViewModel) IsFiltering() bool { // used for type switch func (self *FileTreeViewModel) IsFilterableContext() {} -func (self *FileTreeViewModel) FilterPrefix(tr *i18n.TranslationSet) string { - return tr.FilterPrefix -} - func (self *FileTreeViewModel) GetSearchHistory() *utils.HistoryBuffer[string] { return self.searchHistory } diff --git a/pkg/gui/filetree/file_tree_view_model_test.go b/pkg/gui/filetree/file_tree_view_model_test.go new file mode 100644 index 000000000..c14c91ea8 --- /dev/null +++ b/pkg/gui/filetree/file_tree_view_model_test.go @@ -0,0 +1,32 @@ +package filetree + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/common" + "github.com/stretchr/testify/assert" +) + +func TestSetStatusFilterPreservingSelection(t *testing.T) { + files := []*models.File{ + {Path: "file1"}, + {Path: "file2", HasMergeConflicts: true}, + {Path: "file3", HasMergeConflicts: true}, + } + viewModel := NewFileTreeViewModel( + func() []*models.File { return files }, + common.NewDummyCommon(), + false, + ) + viewModel.SetTree() + viewModel.SetStatusFilter(DisplayConflicted) + viewModel.SetSelection(viewModel.Len() - 2) + viewModel.ToggleStickyRange() + viewModel.MoveSelectedLine(1) + + viewModel.SetStatusFilterPreservingSelection(DisplayAll) + + assert.Equal(t, "file3", viewModel.GetSelectedPath()) + assert.False(t, viewModel.IsSelectingRange()) +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index bde383caf..801fe14d3 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -368,10 +368,7 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context contextToPush := gui.resetState(startArgs) gui.resetHelpersAndControllers() - - if err := gui.resetKeybindings(); err != nil { - return err - } + gui.resetKeybindings() gui.g.SetFocusHandler(func(Focused bool) error { if Focused { @@ -383,9 +380,7 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context gui.c.Log.Info("User config changed - reloading") reloadErr = gui.onUserConfigLoaded() gui.reloadSidePanels() - if err := gui.resetKeybindings(); err != nil { - return err - } + gui.resetKeybindings() if err := gui.checkForChangedConfigsThatDontAutoReload(oldConfig, gui.Config.GetUserConfig()); err != nil { return err @@ -603,13 +598,6 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context { // RefreshHelper.onUIThreadUnlessRepoChanged). gui.repoGeneration.Add(1) - // Un-highlight the current view if there is one. The reason we do this is - // that the repo we are switching to might have a different view focused, - // and would then show an inactive highlight for the previous view. - if oldCurrentView := gui.g.CurrentView(); oldCurrentView != nil { - oldCurrentView.Highlight = false - } - worktreePath := gui.git.RepoPaths.WorktreePath() if state := gui.RepoStateMap[Repo(worktreePath)]; state != nil { @@ -927,6 +915,21 @@ func (gui *Gui) viewTabMap() map[string][]context.TabView { return result } +// The views that each popup panel is made up of. A panel's views share the +// keyboard focus, so clicking from one of them to another stays within the +// panel. +var popupPanelViewGroups = [][]string{ + {"commitMessage", "commitDescription"}, + {"prompt", "suggestions"}, + {"menu", "menuFilterFrame", "menuFilter"}, +} + +func viewsBelongToSamePopupPanel(viewName string, otherViewName string) bool { + return lo.SomeBy(popupPanelViewGroups, func(group []string) bool { + return lo.Contains(group, viewName) && lo.Contains(group, otherViewName) + }) +} + // Run: setup the gui with keybindings and start the mainloop func (gui *Gui) Run(startArgs appTypes.StartArgs) error { g, err := gui.initGocui(Headless(), startArgs.IntegrationTest) @@ -942,15 +945,11 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error { gui.g.ShouldHandleMouseEvent = func(view *gocui.View, key gocui.KeyName) bool { if gui.helpers.Confirmation.IsPopupPanelFocused() && gui.currentViewName() != view.Name() && !gocui.IsMouseScrollKey(key) { - // we ignore click events on views that aren't popup panels, when a popup panel is focused. - // Unless both the current view and the clicked-on view are either commit message or commit - // description, or a prompt and the suggestions view, because we want to allow switching - // between those two views by clicking. - isCommitMessageOrSuggestionsView := func(viewName string) bool { - return viewName == "commitMessage" || viewName == "commitDescription" || - viewName == "prompt" || viewName == "suggestions" - } - if !isCommitMessageOrSuggestionsView(gui.currentViewName()) || !isCommitMessageOrSuggestionsView(view.Name()) { + // we ignore click events on views that aren't popup panels, when a popup + // panel is focused. Unless the clicked-on view is part of the same popup + // panel as the current one, because we want to allow switching between the + // views of a panel by clicking. + if !viewsBelongToSamePopupPanel(gui.currentViewName(), view.Name()) { return false } } diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index e7b14ba04..d693fd77f 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -39,7 +39,11 @@ func (self *guiCommon) RefreshFromWorker(opts types.RefreshOptions) { } func (self *guiCommon) PostRefreshUpdate(context types.Context) { - self.gui.postRefreshUpdate(context) + self.gui.postRefreshUpdate(context, types.OnFocusOpts{}) +} + +func (self *guiCommon) PostRefreshUpdateWithOptions(context types.Context, opts types.OnFocusOpts) { + self.gui.postRefreshUpdate(context, opts) } func (self *guiCommon) RunSubprocessAndRefresh(cmdObj *oscommands.CmdObj) error { @@ -197,8 +201,8 @@ func (self *guiCommon) CallKeybindingHandler(binding *types.Binding) error { return self.gui.callKeybindingHandler(binding) } -func (self *guiCommon) ResetKeybindings() error { - return self.gui.resetKeybindings() +func (self *guiCommon) ResetKeybindings() { + self.gui.resetKeybindings() } func (self *guiCommon) IsAnyModeActive() bool { diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index 25bfcf2d5..07a0b7d4c 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -69,6 +69,10 @@ func (self *GuiDriver) MouseMove(x, y int) { self.replayMouseEvent(x, y, tcell.ButtonPrimary) } +func (self *GuiDriver) ScrollWheelDown(x, y int) { + self.replayMouseEvent(x, y, tcell.WheelDown) +} + func (self *GuiDriver) MouseRelease(x, y int) { self.replayMouseEvent(x, y, tcell.ButtonNone) } @@ -82,7 +86,7 @@ func (self *GuiDriver) WaitUntilIdle() { } func (self *GuiDriver) OnUIThreadAndWait(f func()) { - _ = self.gui.g.OnUIThreadAndWait(func() error { f(); return nil }) + _ = self.gui.g.OnUIThreadAndWait(f) } func (self *GuiDriver) replayMouseEvent(x, y int, buttons tcell.ButtonMask) { @@ -97,14 +101,25 @@ func (self *GuiDriver) replayMouseEventWithoutWaiting(x, y int, buttons tcell.Bu )) } -// FocusIn simulates the terminal window regaining focus, which is how lazygit -// learns to reload changed config files. Tests use it to exercise the live -// config-reload path. -func (self *GuiDriver) FocusIn() { +// replayFocusIn takes the focus away before handing it back, because that's the +// only way a terminal can report regaining it, and lazygit only reacts to focus +// reports that change the focus (see gocui.Gui.IsFocused). +func (self *GuiDriver) replayFocusIn() { + self.gui.g.ReplayFocusEvent(gocui.NewTcellFocusEventWrapper( + tcell.NewEventFocus(false), + 0, + )) self.gui.g.ReplayFocusEvent(gocui.NewTcellFocusEventWrapper( tcell.NewEventFocus(true), 0, )) +} + +// FocusIn simulates the terminal window regaining focus, which is how lazygit +// learns to reload changed config files. Tests use it to exercise the live +// config-reload path. +func (self *GuiDriver) FocusIn() { + self.replayFocusIn() self.waitTillIdle() } @@ -112,10 +127,7 @@ func (self *GuiDriver) FocusIn() { func (self *GuiDriver) FocusInAndClick(x, y int) { self.CheckAllToastsAcknowledged() - self.gui.g.ReplayFocusEvent(gocui.NewTcellFocusEventWrapper( - tcell.NewEventFocus(true), - 0, - )) + self.replayFocusIn() self.gui.g.ReplayMouseEvent(gocui.NewTcellMouseEventWrapper( tcell.NewEventMouse(x, y, tcell.ButtonPrimary, 0), 0, @@ -128,6 +140,16 @@ func (self *GuiDriver) FocusInAndClick(x, y int) { self.waitTillIdle() } +// RefreshInBackground performs the refresh that the background routines perform +// on a timer (see BackgroundRoutineMgr). Tests drive it directly rather than +// turning those routines on, so that they neither wait for a timer nor depend on +// one firing at a particular moment. +func (self *GuiDriver) RefreshInBackground() { + self.gui.c.RefreshFromWorker(types.RefreshOptions{Background: true}) + + self.waitTillIdle() +} + func (self *GuiDriver) PretendMergeOrRebaseStartedInLazygit() { self.gui.onUIThread(func() error { self.gui.State.SetMergeOrRebaseStartedInLazygit(true) @@ -159,6 +181,10 @@ func (self *GuiDriver) CurrentContext() types.Context { return self.gui.State.ContextMgr.Current() } +func (self *GuiDriver) CursorVisible() bool { + return self.gui.g.Cursor +} + func (self *GuiDriver) ContextForView(viewName string) types.Context { context, ok := self.gui.helpers.View.ContextForView(viewName) if !ok { diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 22d76f01b..5d03f6ba5 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -295,8 +295,9 @@ func (gui *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBin }, } - mouseKeybindings := []*gocui.ViewMouseBinding{} - for _, c := range gui.State.Contexts.Flatten() { + contexts := gui.State.Contexts.Flatten() + mouseKeybindings := make([]*gocui.ViewMouseBinding, 0, len(contexts)) + for _, c := range contexts { viewName := c.GetViewName() for _, binding := range c.GetKeybindings(opts) { // TODO: move all mouse keybindings into the mouse keybindings approach below @@ -350,7 +351,7 @@ func (gui *Gui) GetInitialKeybindingsWithCustomCommands() ([]*types.Binding, []* return bindings, mouseBindings } -func (gui *Gui) resetKeybindings() error { +func (gui *Gui) resetKeybindings() { gui.g.DeleteAllKeybindings() bindings, mouseBindings := gui.GetInitialKeybindingsWithCustomCommands() @@ -360,9 +361,7 @@ func (gui *Gui) resetKeybindings() error { } for _, binding := range mouseBindings { - if err := gui.SetMouseKeybinding(binding); err != nil { - return err - } + gui.SetMouseKeybinding(binding) } for _, values := range gui.viewTabMap() { @@ -372,13 +371,9 @@ func (gui *Gui) resetKeybindings() error { return gui.onViewTabClick(gui.helpers.Window.WindowForView(viewName), tabIndex) } - if err := gui.g.SetTabClickBinding(viewName, tabClickCallback); err != nil { - return err - } + gui.g.SetTabClickBinding(viewName, tabClickCallback) } } - - return nil } func (gui *Gui) SetKeybinding(binding *types.Binding) { @@ -391,8 +386,8 @@ func (gui *Gui) SetKeybinding(binding *types.Binding) { } } -func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error { - return gui.g.SetViewClickBinding(binding) +func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) { + gui.g.SetViewClickBinding(binding) } func (gui *Gui) callKeybindingHandler(binding *types.Binding) error { diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index bcdc0edfc..ccc83b1e9 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -88,7 +88,13 @@ func (gui *Gui) layout(g *gocui.Gui) error { if !view.CanScrollPastBottom { maxOriginY -= newHeight - 1 } - if oldOriginY := view.OriginY(); oldOriginY > maxOriginY { + // Don't scroll up while the view's content is still being loaded: its + // height only reflects what has been read so far, so clamping to it now + // would yank the view to the top even though more content is on the way + // (e.g. when re-rendering a diff the user was scrolled into). + manager := gui.getViewBufferManagerForView(view) + stillLoading := manager != nil && manager.IsLoading() + if oldOriginY := view.OriginY(); oldOriginY > maxOriginY && !stillLoading { view.ScrollUp(oldOriginY - maxOriginY) // the view might not have scrolled actually (if it was at the limit // already), so we need to check if it did @@ -138,15 +144,25 @@ func (gui *Gui) layout(g *gocui.Gui) error { } } - // When the screen is too short the side panels are squashed, with the - // unfocused ones taking one row each and the focused one taking the rest. The - // more panels there are, the more rows the unfocused ones reserve, so the - // floor below which there's no room left for the focused panel grows with the - // panel count. Keep the historical floor of 9 for the default five panels. - minimumHeight := max(9, len(gui.helpers.Window.SideWindows())+4) + menuWithFilterRowVisible := gui.Views.Menu.Visible && gui.State.Contexts.Menu.FilterAsYouType() + minimumHeight := minimumScreenHeight(len(gui.helpers.Window.SideWindows()), menuWithFilterRowVisible) minimumWidth := 10 gui.Views.Limit.Visible = height < minimumHeight || width < minimumWidth + filterRowVisible := gui.Views.Menu.Visible && gui.State.Contexts.Menu.FilterStarted() + gui.Views.MenuFilterFrame.Visible = filterRowVisible + gui.Views.MenuFilter.Visible = filterRowVisible + if gui.Views.Menu.Visible { + // Until the user types something there is no filter row to advertise the + // filter, so the menu says that typing is a thing. + gui.Views.Menu.Subtitle = lo.Ternary(menuWithFilterRowVisible && !filterRowVisible, gui.c.Tr.MenuFilterHint, "") + } + if menuWithFilterRowVisible { + // The filter input is the current view for as long as such a menu is open, + // so without this the cursor would sit on the menu's bottom border, where + // the filter row is yet to appear. + gui.g.Cursor = filterRowVisible + } gui.Views.Tooltip.Visible = gui.Views.Menu.Visible && gui.Views.Tooltip.Buffer() != "" for _, context := range gui.transientContexts() { @@ -223,6 +239,26 @@ outer: return nil } +// The height below which we show the "not enough space" view instead of the +// layout. +func minimumScreenHeight(sideWindowCount int, menuWithFilterRowVisible bool) int { + // When the screen is too short the side panels are squashed, with the + // unfocused ones taking one row each and the focused one taking the rest. The + // more panels there are, the more rows the unfocused ones reserve, so the + // floor below which there's no room left for the focused panel grows with the + // panel count. Keep the historical floor of 9 for the default five panels. + minimumHeight := max(9, sideWindowCount+4) + + // A menu popup gets three quarters of the screen, of which its frame, the + // tooltip gap below it and a reserved filter row take seven rows, so below 11 + // rows there is no room left for even one menu item. + if menuWithFilterRowVisible { + minimumHeight = max(minimumHeight, 11) + } + + return minimumHeight +} + func (gui *Gui) prepareView(viewName string) (*gocui.View, error) { // arbitrarily giving the view enough size so that we don't get an error, but // it's expected that the view will be given the correct size before being shown diff --git a/pkg/gui/layout_test.go b/pkg/gui/layout_test.go new file mode 100644 index 000000000..1495bcd49 --- /dev/null +++ b/pkg/gui/layout_test.go @@ -0,0 +1,14 @@ +package gui + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMinimumScreenHeight(t *testing.T) { + assert.Equal(t, 9, minimumScreenHeight(5, false)) + assert.Equal(t, 12, minimumScreenHeight(8, false)) + assert.Equal(t, 11, minimumScreenHeight(5, true)) + assert.Equal(t, 12, minimumScreenHeight(8, true)) +} diff --git a/pkg/gui/main_panels.go b/pkg/gui/main_panels.go index 03b7469d2..bc4a4219e 100644 --- a/pkg/gui/main_panels.go +++ b/pkg/gui/main_panels.go @@ -2,6 +2,7 @@ package gui import ( "github.com/jesseduffield/lazygit/pkg/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -107,16 +108,6 @@ func (gui *Gui) allMainContextPairs() []types.MainContextPair { } func (gui *Gui) refreshMainViews(opts types.RefreshMainOpts) { - // need to reset scroll positions of all other main views - for _, pair := range gui.allMainContextPairs() { - if pair.Main != opts.Pair.Main { - pair.Main.GetView().SetOrigin(0, 0) - } - if pair.Secondary != nil && pair.Secondary != opts.Pair.Secondary { - pair.Secondary.GetView().SetOrigin(0, 0) - } - } - gui.moveMainContextPairToTop(opts.Pair) if opts.Main != nil { @@ -129,9 +120,37 @@ func (gui *Gui) refreshMainViews(opts types.RefreshMainOpts) { opts.Pair.Secondary.GetView().Clear() } + // Reset the scroll positions of all the other main views. We do this after + // moving this pair to the top (which copies the previously-shown view's + // content into the now-visible one to avoid a blank frame): resetting first + // would zero that source view's scroll before it gets copied, forcing the + // placeholder to the top instead of leaving it where the screen already was. + for _, pair := range gui.allMainContextPairs() { + if pair.Main != opts.Pair.Main { + pair.Main.GetView().SetOrigin(0, 0) + } + if pair.Secondary != nil && pair.Secondary != opts.Pair.Secondary { + pair.Secondary.GetView().SetOrigin(0, 0) + } + } + gui.splitMainPanel(opts.Secondary != nil) } func (gui *Gui) splitMainPanel(splitMainPanel bool) { gui.State.SplitMainPanel = splitMainPanel } + +// reApplySearch runs a search the view holds again over the content a render has just +// finished putting there, so that the matches highlighted and the "x of y" status +// describe what the view shows now rather than what it showed when the search was +// typed. Call it once the content is final. +func (gui *Gui) reApplySearch(view *gocui.View) { + // While the prompt is open, the search view holds what the user is typing, and the + // status would be written over it. + if gui.State.ContextMgr.Current().GetKey() == context.SEARCH_CONTEXT_KEY { + return + } + + view.RefreshSearch() +} diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 23016b9a5..1bfdb4581 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -69,10 +69,12 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error { gui.State.Contexts.Menu.SetPrompt(opts.Prompt) gui.State.Contexts.Menu.SetAllowFilteringKeybindings(opts.AllowFilteringKeybindings) gui.State.Contexts.Menu.SetKeybindingsTakePrecedence(!opts.KeepConflictingKeybindings) + gui.State.Contexts.Menu.SetFilterAsYouType(opts.FilterAsYouType) gui.State.Contexts.Menu.SetOnCancel(opts.OnCancel) gui.State.Contexts.Menu.SetSelection(0) - gui.Views.Menu.SetOriginY(0) + gui.Views.MenuFilter.ClearTextArea() + gui.Views.MenuFilter.RenderTextArea() gui.Views.Menu.Title = opts.Title gui.Views.Menu.FgColor = theme.GocuiDefaultTextColor @@ -82,9 +84,7 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error { gui.Views.Tooltip.Visible = true // resetting keybindings so that the menu-specific keybindings are registered - if err := gui.resetKeybindings(); err != nil { - return err - } + gui.resetKeybindings() gui.c.PostRefreshUpdate(gui.State.Contexts.Menu) diff --git a/pkg/gui/pty.go b/pkg/gui/pty.go index 719f5c348..fb7ba352e 100644 --- a/pkg/gui/pty.go +++ b/pkg/gui/pty.go @@ -72,6 +72,16 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error cmd.Args = withPtyGitConfig(cmd.Args, runtime.GOOS) + // Mark the view as loading synchronously now, before the layout pass: the + // actual task is created in afterLayout (below), which runs after layout, so + // without this the next layout pass would clamp the scroll position to the + // not-yet-loaded content. + gui.getManager(view).StartLoading() + // Hold the scrollbar at its current height while the re-render loads, so the + // thumb doesn't shrink and snap back when the first partial paint swaps in + // (see the matching call in newCmdTask). + view.FreezeScrollbarHeight() + // Run the pty after layout so that it gets the correct size gui.afterLayout(func() error { // Need to get the width and the pager command again because the layout might have diff --git a/pkg/gui/tasks_adapter.go b/pkg/gui/tasks_adapter.go index 3dce93874..3ed141d68 100644 --- a/pkg/gui/tasks_adapter.go +++ b/pkg/gui/tasks_adapter.go @@ -18,6 +18,15 @@ func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error ).Debug("RunCommand") manager := gui.getManager(view) + // Mark the view as loading synchronously (before the task's goroutine runs + // and before the next layout pass) so the layout doesn't clamp the scroll + // position to the not-yet-loaded content. + manager.StartLoading() + // Hold the scrollbar at the height the view has now (the previous render), + // while it still shows that render: once the re-render swaps in its first + // partial paint the displayed buffer is briefly short, and we don't want the + // thumb to shrink and snap back as the rest loads. + view.FreezeScrollbarHeight() // Snapshot the view width here, on the UI thread, so the task goroutine // doesn't read the view's live dimensions while it streams output. It's @@ -80,9 +89,9 @@ func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error { manager := gui.getManager(view) f := func(tasks.TaskOpts) error { - return gui.g.OnUIThreadAndWaitBackground(func() error { + return gui.g.OnUIThreadAndWaitBackground(func() { gui.c.SetViewContent(view, str) - return nil + gui.reApplySearch(view) }) } @@ -97,10 +106,10 @@ func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX in manager := gui.getManager(view) f := func(tasks.TaskOpts) error { - return gui.g.OnUIThreadAndWaitBackground(func() error { + return gui.g.OnUIThreadAndWaitBackground(func() { gui.c.SetViewContent(view, str) view.SetOrigin(originX, originY) - return nil + gui.reApplySearch(view) }) } @@ -115,10 +124,10 @@ func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) e manager := gui.getManager(view) f := func(tasks.TaskOpts) error { - return gui.g.OnUIThreadAndWaitBackground(func() error { + return gui.g.OnUIThreadAndWaitBackground(func() { gui.c.ResetViewOrigin(view) gui.c.SetViewContent(view, str) - return nil + gui.reApplySearch(view) }) } @@ -136,12 +145,10 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager { gui.Log, view, func() { - // we could clear here, but that actually has the effect of causing a flicker - // where the view may contain no content momentarily as the gui refreshes. - // Instead, we're rewinding the write pointer so that we will just start - // overwriting the existing content from the top down. Once we've reached - // the end of the content do display, we call view.FlushStaleCells() to - // clear out the remaining content from the previous render. + // Called before showing the "loading..." indicator: clear the + // displayed buffer so only "loading..." is shown. The actual content + // is rendered off-screen (beginRender below) and swapped in, so it + // never overwrites the displayed buffer incrementally. view.Reset() }, func() { @@ -153,6 +160,11 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager { gui.renderContentOnly() }, func() { + // The content is fully loaded now, so let the scrollbar track it + // directly again (it was held at the previous render's height while + // loading, see FreezeScrollbarHeight). + view.UnfreezeScrollbarHeight() + // Need to check if the content of the view is well past the origin. linesHeight := view.ViewLinesHeight() _, originY := view.Origin() @@ -162,11 +174,13 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager { view.SetOrigin(0, newOriginY) } - view.FlushStaleCells() + gui.reApplySearch(view) }, func() { view.SetOrigin(0, 0) }, + view.BeginOffscreenRender, + view.SwapInOffscreenRender, func() gocui.Task { // A background task: rendering content into a view is display // work, not lazygit driving a git operation, so it must not diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 75cb53018..58ccf6d60 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -48,8 +48,12 @@ type IGuiCommon interface { RefreshFromWorker(RefreshOptions) // we call this when we've changed something in the view model but not the actual model, // e.g. expanding or collapsing a folder in a file view. Calling 'Refresh' in this - // case would be overkill, although refresh will internally call 'PostRefreshUpdate' + // case would be overkill, although refresh will internally call 'PostRefreshUpdate'. + // It re-focuses the context's selection, which scrolls it into view. PostRefreshUpdate(Context) + // Like PostRefreshUpdate, with control over scrolling and whether to update + // the main view. + PostRefreshUpdateWithOptions(Context, OnFocusOpts) // renders string to a view without resetting its origin SetViewContent(view *gocui.View, content string) @@ -141,7 +145,7 @@ type IGuiCommon interface { KeybindingsOpts() KeybindingsOpts CallKeybindingHandler(binding *Binding) error - ResetKeybindings() error + ResetKeybindings() // hopefully we can remove this once we've moved all our keybinding stuff out of the gui god struct. GetInitialKeybindingsWithCustomCommands() ([]*Binding, []*gocui.ViewMouseBinding) @@ -209,6 +213,11 @@ type CreateMenuOptions struct { ColumnAlignment []utils.Alignment AllowFilteringKeybindings bool KeepConflictingKeybindings bool // if true, the keybindings that match essential bindings such as confirm or return will not be removed from menu items + // if true, the menu has a filter row of its own and filters its items as the + // user types, instead of being filtered through the search prompt. Only for + // menus whose items don't have keybindings of their own, because those keys + // would clash with typing. + FilterAsYouType bool } type CreatePopupPanelOpts struct { diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 416b39b95..93b92c70e 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -4,7 +4,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" - "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sasha-s/go-deadlock" ) @@ -57,6 +56,10 @@ type IBaseContext interface { GetKind() ContextKind GetViewName() string + // The view that keyboard input goes to while this context is focused. That is + // the context's own view, unless the context has an editable view embedded in + // it which takes the keyboard instead, like the menu's filter input. + GetInputViewName() string GetView() *gocui.View GetViewTrait() IViewTrait GetWindowName() string @@ -72,6 +75,10 @@ type IBaseContext interface { // determined independently. HasControlledBounds() bool + // true if the context holds something for a selection to sit on. Contexts that + // don't show a selection at all say false, and so do lists with nothing in them. + HasSelectableContent() bool + // the total height of the content that the view is currently showing TotalContentHeight() int @@ -136,7 +143,6 @@ type IFilterableContext interface { ReApplyFilter(bool) IsFiltering() bool IsFilterableContext() - FilterPrefix(tr *i18n.TranslationSet) string } type ISearchableContext interface { @@ -223,13 +229,20 @@ type IViewTrait interface { ScrollDown(value int) PageDelta() int SelectedLineIdx() int - SetHighlight(bool) } type OnFocusOpts struct { - ClickedWindowName string - ClickedViewLineIdx int - ScrollSelectionIntoView bool + ClickedWindowName string + ClickedViewLineIdx int + + // Focusing a list context scrolls its selection into view. Set this to leave + // the view's scroll position alone instead; only for callers that maintain + // it themselves, e.g. by keeping the selection at the edge of the viewport. + KeepScrollPosition bool + + // Set this when the focused item hasn't changed and the main view's current + // content is still valid. + SkipMainViewUpdate bool } type OnFocusLostOpts struct { diff --git a/pkg/gui/types/refresh.go b/pkg/gui/types/refresh.go index c733e589e..d40a1bec5 100644 --- a/pkg/gui/types/refresh.go +++ b/pkg/gui/types/refresh.go @@ -72,6 +72,10 @@ type RefreshOptions struct { // letting each scope update the UI as soon as it's done. BatchUIUpdates bool + // Set this when the refresh doesn't invalidate the main view's current + // content, so refreshing the side context needn't render it again. + SkipMainViewUpdate bool + // Controls which local branch is selected after the refresh. Defaults to // KeepBranchSelectionByName. BranchSelection BranchSelectionBehavior diff --git a/pkg/gui/types/views.go b/pkg/gui/types/views.go index c740ccb2e..1a48d170a 100644 --- a/pkg/gui/types/views.go +++ b/pkg/gui/types/views.go @@ -27,6 +27,8 @@ type Views struct { Confirmation *gocui.View Prompt *gocui.View Menu *gocui.View + MenuFilterFrame *gocui.View + MenuFilter *gocui.View CommitMessage *gocui.View CommitDescription *gocui.View CommitFiles *gocui.View diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index d139984fa..3d924a566 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -25,6 +25,18 @@ func (gui *Gui) linesToReadFromCmdTask(v *gocui.View) tasks.LinesToRead { linesForFirstRefresh := height + oy + 10 + // A search counts the matches in everything the view holds, so a re-render of a + // view that is being searched is read all the way to the end (as opening the + // search prompt reads it, see MainViewController.openSearch). Lines left unread + // hold matches the search doesn't know about, and would add themselves to the + // "x of y" as the user scrolled far enough to load them. + if v.IsSearching() { + return tasks.LinesToRead{ + Total: -1, + InitialRefreshAfter: linesForFirstRefresh, + } + } + // We want to read as many lines initially as necessary to let the // scrollbar go to its minimum height, so that the scrollbar thumb doesn't // change size as you scroll down. @@ -132,7 +144,7 @@ func (gui *Gui) renderContentOnly() { // postRefreshUpdate is to be called on a context after the state that it depends on has been refreshed // if the context's view is set to another context we do nothing. // if the context's view is the current view we trigger a focus; re-selecting the current item. -func (gui *Gui) postRefreshUpdate(c types.Context) { +func (gui *Gui) postRefreshUpdate(c types.Context, opts types.OnFocusOpts) { t := time.Now() defer func() { gui.Log.Infof("postRefreshUpdate for %s took %s", c.GetKey(), time.Since(t)) @@ -140,27 +152,28 @@ func (gui *Gui) postRefreshUpdate(c types.Context) { c.HandleRender() - if gui.currentViewName() == c.GetViewName() { - c.HandleFocus(types.OnFocusOpts{}) + // The render may have given the context its first item, or taken its last one + // away, which decides whether its view draws a selection at all. + gui.State.ContextMgr.updateSelectionHighlights() + + if gui.currentViewName() == c.GetInputViewName() { + c.HandleFocus(opts) } else { // The FocusLine call is included in the HandleFocus method which we // call for focused views above; but we need to call it here for // non-focused views to ensure that an inactive selection is painted // correctly, and that integration tests see the up to date selection // state. - c.FocusLine(false) + c.FocusLine(!opts.KeepScrollPosition) + if opts.SkipMainViewUpdate { + return + } currentCtx := gui.State.ContextMgr.Current() if currentCtx.GetKey() == context.NORMAL_MAIN_CONTEXT_KEY || currentCtx.GetKey() == context.NORMAL_SECONDARY_CONTEXT_KEY { - // Searching can't cope well with the view being updated while it is being searched. - // We might be able to fix the problems with this, but it doesn't seem easy, so for now - // just don't rerender the view while searching, on the assumption that users will probably - // either search or change their data, but not both at the same time. - if !currentCtx.GetView().IsSearching() { - sidePanelContext := gui.State.ContextMgr.NextInStack(currentCtx) - if sidePanelContext != nil && sidePanelContext.GetKey() == c.GetKey() { - sidePanelContext.HandleRenderToMain() - } + sidePanelContext := gui.State.ContextMgr.NextInStack(currentCtx) + if sidePanelContext != nil && sidePanelContext.GetKey() == c.GetKey() { + sidePanelContext.HandleRenderToMain() } } else if c.GetKey() == gui.State.ContextMgr.CurrentStatic().GetKey() { // If our view is not the current one, but it is the current static context, then this diff --git a/pkg/gui/views.go b/pkg/gui/views.go index b47e76c5a..7b6fa93eb 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -66,6 +66,12 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping { {viewPtr: &gui.Views.CommitMessage, name: "commitMessage"}, {viewPtr: &gui.Views.CommitDescription, name: "commitDescription"}, {viewPtr: &gui.Views.Menu, name: "menu"}, + // the filter row of a menu that filters as you type: a frame that hangs off + // the bottom of the menu and shows the "Filter:" prompt, plus the input + // field that sits inside it. Both must come after the menu so that the row's + // top border is drawn over the menu's bottom border. + {viewPtr: &gui.Views.MenuFilterFrame, name: "menuFilterFrame"}, + {viewPtr: &gui.Views.MenuFilter, name: "menuFilter"}, {viewPtr: &gui.Views.Suggestions, name: "suggestions"}, {viewPtr: &gui.Views.Confirmation, name: "confirmation"}, {viewPtr: &gui.Views.Prompt, name: "prompt"}, @@ -139,6 +145,16 @@ func (gui *Gui) createAllViews() error { gui.Views.Menu.Visible = false + gui.Views.MenuFilterFrame.Visible = false + gui.Views.MenuFilter.Visible = false + gui.Views.MenuFilter.Frame = false + gui.Views.MenuFilter.Editable = true + gui.Views.MenuFilter.Editor = gocui.EditorFunc(gui.menuFilterEditor) + // The filter row belongs to the menu: it shares the menu's focus, and keys + // that the input field doesn't take are the menu's to handle. + gui.Views.MenuFilterFrame.ParentView = gui.Views.Menu + gui.Views.MenuFilter.ParentView = gui.Views.Menu + gui.Views.Tooltip.Visible = false gui.Views.Tooltip.AutoRenderHyperLinks = true @@ -155,17 +171,30 @@ func (gui *Gui) createAllViews() error { return nil } +// gocui expects a view's frame runes in this order: the horizontal and the +// vertical edge, then the top left, top right, bottom left and bottom right +// corner. +func frameRunesWithTopCorners(frameRunes []rune, topLeft rune, topRight rune) []rune { + return []rune{frameRunes[0], frameRunes[1], topLeft, topRight, frameRunes[4], frameRunes[5]} +} + func (gui *Gui) configureViewProperties() { frameRunes := []rune{'─', '│', '┌', '┐', '└', '┘'} + // The corners for a view that hangs off the bottom of another one, so that the + // border they share reads as a divider rather than as two frames touching. + teeLeft, teeRight := '├', '┤' switch gui.c.UserConfig().Gui.Border { case "double": frameRunes = []rune{'═', '║', '╔', '╗', '╚', '╝'} + teeLeft, teeRight = '╠', '╣' case "rounded": frameRunes = []rune{'─', '│', '╭', '╮', '╰', '╯'} case "hidden": frameRunes = []rune{' ', ' ', ' ', ' ', ' ', ' '} + teeLeft, teeRight = ' ', ' ' case "bold": frameRunes = []rune{'━', '┃', '┏', '┓', '┗', '┛'} + teeLeft, teeRight = '┣', '┫' } for _, mapping := range gui.orderedViewNameMappings() { @@ -177,6 +206,8 @@ func (gui *Gui) configureViewProperties() { (*mapping.viewPtr).InactiveViewSelBgColor = theme.GocuiInactiveViewSelectedLineBgColor } + gui.Views.MenuFilterFrame.FrameRunes = frameRunesWithTopCorners(frameRunes, teeLeft, teeRight) + gui.c.SetViewContent(gui.Views.SearchPrefix, gui.c.Tr.SearchPrefix) gui.Views.Stash.Title = gui.c.Tr.StashTitle diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 6d41d31d6..9c72b53df 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -879,6 +879,7 @@ type TranslationSet struct { SearchPrefix string FilterPrefix string FilterPrefixMenu string + MenuFilterHint string ExitSearchMode string ExitTextFilterMode string Switch string @@ -2039,7 +2040,8 @@ func EnglishTranslationSet() *TranslationSet { SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode", SearchPrefix: "Search: ", FilterPrefix: "Filter: ", - FilterPrefixMenu: "Filter (prepend '@' to filter keybindings): ", + FilterPrefixMenu: "Filter ('@' for keybindings): ", + MenuFilterHint: "(Type to filter)", WorktreesTitle: "Worktrees", WorktreeTitle: "Worktree", Switch: "Switch", diff --git a/pkg/i18n/translations/ja.json b/pkg/i18n/translations/ja.json index 593e253fb..8716d4da4 100644 --- a/pkg/i18n/translations/ja.json +++ b/pkg/i18n/translations/ja.json @@ -390,7 +390,6 @@ "DiscardFileChangesTitle": "ファイルの変更を破棄", "DisabledForGPG": "GPGを使用しているユーザーには利用できない機能です。\n\nパスフレーズエージェント(gpg-agentなど)を使用して署名時にパスフレーズを入力しなくても済むようにしている場合は、lazygitの設定ファイルに\n\ngit:\n overrideGpg: true\n\nを追加することでこの機能を有効にできます。", "CreateRepo": "Gitリポジトリがありません。新しいgitリポジトリを作成しますか? (y/N): ", - "BareRepo": "ベアリポジトリでLazygitを開こうとしましたが、Lazygitはまだベアリポジトリをサポートしていません。最近のリポジトリを開いてよろしいですか? (y/n) ", "InitialBranch": "ブランチ名(gitのデフォルトの場合は空のままにしてください): ", "NoRecentRepositories": "Lazygitはgitリポジトリで開く必要があります。有効な最近のリポジトリはありません。終了します。", "IncorrectNotARepository": "'notARepository'の値が正しくありません。'prompt'、'create'、'skip'、または'quit'のいずれかである必要があります。", @@ -764,7 +763,11 @@ "Switching": "チェックアウト中", "RemoveWorktree": "ワークツリーを削除", "RemoveWorktreeTitle": "ワークツリーを削除", + "RemoveWorktreeAndDeleteBranch": "ワークツリーとローカルブランチを削除", + "RemoveWorktreeAndDeleteBothBranches": "ワークツリーとローカル/リモートブランチを削除", "DetachWorktree": "ワークツリーをデタッチ", + "DetachWorktreeAndDeleteBranch": "ワークツリーをデタッチしてローカルブランチを削除", + "DetachWorktreeAndDeleteBothBranches": "ワークツリーをデタッチしてローカル/リモートブランチを削除", "DetachingWorktree": "ワークツリーをデタッチ中", "WorktreesTitle": "ワークツリー", "WorktreeTitle": "ワークツリー", diff --git a/pkg/i18n/translations/nl.json b/pkg/i18n/translations/nl.json index 4ebdcde84..e0878212b 100644 --- a/pkg/i18n/translations/nl.json +++ b/pkg/i18n/translations/nl.json @@ -344,10 +344,17 @@ "EditRemoteName": "Enter updated remote naam voor {{.remoteName}}:", "EditRemoteUrl": "Enter updated remote url voor {{.remoteName}}:", "RemoveRemote": "Verwijder remote", + "RemoveRemoteTooltip": "Verwijder de geselecteerde remote. Locale branches die een branch tracken van de remote worden niet aangepast.", + "RemoveRemotePrompt": "Weet je zeker dat je de remote wilt verwijderen?", "DeleteRemoteBranch": "Verwijder remote branch", + "DeleteRemoteBranches": "Verwijder remote branches", + "DeleteRemoteBranchTooltip": "Verwijder de remote branch van de remote.", + "DeleteLocalAndRemoteBranch": "Verwijder zowel lokale als remote branch", + "DeleteLocalAndRemoteBranches": "Verwijder zowel lokale als remote branches", "SetAsUpstream": "Instellen als upstream", "SetAsUpstreamTooltip": "Stel in als upstream van uitgecheckte branch", "SetUpstream": "Stel in als upstream van uitgecheckte branch", + "UnsetUpstream": "Verwijder de upstream configuratie van de geselecteerde branch", "DivergenceSectionHeaderLocal": "Lokaal", "DivergenceSectionHeaderRemote": "Remote", "SetUpstreamTitle": "Stel in als upstream branch", diff --git a/pkg/i18n/translations/pl.json b/pkg/i18n/translations/pl.json index 60ecf6109..bd744e3b0 100644 --- a/pkg/i18n/translations/pl.json +++ b/pkg/i18n/translations/pl.json @@ -337,7 +337,6 @@ "DiscardOldFileChangeTooltip": "Odrzuć zmiany w tym pliku z tego commita. Uruchamia interaktywne przebazowanie w tle, więc możesz otrzymać konflikt scalania, jeśli późniejszy commit również zmienia ten plik.", "DiscardFileChangesTitle": "Odrzuć zmiany w pliku", "CreateRepo": "Nie jesteś w repozytorium git. Utwórz nowe repozytorium git? (y/N): ", - "BareRepo": "Próbujesz otworzyć Lazygit w gołym repozytorium, ale Lazygit jeszcze nie obsługuje gołych repozytoriów. Otworzyć najnowsze repozytorium? (t/n) ", "InitialBranch": "Nazwa gałęzi? (pozostaw puste dla domyślnej gita): ", "NoRecentRepositories": "Musisz otworzyć lazygit w repozytorium git. Brak ważnych ostatnich repozytoriów. Wyjście.", "IncorrectNotARepository": "Wartość 'notARepository' jest nieprawidłowa. Powinna być jedną z 'prompt', 'create', 'skip', lub 'quit'.", diff --git a/pkg/i18n/translations/pt.json b/pkg/i18n/translations/pt.json index 499e45e8b..b292685f1 100644 --- a/pkg/i18n/translations/pt.json +++ b/pkg/i18n/translations/pt.json @@ -393,7 +393,6 @@ "DiscardOldFileChangeTooltip": "Descartar as alterações desse commit para este arquivo. Isso executa uma rebase interativa em segundo plano, então você pode ter um conflito de merge se um commit posterior também alterar este arquivo.", "DiscardFileChangesTitle": "Descartar alterações de arquivo", "CreateRepo": "Não está em um repositório git. Criar um novo repositório git? (y/N): ", - "BareRepo": "Você tentou abrir Lazygit em um repositório puro, mas Lazygit ainda não suporta repositórios vazios. Abrir os repositórios mais recentes? (y/n) ", "InitialBranch": "Nome da branch? (deixe vazio para o padrão do git): ", "NoRecentRepositories": "É necessário abrir lazygit em um repositório git. Nenhum repositório recente válido. Saindo do sistema.", "IncorrectNotARepository": "O valor de 'notARepository' está incorreto. Deve ser um dos 'prompt', 'create', 'sk', ou 'quit'.", diff --git a/pkg/i18n/translations/ru.json b/pkg/i18n/translations/ru.json index 34d7105fe..c3a025015 100644 --- a/pkg/i18n/translations/ru.json +++ b/pkg/i18n/translations/ru.json @@ -159,7 +159,7 @@ "RebaseOptionsTitle": "Параметры перебазирования", "CommitSummaryTitle": "Сводка коммита", "CommitDescriptionTitle": "Описание коммита", - "CommitDescriptionSubTitle": "Нажмите вкладку, чтобы переключить фокус", + "CommitDescriptionSubTitle": "Нажмите {{.togglePanelKeyBinding}}, чтобы переключить фокус, {{.commitMenuKeybinding}} для открытия меню", "LocalBranchesTitle": "Локальные Ветки", "SearchTitle": "Поиск", "TagsTitle": "Теги", @@ -228,7 +228,6 @@ "CanOnlyDiscardFromLocalCommits": "Изменения можно отменить только из локальных коммитов.", "DiscardOldFileChangeTooltip": "Отменить изменения коммита в этом файле", "DiscardFileChangesTitle": "Отменить изменения файла", - "BareRepo": "Вы пытались открыть Lazygit в пустом репозитории, но Lazygit ещё не поддерживает пустые репозитории. Открыть последний репозиторий? (y/n)", "InitialBranch": "Название ветки? (оставьте пустым для git по умолчанию):", "NoRecentRepositories": "Необходимо открыть lazygit в git репозитории. Нет валидных последних репозиториев. Выход.", "IncorrectNotARepository": "Неверное значение 'notARepository'. Это должно быть одним из 'prompt', 'create', 'skip', или 'quit'.", diff --git a/pkg/i18n/translations/zh-CN.json b/pkg/i18n/translations/zh-CN.json index 3ae7d4e65..fd522d669 100644 --- a/pkg/i18n/translations/zh-CN.json +++ b/pkg/i18n/translations/zh-CN.json @@ -89,6 +89,8 @@ "MergeConflictPressEnterToResolve": "按%s键解决。", "MergeConflictKeepFile": "保留文件", "MergeConflictDeleteFile": "删除文件", + "MergeConflictTakeCurrentCommit": "接受当前提交", + "MergeConflictTakeIncomingCommit": "接受传入提交", "Checkout": "检出", "CheckoutTooltip": "检出选中的项目", "CantCheckoutBranchWhilePulling": "当前分支在拉取远端时,无法检出到其他分支。", @@ -239,6 +241,7 @@ "UpdateFailedErr": "更新失败: {{.errMessage}}", "ConfirmQuitDuringUpdateTitle": "当前正在更新中...", "ConfirmQuitDuringUpdate": "当前正在更新中,您确定要退出吗?", + "IntroPopupMessage": "\n感谢使用 lazygit!你太棒了。有三件事想与你分享:\n\n 1) 如果您想了解 lazygit 的功能,请观看这个视频:\n https://youtu.be/CPLdltN7wgE\n\n 2) 务必阅读以下链接的最新发布说明:\n https://github.com/jesseduffield/lazygit/releases\n\n 3) 如果 lazygit 让您的生活更轻松,你可以通过点击右下角的\n 捐赠按钮来表达感谢。捐赠不会获得优先支持,\n 但我们对此深表感激。\n\n按下 {{confirmationKey}} 开始。\n", "NonReloadableConfigWarningTitle": "配置已更改", "NonReloadableConfigWarning": "以下配置设置已更改,但更改不会立即生效。请退出并重新启动lazygit以使更改生效:\n\n{{configs}}", "GitconfigParseErr": "由于存在未加引号的'\\'字符,因此 Gogit 无法解析您的 gitconfig 文件。删除它们应该可以解决问题。", @@ -349,12 +352,15 @@ "FwdNoLocalUpstream": "此分支的远程未在本地注册,无法快进", "FwdCommitsToPush": "此分支带有尚未推送的提交,无法快进", "PullRequestNoUpstream": "没有设置上游的分支无法执行拉取请求", + "PullRequestChecksError": "错误", "ErrorOccurred": "发生错误!请在以下位置创建 issue", "ConflictLabel": "冲突", "PendingRebaseTodosSectionHeader": "待处理变基任务", "PendingCherryPicksSectionHeader": "待处理拣选", "PendingRevertsSectionHeader": "待处理还原", "CommitsSectionHeader": "提交", + "MoveCommitsHere": "拖放到此", + "MovingCommitsHere": "在此处移动提交", "YouDied": "您死了!", "RewordNotSupported": "当前不支持交互式重新基准化时的重新措词提交", "ChangingThisActionIsNotAllowed": "不允许更改这类变基待办项目", @@ -409,9 +415,11 @@ "UndoingStatus": "正在撤销", "RedoingStatus": "正在重做", "CheckingOutStatus": "正在检出", + "CreatingBranchStatus": "创建分支", "CommittingStatus": "正在提交", "RewordingStatus": "修改提交信息", "RevertingStatus": "还原中...", + "ResettingStatus": "重置中", "CreatingFixupCommitStatus": "正在创建一个修复提交", "MovingCommitsToNewBranchStatus": "正在将提交移动到新分支", "CommitFiles": "提交文件", @@ -431,7 +439,7 @@ "DiscardFileChangesPromptResetPatch": "确定要从此提交中丢弃所选文件的更改吗?\n\n此操作将启动变基,还原这些文件更改。请注意,如果后续提交依赖于这些更改,您可能需要解决冲突。\n\n注意:这将重置活动的自定义补丁!", "DisabledForGPG": "使用GPG的用户无法使用此功能。\n\n如果您正在使用密码代理(如gpg-agent)以避免每次签名时输入密码,可以通过在lazygit配置文件中添加\n\ngit:\n overrideGpg: true\n\n来启用此功能。", "CreateRepo": "不在 git 仓库中。创建一个新的 git 仓库吗?(y/N): ", - "BareRepo": "您已经尝试在空仓库中打开Lazygit,但是Lazygit还不支持空仓库。打开最近的仓库吗?(y / n) ", + "BareRepoNotSupported": "Lazygit 不支持裸仓库。", "InitialBranch": "分支名称? (git的默认值为空): ", "NoRecentRepositories": "必须在git存储库中打开lazygit。没有有效的最近存储库。即将退出...", "IncorrectNotARepository": "'notARepository'的值不正确。它应该是“prompt”,“create”,“skip”或“quit”中的一个。", @@ -590,6 +598,8 @@ "ViewResetToUpstreamOptions": "查看上游重置选项", "NextScreenMode": "下一屏模式(正常/半屏/全屏)", "PrevScreenMode": "上一屏模式", + "DefaultDiffRendererName": "(默认)", + "ExternalDiffDiffRendererName": "(外部差异)", "StartSearch": "开始搜索", "StartFilter": "通过文本过滤当前视图", "SelectRemoteRepository": "为拉取请求选择基础仓库", @@ -598,6 +608,7 @@ "KeybindingsMenuSectionLocal": "本地", "KeybindingsMenuSectionGlobal": "全局", "KeybindingsMenuSectionNavigation": "导航", + "KeybindingsTooltip": "键绑定: ", "RenameBranch": "重命名分支", "Upstream": "上游", "BranchUpstreamOptionsTitle": "上游选项", @@ -740,6 +751,7 @@ "ErrStageDirWithInlineMergeConflicts": "无法 暂存/取消暂存 包含具有内联合并冲突的文件的目录。请先解决合并冲突", "ErrRepositoryMovedOrDeleted": "找不到仓库。它可能已被移动或删除 ¯\\_(ツ)_/¯", "ErrWorktreeMovedOrRemoved": "找不到工作树,它可能被删除或者移走了。 ¯\\\\_(ツ)_/¯", + "CantSwitchWhileOperationInProgress": "操作进行时无法切换仓库", "CommandLog": "命令日志", "ToggleShowCommandLog": "切换 显示/隐藏 命令日志", "FocusCommandLog": "焦点命令日志", @@ -832,7 +844,6 @@ "SearchKeybindings": "%s: 下一个匹配项, %s: 上一个匹配项, %s: 退出搜索模式", "SearchPrefix": "搜索: ", "FilterPrefix": "过滤: ", - "FilterPrefixMenu": "筛选(在筛选快捷键前添加 '@'):", "ExitSearchMode": "%s:退出搜索模式", "ExitTextFilterMode": "%s:退出过滤模式", "Switch": "切换", @@ -862,6 +873,9 @@ "NewWorktreePath": "新建工作树路径", "RemoveWorktreeTooltip": "删除选定的工作树。这将删除工作树的目录以及 .git 目录中有关工作树的元数据。", "NewBranchName": "新分支名称", + "NewWorktreeName": "新工作树名", + "WorktreeLocationTitle": "工作区位置", + "WorktreeLocationOther": "其他…", "LcWorktree": "工作区", "ChangingDirectoryTo": "将目录更改为 {{.path}}", "Name": "名称", diff --git a/pkg/i18n/translations/zh-TW.json b/pkg/i18n/translations/zh-TW.json index 5c7b50e47..dc430fab0 100644 --- a/pkg/i18n/translations/zh-TW.json +++ b/pkg/i18n/translations/zh-TW.json @@ -13,11 +13,13 @@ "MergingTitle": "主面板(合併)", "NormalTitle": "主面板(一般)", "LogTitle": "版本記錄", + "LogXOfYTitle": "日誌(%d / %d)", "CommitSummary": "提交摘要", "CredentialsUsername": "使用者名稱", "CredentialsPassword": "密碼", "CredentialsPassphrase": "SSH 金鑰密語", "CredentialsPIN": "SSH 金鑰 PIN 碼", + "CredentialsToken": "輸入 SSH 金鑰的 Token", "PassUnameWrong": "密碼、密語或使用者名稱錯誤", "Commit": "提交變更", "CommitTooltip": "提交暫存區變更", @@ -26,15 +28,27 @@ "SureToAmend": "是否確定要修改上次提交?之後你可以從提交面板中再次更改此次提交的訊息。", "NoCommitToAmend": "沒有可以修改的提交。", "CommitChangesWithEditor": "使用 git 編輯器提交變更", + "FindBaseCommitForFixup": "尋找 fixup 的基礎提交", + "FindBaseCommitForFixupTooltip": "找出目前變更所依據的提交,以便 amend/fixup。這樣不必逐一檢視分支中的提交來找出要 amend/fixup 的提交。請見文件:", + "NoBaseCommitsFound": "找不到基礎提交", + "MultipleBaseCommitsFoundStaged": "找到多個基礎提交。(請嘗試一次暫存較少變更。)", + "MultipleBaseCommitsFoundUnstaged": "找到多個基礎提交。(請嘗試暫存部分變更。)", + "BaseCommitIsAlreadyOnMainBranch": "此變更的基礎提交已在 main 分支上", + "BaseCommitIsNotInCurrentView": "基礎提交不在目前檢視中", + "HunksWithOnlyAddedLinesWarning": "diff 中有僅含新增行的區段;請仔細確認它們是否應納入找到的基礎提交。要繼續嗎?", "StatusTitle": "狀態", "GlobalTitle": "全域快捷鍵", "Execute": "執行", "Stage": "切換預存", + "StageTooltip": "切換所選檔案的暫存狀態。", "ToggleStagedAll": "全部預存/取消預存", + "ToggleStagedAllTooltip": "切換工作區中所有檔案的已暫存/未暫存狀態。", "ToggleTreeView": "顯示檔案樹狀視圖", + "ToggleTreeViewTooltip": "在平面佈局和樹佈局之間切換檔案檢視。平面佈局在單個列表中顯示所有檔案路徑,樹佈局按目錄分組檔案。\n\n可以在設定檔中使用 'gui.showFileTree' 鍵更改預設設定。", "OpenDiffTool": "開啟外部差異工具 (git difftool)", "OpenMergeTool": "開啟外部合併工具", "Refresh": "重新整理", + "RefreshTooltip": "重新整理Git狀態(即在背景執行`git status`、`git branch`等命令以更新面板內容)。此操作不會執行`git fetch`。", "Push": "推送", "Pull": "拉取", "PushTooltip": "推送到遠端。如果沒有設定遠端,會開啟設定視窗。", @@ -42,18 +56,49 @@ "FileFilter": "篩選檔案 (預存/未預存)", "CopyToClipboardMenu": "複製到剪貼簿", "CopyFileName": "檔案名稱", + "CopyRelativeFilePath": "相對路徑", + "CopyAbsoluteFilePath": "絕對路徑", "CopyFileDiffTooltip": "如果有已預存的項目,此指令只考慮它們。否則,它將考慮所有未暫存的項目。", "CopySelectedDiff": "所選檔案的差異", "CopyAllFilesDiff": "所有檔案的差異", + "CopyFileContent": "所選檔案內容", + "NoContentToCopyError": "無可複製內容", "FileNameCopiedToast": "檔案名稱已複製", "FilePathCopiedToast": "檔案路徑已複製", "FileDiffCopiedToast": "已複製檔案差異", "AllFilesDiffCopiedToast": "已複製所有檔案差異", + "FileContentCopiedToast": "檔案內容已複製到剪貼簿", "FilterStagedFiles": "僅顯示預存的檔案", "FilterUnstagedFiles": "僅顯示未預存的檔案", + "FilterTrackedFiles": "僅顯示已跟蹤的檔案", + "FilterUntrackedFiles": "僅顯示未跟蹤的檔案", + "NoFilter": "無過濾", + "FilterLabelStagedFiles": "(僅暫存)", + "FilterLabelUnstagedFiles": "(僅未暫存)", + "FilterLabelTrackedFiles": "(僅跟蹤)", + "FilterLabelUntrackedFiles": "(僅未跟蹤)", + "FilterLabelConflictingFiles": "(僅衝突)", "MergeConflictsTitle": "合併衝突", + "MergeConflictDescription_DD": "衝突:目前變更和傳入變更都移動或重新命名了此檔案,但目標位置不同。雖然不知道具體位置,但這兩個目標檔案應該都會顯示為衝突(分別標記為'AU'和'UA')。最可能的解決方法是刪除此檔案,並選擇其中一個目標位置,刪除另一個。", + "MergeConflictDescription_AU": "衝突:此檔案是目前變更中移動或重新命名的目標位置,但在傳入變更中被移動或重新命名到其他位置。另一個目標位置也應顯示為衝突(標記為'UA'),同時兩個重新命名前的原檔案也會顯示為衝突(標記為'DD')。", + "MergeConflictDescription_UA": "衝突:此檔案是傳入變更中移動或重新命名的目標位置,但在目前變更中被移動或重新命名到其他位置。另一個目標位置也應顯示為衝突(標記為'AU'),同時兩個重新命名前的原檔案也會顯示為衝突(標記為'DD')。", + "MergeConflictDescription_DU": "衝突:目前變更刪除了此檔案,而傳入變更修改了此檔案。\n\n最可能的解決方法是手動將傳入的修改應用到程式碼其他位置後再刪除該檔案。", + "MergeConflictDescription_UD": "衝突:目前變更修改了此檔案,而傳入變更刪除了此檔案。\n\n最可能的解決方法是手動將目前修改應用到程式碼其他位置後再刪除該檔案。", + "MergeConflictIncomingDiff": "傳入變更:", + "MergeConflictCurrentDiff": "目前變更:", + "MergeConflictPressEnterToResolve": "按%s鍵解決。", + "MergeConflictKeepFile": "保留檔案", + "MergeConflictDeleteFile": "刪除檔案", + "MergeConflictTakeCurrentCommit": "採用目前提交", + "MergeConflictTakeIncomingCommit": "採用傳入提交", + "SubmoduleMergeConflictDescription": "衝突:子模組 '{{.path}}' 在目前和傳入的變更中被設定為不同的提交。請選擇要保留的提交。", + "StageConflictsRangeDisabled": "無法暫存包含合併衝突檔案的選取範圍;請先用 {{.goIntoKey}} 逐一解決衝突。", "Checkout": "檢出", "CheckoutTooltip": "檢出選定的項目。", + "CantCheckoutBranchWhilePulling": "目前分支在拉取遠端時,無法檢出到其他分支。", + "TagCheckoutTooltip": "檢出選擇的標籤作為分離的HEAD。", + "RemoteBranchCheckoutTooltip": "基於目前選中的遠端分支檢出一個新的本地分支,或者將遠端分支作分離的HEAD。", + "CantPullOrPushSameBranchTwice": "在推送或拉取分支的過程中,您不能再次推送或拉取同一個分支", "NoChangedFiles": "沒有變更的檔案", "SoftReset": "軟重設", "AlreadyCheckedOutBranch": "你已經檢出這個分支了", @@ -62,63 +107,113 @@ "BranchName": "分支名稱", "NewBranchNameBranchOff": "新的分支名稱 (根據 '{{.branchName}}' 分支創建)", "CantDeleteCheckOutBranch": "無法刪除已檢出的分支!", + "DeleteBranchTitle": "刪除分支'{{.selectedBranchName}}'?", + "DeleteBranchesTitle": "刪除選定的分支?", + "DeleteLocalBranch": "刪除本地分支", + "DeleteLocalBranches": "刪除本地分支", "DeleteRemoteBranchPrompt": "確定要刪除遠端 {{.upstream}} 的標籤 '{{.selectedBranchName}}'?", + "DeleteRemoteBranchesPrompt": "確定要從各自遠端倉庫刪除所選分支的遠端分支嗎?", + "DeleteLocalAndRemoteBranchPrompt": "確定要同時刪除本地的'{{.localBranchName}}'分支和遠端'{{.remoteName}}'上的'{{.remoteBranchName}}'分支嗎?", + "DeleteLocalAndRemoteBranchesPrompt": "確定要從本機刪除所選分支,並從各自的遠端儲存庫刪除對應的遠端分支嗎?", + "ForceDeleteBranchTitle": "強制刪除分支", "ForceDeleteBranchMessage": "'{{.selectedBranchName}}' 分支尚未完全合併。是否刪除?", + "ForceDeleteBranchesMessage": "部分所選分支尚未完全合併。確定要刪除它們嗎?", "RebaseBranch": "將已檢出的分支變基至此分支", + "RebaseBranchTooltip": "將檢出的分支變基到所選的分支上。", "CantRebaseOntoSelf": "無法將分支變基至自己", "CantMergeBranchIntoItself": "無法將一個分支合併至自己", "ForceCheckout": "強制檢出", + "ForceCheckoutTooltip": "強制檢出所選分支。這將在檢出所選分支之前放棄工作目錄中的所有本地更改。", "CheckoutByName": "根據名稱檢出", + "CheckoutByNameTooltip": "按名稱檢出。在輸入框中,您可以輸入'-' 來切換到最後一個分支。", + "CheckoutPreviousBranch": "簽出上一個分支", "RemoteBranchCheckoutTitle": "檢出 {{.branchName}}", + "RemoteBranchCheckoutPrompt": "您希望已什麼方式檢出到該分支?", "CheckoutTypeNewBranch": "新本地分支", "CheckoutTypeNewBranchTooltip": "將遠端分支檢出為追蹤它的本地分支。", "CheckoutTypeDetachedHead": "分離 HEAD", "CheckoutTypeDetachedHeadTooltip": "將遠端分支檢出為分離的 HEAD,在只想測試但不動工時很實用。您稍後仍能根據它建立一個本地分支。", "NewBranch": "新分支", + "NewBranchFromStashTooltip": "從選定的貯藏項建立一個新分支。這是透過 git 檢查建立貯藏項的提交,從該提交建立一個新分支,然後將貯藏項作為附加提交應用到新分支來實現的。", + "MoveCommitsToNewBranch": "移動提交至新分支", + "MoveCommitsToNewBranchTooltip": "建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。\n\n請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。", + "MoveCommitsToNewBranchFromMainPrompt": "這將把所有未推送的提交移動到一個新分支(基於 {{.baseBranchName}})。然後,它會將目前分支硬重置到其上游分支。您要繼續嗎?", + "MoveCommitsToNewBranchMenuPrompt": "這將把所有未推送的提交移動到一個新分支。這個新分支可以從主分支({{.baseBranchName}})建立,也可以堆疊在目前分支之上。您希望選擇哪種方式?", + "MoveCommitsToNewBranchFromBaseItem": "從基礎分支建立新分支 (%s)", + "MoveCommitsToNewBranchStackedItem": "堆疊在目前分支上的新分支 (%s)", + "CannotMoveCommitsFromDetachedHead": "無法從分離頭移動提交", + "CannotMoveCommitsNoUpstream": "無法從沒有上游分支的分支中移動提交", + "CannotMoveCommitsBehindUpstream": "無法移動落後於其上游分支的分支的提交", + "CannotMoveCommitsNoUnpushedCommits": "沒有未推送的提交可以移動到新分支", "NoBranchesThisRepo": "這個版本庫中沒有分支", "CommitWithoutMessageErr": "沒有提交訊息,無法提交", "Close": "關閉", "CloseCancel": "關閉/取消", "Confirm": "確認", "Quit": "結束", + "SquashTooltip": "將已選提交壓縮到該提交之下。這些選定的提交的訊息會附加到該提交的訊息之下。", "CannotSquashOrFixupFirstCommit": "沒有可以壓縮的提交", + "CannotSquashOrFixupMergeCommit": "無法對合並提交進行壓縮或修正", "Fixup": "修復 (Fixup)", + "FixupTooltip": "將選定的提交合併到其下面的提交中。與壓縮類似,但所選提交的訊息將被丟棄。", + "FixupKeepMessage": "修復並使用此提交資訊", + "FixupKeepMessageTooltip": "將所選提交壓縮到下方的提交中,使用此提交的資訊,並丟棄下方提交的資訊。", + "SetFixupMessage": "設定修復提交資訊", + "SetFixupMessageTooltip": "設定修復提交的資訊選項。-C 選項表示使用此提交的資訊,而非目標提交的資訊。", + "FixupDiscardMessage": "修復並丟棄此提交的資訊", + "FixupDiscardMessageTooltip": "將所選提交壓縮到下方的提交中,丟棄此提交的資訊。", "SureSquashThisCommit": "是否要把這個提交壓縮到下面的提交中?", "Squash": "壓縮 (Squash)", "PickCommitTooltip": "挑選提交 (於變基過程中)", "Pick": "挑選", "Edit": "編輯", "Revert": "還原", + "RevertCommitTooltip": "為所選提交建立還原提交,這會反向應用所選提交的更改。", "Reword": "改寫提交", "CommitRewordTooltip": "改寫選中的提交訊息", "DropCommit": "刪除提交", + "DropCommitTooltip": "刪除選中的提交。這將透過變基從分支中刪除該提交,如果該提交修改的內容依賴於後續的提交,則需要解決合併衝突。", "MoveDownCommit": "向下移動提交", "MoveUpCommit": "向上移動提交", + "CannotMoveAnyFurther": "無法進一步移動", + "CannotMoveMergeCommit": "無法移動合併提交", "EditCommit": "編輯(開始互動變基)", "EditCommitTooltip": "編輯提交", "AmendCommitTooltip": "使用已預存的更改修正提交", "Amend": "修改", "ResetAuthor": "重設作者", + "ResetAuthorTooltip": "將提交作者重置為目前設定的使用者。這也將更新作者的時間戳", "SetAuthor": "設定作者", + "SetAuthorTooltip": "基於提示設定作者", "AddCoAuthor": "添加合作者", "AmendCommitAttribute": "設定/重設提交作者", + "AmendCommitAttributeTooltip": "設定或重置提交的作者,或新增其他作者。", "SetAuthorPromptTitle": "設定作者(格式:「姓名 <電子郵件>」)", + "AddCoAuthorPromptTitle": "新增共同作者(格式為 'Name ')", + "AddCoAuthorTooltip": "新增共同作者 使用GitHub/GitLab後設資料共同作者(Co-authored-by)。", "RewordCommitEditor": "使用編輯器改寫提交", "NoCommitsThisBranch": "這個分支沒有提交", "UpdateRefHere": "在這裡更新 '{{.ref}}' 分支", + "ExecCommandHere": "在這裡執行以下命令:", "Error": "錯誤", "Undo": "復原", "UndoReflog": "復原", "RedoReflog": "取消復原", "UndoTooltip": "將使用 reflog 確任 git 指令以復原。這不包括工作區更改;只考慮提交。", "RedoTooltip": "將使用 reflog 確任 git 指令以重作。這不包括工作區更改;只考慮提交。", + "UndoMergeResolveTooltip": "撤消上次合併衝突解決。", "DiscardAllTooltip": "捨棄 '{{.path}}' 預存/未預存更改。", "DiscardUnstagedTooltip": "捨棄 '{{.path}}' 未預存更改。", + "DiscardUnstagedDisabled": "選中的專案既沒有已暫存的變更也沒有未暫存的變更。", "Pop": "還原", + "StashPopTooltip": "將儲存項應用到工作目錄並刪除儲存項。", "Drop": "捨棄", + "StashDropTooltip": "從貯藏列表中刪除該貯藏項。", "Apply": "套用", + "StashApplyTooltip": "將貯藏項應用到您的工作目錄。", "NoStashEntries": "沒有收藏記錄", "StashDrop": "放棄收藏記錄", + "SureDropStashEntry": "確定要刪除選中的儲藏條目嗎?", "StashPop": "還原收藏記錄", "SurePopStashEntry": "是否從收藏中還原這個記錄?", "StashApply": "套用收藏記錄", @@ -132,6 +227,7 @@ "ForcePush": "強制推送", "ForcePushPrompt": "你的分支與遠端分支分岔。按 'ESC' 取消,或按 'Enter' 強制推送。", "ForcePushDisabled": "你的分支與遠端分支分岔,你已禁用強制推送", + "UpdatesRejected": "更新被拒絕。在下次推送前,請先抓取並檢查遠端分支。", "UpdatesRejectedAndForcePushDisabled": "更新被拒絕,你已禁用強制推送", "CheckForUpdate": "檢查更新", "CheckingForUpdates": "正在檢查更新...", @@ -147,6 +243,9 @@ "UpdateFailedErr": "更新失敗:{{.errMessage}}", "ConfirmQuitDuringUpdateTitle": "正在更新中", "ConfirmQuitDuringUpdate": "正在進行更新,是否結束?", + "IntroPopupMessage": "\n感謝你使用 lazygit!真的,你很棒。以下有三件事想和你分享:\n\n 1) 如果你想了解 lazygit 的功能,請觀看這支影片:\n https://youtu.be/CPLdltN7wgE\n\n 2) 請務必閱讀最新的發行說明:\n https://github.com/jesseduffield/lazygit/releases\n\n 3) 如果 lazygit 讓你的生活更輕鬆,可以按右下角的捐款按鈕向我們致謝。捐款不會提供優先支援,但我們非常感謝。\n\n按 {{confirmationKey}} 開始。\n", + "NonReloadableConfigWarningTitle": "設定已更改", + "NonReloadableConfigWarning": "以下設定設定已更改,但更改不會立即生效。請退出並重新啟動lazygit以使更改生效:\n\n{{configs}}", "GitconfigParseErr": "Gogit 無法解析你的 gitconfig 檔案,因為存在未引用的 '\\' 字符,刪除它們應該可以解決這個問題。", "EditFile": "編輯檔案", "EditFileTooltip": "使用外部編輯器開啟", @@ -156,20 +255,51 @@ "IgnoreFile": "添加到 .gitignore", "ExcludeFile": "添加到 .git/info/exclude", "RefreshFiles": "重新整理檔案", + "FocusMainView": "聚焦主檢視", "Merge": "合併到當前檢出的分支", + "MergeBranchTooltip": "檢視將選中項合併到目前分支的選項(正常合併,壓縮合並)", + "RegularMergeFastForward": "常規合併(快進)", + "RegularMergeFastForwardTooltip": "將 '{{.checkedOutBranch}}' 快轉至 '{{.selectedBranch}}',不建立合併提交。", + "CannotFastForwardMerge": "無法將 '{{.checkedOutBranch}}' 快進到 '{{.selectedBranch}}'", + "RegularMergeNonFastForward": "常規合併(帶合併提交)", + "RegularMergeNonFastForwardTooltip": "將 '{{.selectedBranch}}' 合併到 '{{.checkedOutBranch}}',建立一個合併提交。", + "SquashMergeUncommitted": "壓縮合並並保持未提交狀態", + "SquashMergeUncommittedTooltip": "將 '{{.selectedBranch}}' 壓縮合併到工作樹中。", + "SquashMergeCommitted": "壓縮合並,然後提交", + "SquashMergeCommittedTooltip": "將 '{{.selectedBranch}}' 壓縮合併到 '{{.checkedOutBranch}}' 作為一次提交。", "ConfirmQuit": "是否結束?", "SwitchRepo": "切換到最近使用的版本庫", + "AllBranchesLogGraph": "顯示/迴圈所有分支日誌", + "AllBranchesLogGraphReverse": "顯示/迴圈所有分支日誌(反向)", "UnsupportedGitService": "不支援的 git 服務", "CopyPullRequestURL": "複製拉取請求的 URL 到剪貼板", + "OpenPullRequestInBrowser": "在瀏覽器中開啟拉取請求", + "NoPullRequestForBranch": "未找到此分支的拉取請求", "NoBranchOnRemote": "這個分支在遠端不存在。需要先將其推送至遠端。", "Fetch": "擷取", "FetchTooltip": "同步遠端異動", + "CollapseAll": "摺疊全部檔案", + "CollapseAllTooltip": "摺疊檔案樹中的全部目錄", + "ExpandAll": "展開全部檔案", + "ExpandAllTooltip": "展開檔案樹中的全部目錄", + "DisabledInFlatView": "平面檢視中不可用", "FileEnter": "選擇檔案中的單個程式碼塊/行,或展開/折疊目錄", + "FileEnterTooltip": "如果選中的是一個檔案,則會進入到暫存檢視,以便可以暫存單個程式碼塊/行。如果選中的是一個目錄,則會摺疊/展開這個目錄。", "StageSelectionTooltip": "切換現有行的狀態 (已預存/未預存)", "DiscardSelection": "刪除變更 (git reset)", + "DiscardSelectionTooltip": "選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。", + "ToggleSelectHunk": "切換程式碼塊選擇", + "SelectHunk": "選擇程式碼塊", + "SelectLineByLine": "逐行選擇", + "ToggleSelectHunkTooltip": "切換逐行選擇與程式碼塊選擇模式。", + "HunkStagingHint": "程式碼塊選擇模式現在是暫存區的預設模式。如果您想暫存單行,請按 '%s' 切換到逐行模式。\n\n如果您希望預設使用逐行模式(像早期 lazygit 版本那樣),請將\n\ngui:\n useHunkModeInStagingView: false\n\n新增到您的 lazygit 設定中。", "ToggleSelectionForPatch": "向 (或從) 補丁中添加/刪除行", + "RemoveSelectionFromPatch": "從提交中移除行", + "RemoveSelectionFromPatchTooltip": "從本次提交中移除所選行。此操作會在背景執行互動式變基,因此如果後續提交也修改了這些行,您可能會遇到合併衝突。", "EditHunk": "編輯程式碼塊", + "EditHunkTooltip": "在外部編輯器中編輯選中的程式碼塊。", "ToggleStagingView": "切換至另一個面板 (已預存/未預存更改)", + "ToggleStagingViewTooltip": "切換到其他檢視(已暫存/未暫存的變更)。", "ReturnToFilesPanel": "返回檔案面板", "FastForward": "從上游快進此分支", "FastForwardTooltip": "從遠端快進所選的分支", @@ -178,42 +308,78 @@ "ViewConflictsMenuItem": "檢視衝突", "AbortMenuItem": "中止%s", "PickHunk": "挑選程式碼片段", + "PickBothHunks": "選取兩個區塊", "ViewMergeRebaseOptions": "查看合併/變基選項", + "ViewMergeRebaseOptionsTooltip": "檢視目前合併或變基的中止、繼續、跳過選項。", + "ViewMergeOptions": "檢視合併選項", "ViewRebaseOptions": "查看合併/變基選項", + "ViewCherryPickOptions": "檢視揀選選項", + "ViewRevertOptions": "檢視撤銷選項", "NotMergingOrRebasing": "你當前既不在變基也不在合併中", "AlreadyRebasing": "無法在變基期間執行此操作", + "NotMidRebase": "此操作僅在互動式變基期間有效", + "MustSelectFixupCommit": "此操作僅適用於修復提交", "RecentRepos": "最近的版本庫", "MergeOptionsTitle": "合併選項", "RebaseOptionsTitle": "變基選項", + "CherryPickOptionsTitle": "揀選選項", + "RevertOptionsTitle": "撤銷選項", "CommitSummaryTitle": "提交摘要", "CommitDescriptionTitle": "提交描述", "CommitDescriptionSubTitle": "按 tab 鍵聚焦", + "CommitDescriptionFooter": "按 {{.confirmInEditorKeybinding}} 提交", + "CommitHooksDisabledSubTitle": "(鉤子已禁用)", "LocalBranchesTitle": "本地分支", "SearchTitle": "搜尋", "TagsTitle": "標籤", "MenuTitle": "功能表", + "CommitMenuTitle": "提交 選單", "RemotesTitle": "遠端", "RemoteBranchesTitle": "遠端分支", "PatchBuildingTitle": "主面板 (補丁生成)", "InformationTitle": "資訊", "SecondaryTitle": "次要", "ReflogCommitsTitle": "日誌", + "ConflictsResolved": "所有合併衝突已解決。繼續 %s 嗎?", "Continue": "確認", + "UnstagedFilesAfterConflictsResolved": "衝突解決後檔案已被修改。是否自動暫存並繼續?", "RebasingTitle": "將 '{{.checkedOutBranch}}'", + "RebasingFromBaseCommitTitle": "從標記的幾點變基'{{.checkedOutBranch}}'", "SimpleRebase": "簡單變基 變基至 '{{.ref}}'", "InteractiveRebase": "互動變基 變基至 '{{.ref}}'", + "RebaseOntoBaseBranch": "變基到主分支 ({{.baseBranch}})", "InteractiveRebaseTooltip": "開始一個互動變基,以中斷開始,這樣你可以在繼續之前更新TODO提交", + "RebaseOntoBaseBranchTooltip": "將已檢出的分支變基到主分支上(例如最近的主分支)。", + "MustSelectTodoCommits": "在變基過程中, 該操作僅在選中TODO提交時有效。", "FwdNoUpstream": "無法快進無遠端的分支 ", "FwdNoLocalUpstream": "無法快進尚未在本地註冊的遠端分支", "FwdCommitsToPush": "無法快進帶有尚未推送的提交的分支", "PullRequestNoUpstream": "無法對沒有遠端的分支拉取", + "PullRequestChecksPassing": "通過", + "PullRequestChecksPending": "等待中", + "PullRequestChecksFailing": "失敗", + "PullRequestChecksError": "錯誤", + "PullRequestChecksExpected": "預期中", "ErrorOccurred": "發生錯誤!請在此詢問錯誤:", + "ConflictLabel": "衝突", + "PendingRebaseTodosSectionHeader": "待處理變基任務", + "PendingCherryPicksSectionHeader": "待處理揀選", + "PendingRevertsSectionHeader": "待處理還原", + "CommitsSectionHeader": "提交", + "MoveCommitsHere": "放到這裡", + "MovingCommitsHere": "正在將提交移到這裡", "YouDied": "你死了!", "RewordNotSupported": "在互動變基期間改寫提交目前不支援", "ChangingThisActionIsNotAllowed": "不允許更改此類變基待辦事項", + "NotAllowedMidCherryPickOrRevert": "在揀選或還原過程中不允許此操作", + "PickIsOnlyAllowedDuringRebase": "此操作僅在變基過程中允許", + "DroppingMergeRequiresSingleSelection": "刪除合併提交需要單個選中項", "CherryPickCopy": "複製提交 (揀選)", + "CherryPickCopyTooltip": "標記提交為已複製。然後,在本地提交檢視中,您可以按 `{{.paste}}` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `{{.escape}}` 來取消選擇。", "PasteCommits": "貼上提交 (揀選)", + "SureCherryPick": "確定要將複製的{{.numCommits}}個提交揀選到該分支上嗎?", "CherryPick": "揀選 (Cherry-pick)", + "CannotCherryPickNonCommit": "無法揀選TODO型別的提交", "Donate": "贊助", "AskQuestion": "諮詢", "PrevHunk": "選擇上一段", @@ -226,28 +392,48 @@ "ScrollUp": "向上捲動", "ScrollUpMainWindow": "向上捲動主面板", "ScrollDownMainWindow": "向下捲動主面板", + "SuspendApp": "掛起應用程式", + "CannotSuspendApp": "Windows 不支援掛起應用程式", "AmendCommitTitle": "修改提交", "AmendCommitPrompt": "是否使用預存檔案修改提交?", + "AmendCommitWithConflictsMenuPrompt": "警告:您即將使用已解決的衝突來修正上一個已完成的提交。此時這樣做很可能並非您所期望的。更可能的情況是,您只是想繼續執行變基操作。\n\n您仍然想要修正前一個提交嗎?", + "AmendCommitWithConflictsContinue": "否,繼續變基", + "AmendCommitWithConflictsAmend": "是,修正上一個提交", "DropCommitTitle": "刪除提交", "DropCommitPrompt": "是否刪除此提交?", + "DropUpdateRefPrompt": "您確定要刪除選定的 update-ref 待辦事項嗎?除非中止變基,否則這是不可逆轉的。", + "DropMergeCommitPrompt": "確定要刪除選中的合併提交嗎?注意:這將同時刪除透過該合併提交引入的所有提交。", "PullingStatus": "拉取", "PushingStatus": "推送", "FetchingStatus": "擷取", "SquashingStatus": "壓縮中", "FixingStatus": "修復中", "DeletingStatus": "刪除中", + "DroppingStatus": "刪除中...", "MovingStatus": "移動中", "RebasingStatus": "變基中", "MergingStatus": "合併中", "LowercaseRebasingStatus": "變基", "LowercaseMergingStatus": "合併", + "LowercaseCherryPickingStatus": "揀選中", + "LowercaseRevertingStatus": "還原中", "AmendingStatus": "修改中", "CherryPickingStatus": "揀選中", "UndoingStatus": "復原中", "RedoingStatus": "重做中", "CheckingOutStatus": "檢出中", + "CreatingBranchStatus": "正在建立分支", "CommittingStatus": "提交中", + "RewordingStatus": "修改提交資訊", "RevertingStatus": "還原中", + "ResettingStatus": "正在重設", + "CreatingFixupCommitStatus": "正在建立一個修復提交", + "MovingCommitsToNewBranchStatus": "正在將提交移動到新分支", + "ApplyingFilterStatus": "正在套用篩選條件", + "RemovingFilterStatus": "正在移除篩選條件", + "StashingStatus": "正在儲藏", + "ApplyingStashStatus": "正在套用儲藏", + "PoppingStashStatus": "正在彈出儲藏", "CommitFiles": "提交檔案", "SubCommitsDynamicTitle": "提交(%s)", "CommitFilesDynamicTitle": "差異檔案(%s)", @@ -255,13 +441,29 @@ "ViewItemFiles": "檢視所選項目的檔案", "CommitFilesTitle": "提交檔案", "CheckoutCommitFileTooltip": "檢出檔案", + "CannotCheckoutWithModifiedFilesErr": "您已有對您試圖簽出的檔案作出的本地修改。您需要先儲存或丟棄這些檔案。", + "CanOnlyDiscardFromLocalCommits": "只能從本地提交中丟棄更改", + "CannotDiscardFromMultipleCommits": "無法從多選提交中丟棄更改", + "Remove": "刪除", + "DiscardOldFileChangeTooltip": "放棄對此檔案的提交變更。", "DiscardFileChangesTitle": "捨棄檔案更改", - "BareRepo": "你嘗試在裸版本庫中開啟 Lazygit,但 Lazygit 尚未支援裸版本庫。是否開啟最新版本庫? (y/n) ", + "DiscardFileChangesPrompt": "確定要從此提交中丟棄所選檔案的更改嗎?\n\n此操作將啟動變基,還原這些檔案更改。請注意,如果後續提交依賴於這些更改,您可能需要解決衝突。", + "DiscardFileChangesPromptResetPatch": "確定要從此提交中丟棄所選檔案的更改嗎?\n\n此操作將啟動變基,還原這些檔案更改。請注意,如果後續提交依賴於這些更改,您可能需要解決衝突。\n\n注意:這將重置活動的自定義補丁!", + "DisabledForGPG": "使用GPG的使用者無法使用此功能。\n\n如果您正在使用密碼代理(如gpg-agent)以避免每次簽名時輸入密碼,可以透過在lazygit設定檔中新增\n\ngit:\n overrideGpg: true\n\n來啟用此功能。", + "CreateRepo": "不在 git 倉庫中。建立一個新的 git 倉庫嗎?(y/N): ", + "BareRepo": "您已經嘗試在空倉庫中開啟Lazygit,但是Lazygit還不支援空倉庫。開啟最近的倉庫嗎?(y / n) ", + "BareRepoNotSupported": "Lazygit 不支援裸儲存庫。", "InitialBranch": "分支名稱?(留空使用 git 的預設值):", "NoRecentRepositories": "必須在 git 版本庫中開啟 lazygit。沒有有效的最近版本庫。退出。", "IncorrectNotARepository": "無效 `notARepository` 輸入。輸入應為「prompt」、「create」、「skip」、或「quit」。", "AutoStashTitle": "是否自動收藏?", "AutoStashPrompt": "必須收藏並拾起變更才得以繼續操作。是否自動執行?(Enter/Esc)", + "AutoStashForUndo": "正在自動儲藏更改以便撤銷到 %s", + "AutoStashForCheckout": "正在自動儲藏更改以便檢出 %s", + "AutoStashForNewBranch": "正在自動儲藏更改以便建立新分支 %s", + "AutoStashForMovingPatchToIndex": "正在自動儲藏更改以便將自定義補丁從 %s 移動到暫存區", + "AutoStashForCherryPicking": "正在自動儲藏更改以便揀選提交", + "AutoStashForReverting": "正在自動儲藏更改以便還原提交", "Discard": "捨棄", "DiscardChangesTitle": "捨棄變更", "DiscardFileChangesTooltip": "檢視選中變動進行捨棄復原", @@ -273,18 +475,43 @@ "DiscardUntrackedFiles": "刪除未追蹤檔案", "DiscardStagedChanges": "刪除已預存變更", "HardReset": "強制重設", + "BranchDeleteTooltip": "檢視本地/遠端分支的刪除選項。", + "TagDeleteTooltip": "檢視本機/遠端標籤的刪除選項。", "Delete": "刪除", "Reset": "重設", + "ResetTooltip": "檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。", "ViewResetOptions": "檢視重設選項", + "FileResetOptionsTooltip": "檢視工作樹的重置選項(例如:清除工作樹)。", "CreateFixupCommit": "建立修復提交", "CreateFixupCommitTooltip": "為此提交建立修復提交", + "CreateAmendCommit": "建立 'amend!' 提交", + "FixupMenu_Fixup": "修復提交", + "FixupMenu_FixupTooltip": "允許您修復另一個提交併保持原本的提交資訊。", + "FixupMenu_AmendWithChanges": "基於目前變動內容修改提交", + "FixupMenu_AmendWithChangesTooltip": "允許您修復另一個提交併修改提交資訊。", + "FixupMenu_AmendWithoutChanges": "修改提交(不含變動內容,類似reword)", + "FixupMenu_AmendWithoutChangesTooltip": "允許您修改另一個提交的提交訊息而不更改其內容。", "SquashAboveCommitsTooltip": "是否壓縮上方 {{.commit}} 所有「fixup」提交?", + "SquashCommitsAboveSelectedTooltip": "壓縮目前選中提交下的所有修復提交(自動壓縮)。", + "SquashCommitsInCurrentBranchTooltip": "壓縮目前分支中的所有修復提交(自動壓縮)。", "SquashAboveCommits": "壓縮上方所有「fixup」提交(自動壓縮)", + "SquashCommitsInCurrentBranch": "在目前分支", + "SquashCommitsAboveSelectedCommit": "在選定提交之上", + "CannotSquashCommitsInCurrentBranch": "在目前分支中無法壓縮提交:因為分離HEAD提交是一個合併提交或者已經存在於主分支中。", + "ExecuteShellCommand": "執行 Shell 命令", + "ExecuteShellCommandTooltip": "調出可輸入shell命令執行的提示符。", + "ShellCommand": "Shell 命令:", "CommitChangesWithoutHook": "沒有預提交 hook 就提交更改", "ResetTo": "重設至", + "ResetSoftTooltip": "將 HEAD 重置為所選提交,並將目前提交和所選提交之間的更改保留為已暫存更改。", + "ResetMixedTooltip": "將 HEAD 重置為所選提交,並將目前提交和所選提交之間的更改保留為未暫存的更改。", + "ResetHardTooltip": "將 HEAD 重置為所選提交,並丟棄目前提交和所選提交之間的所有更改,以及工作樹中的所有目前修改。", + "ResetHardConfirmation": "您確定要執行硬重置嗎?這將丟棄所有未提交的更改(包括已暫存和未暫存的),且無法撤銷。", "PressEnterToReturn": "按 Enter 返回到 lazygit", "ViewStashOptions": "檢視收藏選項", + "ViewStashOptionsTooltip": "檢視貯藏選項(例如:貯藏所有、貯藏已暫存變更、貯藏未暫存變更)。", "Stash": "收藏", + "StashTooltip": "貯藏所有變更.若要使用其他貯藏變體,請使用檢視貯藏選項快捷鍵。", "StashAllChanges": "收藏所有變更", "StashStagedChanges": "收藏已預存變更", "StashAllChangesKeepIndex": "收藏所有變更並保留預存區", @@ -292,76 +519,127 @@ "StashIncludeUntrackedChanges": "收藏所有變更,包括未追蹤檔案", "StashOptions": "收藏選項", "NotARepository": "錯誤:必須在 git 版本庫中執行", + "WorkingDirectoryDoesNotExist": "錯誤:目前工作目錄不存在", "ScrollLeft": "向左捲動", "ScrollRight": "向右捲動", "DiscardPatch": "捨棄補丁", "DiscardPatchConfirm": "你只能從單一提交或收藏項目建立一個補丁。是否捨棄當前補丁?", "CantPatchWhileRebasingError": "在合併或變基狀態下,你不能建立或運行補丁命令", "ToggleAddToPatch": "切換檔案是否包含在補丁中", + "ToggleAddToPatchTooltip": "切換檔案是否包含在自定義補丁中。請參閱 {{.doc}}。", "ToggleAllInPatch": "切換所有檔案是否包含在補丁中", + "ToggleAllInPatchTooltip": "新增或刪除所有提交中的檔案到自定義的補丁中。請參閱 {{.doc}}。", "UpdatingPatch": "正在更新補丁", "ViewPatchOptions": "檢視自訂補丁選項", "PatchOptionsTitle": "補丁選項", "NoPatchError": "尚未建立補丁。要開始建立補丁,請在提交檔案上使用空格或輸入以添加特定行", + "EmptyPatchError": "補丁還是空的。首先將一些檔案或行新增到您的補丁中。", "EnterCommitFile": "輸入檔案以將選定的行添加至補丁(或切換目錄折疊)", + "EnterCommitFileTooltip": "如果已選擇一個檔案,則Enter進入該檔案,以便您可以向自定義補丁新增/刪除單獨的行。如果選擇了目錄,則切換目錄。", "ExitCustomPatchBuilder": "退出自訂補丁建立器", + "ExitFocusedMainView": "退出回到側邊面板", "EnterUpstream": "輸入遠端為 ' '", "InvalidUpstream": "無效的遠端分支名稱。必須符合 ' ' 的格式", "NewRemote": "新增遠端", "NewRemoteName": "新遠端名稱:", "NewRemoteUrl": "新遠端 URL:", + "AddForkRemote": "新增復刻遠端倉庫", + "AddForkRemoteUsername": "復刻所有者(使用者名稱/組織)。使用 使用者名稱:分支 來檢出分支", + "AddForkRemoteTooltip": "透過替換 origin URL 中的所有者來快速新增復刻遠端倉庫,並可選擇從新遠端倉庫檢出分支。", + "IncompatibleForkAlreadyExistsError": "遠端倉庫 {{.remoteName}} 已存在且 URL 不同", + "NoOriginRemote": "此操作需要 'origin' 遠端倉庫", + "ViewBranches": "檢視分支", "EditRemoteName": "輸入更新 {{.remoteName}} 遠端名稱:", "EditRemoteUrl": "輸入更新 {{.remoteName}} 遠端 URL:", "RemoveRemote": "移除遠端", + "RemoveRemoteTooltip": "刪除選中的遠端。從遠端跟蹤遠端分支的任何本地分支都不會受到影響。", + "RemoveRemotePrompt": "確定要刪除遠端倉庫嗎?", "DeleteRemoteBranch": "刪除遠端分支", + "DeleteRemoteBranches": "刪除原創分支", + "DeleteRemoteBranchTooltip": "從遠端刪除遠端分支。", + "DeleteLocalAndRemoteBranch": "刪除本地和遠端分支", + "DeleteLocalAndRemoteBranches": "刪除本地和遠端分支", "SetAsUpstream": "設置為遠端", "SetAsUpstreamTooltip": "將此分支設為當前分支之遠端", "SetUpstream": "設定選定分支的遠端分支", "UnsetUpstream": "重置選定分支的遠端", "ViewDivergenceFromUpstream": "檢視與遠端的差異", + "ViewDivergenceFromBaseBranch": "檢視主分支({{.baseBranch}})與上游的差異", + "CouldNotDetermineBaseBranch": "無法確定主分支", "DivergenceSectionHeaderLocal": "本地", + "DivergenceSectionHeaderRemote": "遠端", "ViewUpstreamResetOptions": "重設當前分支進 {{.upstream}}", "ViewUpstreamResetOptionsTooltip": "查看重設當前分支進 {{upstream}} 的選項。注意:此動作不會重置所選的遠端,而是將當前分支重置到遠端", "ViewUpstreamRebaseOptions": "將當前分支變基到 {{.upstream}}", "ViewUpstreamRebaseOptionsTooltip": "查看變基當前分支到 {{upstream}} 的選項。注意:此動作不會變基所選的遠端,而是將當前分支變基到遠端", "UpstreamGenericName": "選定分支的選端", "SetUpstreamTitle": "設定遠端分支", + "SetUpstreamMessage": "確定要將'{{.checkedOut}}'的上游分支設定為'{{.selected}}'嗎?", "EditRemoteTooltip": "編輯遠端", "TagCommit": "打標籤到提交", + "TagCommitTooltip": "建立一個新標籤指向所選提交。您可以在彈窗中輸入標籤名稱和描述(可選)。", "TagNameTitle": "標籤名稱", "TagMessageTitle": "標籤訊息", "LightweightTag": "輕量標籤", "AnnotatedTag": "附註標籤", + "DeleteTagTitle": "要刪除 '{{.tagName}}' 標籤?", "DeleteLocalTag": "刪除本地標籤", + "DeleteRemoteTag": "刪除遠端標籤", + "DeleteLocalAndRemoteTag": "刪除本地和遠端標籤", + "SelectRemoteTagUpstream": "被刪除標籤'{{.tagName}}'的遠端:", "DeleteRemoteTagPrompt": "確定要刪除遠端 {{.upstream}} 的標籤 '{{.tagName}}'?", + "DeleteLocalAndRemoteTagPrompt": "確定要從本機和 '{{.upstream}}' 遠端刪除 '{{.tagName}}' 嗎?", + "RemoteTagDeletedMessage": "遠端標籤已刪除", "PushTagTitle": "推送標籤 '{{.tagName}}' 至遠端:", "PushTag": "推送標籤", + "PushTagTooltip": "推送選擇的標籤到遠端。您將在彈窗中選擇一個遠端。", "NewTag": "建立標籤", + "NewTagTooltip": "基於目前提交建立一個新標籤。您將在彈窗中輸入標籤名稱和描述(可選)。", + "CreatingTag": "建立標籤", + "ForceTag": "強制標記標籤", + "ForceTagPrompt": "該標籤‘{{.tagName}}’已存在。請按{{.cancelKey}}取消,或者按{{.confirmKey}}覆蓋它。", "FetchRemoteTooltip": "擷取遠端", + "CheckoutCommitTooltip": "檢出所選擇的提交作為分離HEAD。", + "NoBranchesFoundAtCommitTooltip": "在選定的提交處未找到分支。", "GitFlowOptions": "顯示 git-flow 選項", "NotAGitFlowBranch": "這似乎不是一個 git flow 分支", "NewBranchNamePrompt": "為分支輸入新名稱", "IgnoreTracked": "忽略已追蹤檔案", "ExcludeTracked": "排除已追蹤檔案", "IgnoreTrackedPrompt": "你確定要忽略一個已追蹤的檔案?", + "ExcludeTrackedPrompt": "您確定要排除已跟蹤的檔案嗎?", "ViewResetToUpstreamOptions": "檢視遠端重設選項", "NextScreenMode": "下一個螢幕模式(常規/半螢幕/全螢幕)", "PrevScreenMode": "上一個螢幕模式", + "CycleDiffRenderers": "切換差異渲染器", + "CycleDiffRenderersTooltip": "選擇已設定的差異渲染器清單中的下一個渲染器。", + "CycleDiffRenderersReverse": "切換差異渲染器(反向)", + "CycleDiffRenderersReverseTooltip": "選擇已設定的差異渲染器清單中的上一個渲染器。", + "CycleDiffRenderersDisabledReason": "沒有設定其他差異渲染器", + "SelectedDiffRenderers": "差異渲染器:{{.name}}(第 {{.current}} 個,共 {{.total}} 個)", + "DefaultDiffRendererName": "(預設)", + "ExternalDiffDiffRendererName": "(外部差異)", "StartSearch": "搜尋", "StartFilter": "搜尋", + "SelectRemoteRepository": "為拉取請求選擇基礎倉庫", + "FetchingPullRequests": "正在獲取拉取請求", "Keybindings": "鍵盤快捷鍵", "KeybindingsMenuSectionLocal": "本地", "KeybindingsMenuSectionGlobal": "全域", + "KeybindingsMenuSectionNavigation": "導航", + "KeybindingsTooltip": "快速鍵:", "RenameBranch": "重新命名分支", "Upstream": "遠端", "BranchUpstreamOptionsTitle": "上游遠端設定", "ViewBranchUpstreamOptions": "檢視遠端設定", "ViewBranchUpstreamOptionsTooltip": "檢視有關遠端分支的設定(例如重設至遠端)", "UpstreamNotSetError": "目標分支沒有遠端對應分支(或其遠端分支未儲存於本地)", + "UpstreamsNotSetError": "部分選中分支沒有上游分支(或上游分支未在本地儲存)", "NewGitFlowBranchPrompt": "{{.branchType}} 名稱:", "RenameBranchWarning": "此分支正在追蹤遠端分支。此操作僅會重新命名本地分支名稱,而不是遠端分支的名稱。是否繼續?", "OpenKeybindingsMenu": "開啟選單", "ResetCherryPick": "重設選定的揀選 (複製) 提交", + "ResetCherryPickShort": "重置已複製的提交", "NextTab": "下一個索引標籤", "PrevTab": "上一個索引標籤", "CantUndoWhileRebasing": "在變基時無法復原", @@ -369,6 +647,8 @@ "MustStashWarning": "將補丁提取到索引中需要收藏並取消收藏你的變更。如果出現問題,你可以從收藏中訪問你的檔案。是否繼續?", "MustStashTitle": "必須收藏", "ConfirmationTitle": "確認面板", + "PromptTitle": "輸入提示", + "PromptInputCannotBeEmptyToast": "不允許輸入為空", "PrevPage": "上一頁", "NextPage": "下一頁", "GotoTop": "捲動到頂部", @@ -376,11 +656,15 @@ "FilteringBy": "篩選方式", "ResetInParentheses": "(已重設)", "OpenFilteringMenu": "檢視篩選路徑選項", + "OpenFilteringMenuTooltip": "檢視用於過濾提交日誌的選項,以便僅顯示與過濾器匹配的提交。", "FilterBy": "篩選路徑", "ExitFilterMode": "停止按路徑篩選", "FilterPathOption": "輸入要依路徑篩選的路徑", + "FilterAuthorOption": "輸入作者進行過濾", "EnterFileName": "輸入路徑:", + "EnterAuthor": "輸入作者:", "FilteringMenuTitle": "篩選", + "WillCancelExistingFilterTooltip": "注意:這將取消現有的過濾器", "MustExitFilterModeTitle": "命令不可用", "MustExitFilterModePrompt": "在按路徑篩選的模式下,該命令不可用。是否退出按路徑篩選的模式?", "Diff": "差異", @@ -390,15 +674,27 @@ "DiffingMenuTitle": "差異比較", "SwapDiff": "反轉差異方向", "ViewDiffingOptions": "開啟差異比較選單", + "ViewDiffingOptionsTooltip": "檢視與比較兩個引用相關的選項,例如與選定的 ref 進行比較,輸入要比較的 ref,然後反轉比較方向。", + "CancelDiffingMode": "取消差異比較模式", "OpenCommandLogMenu": "開啟命令記錄選單", + "OpenCommandLogMenuTooltip": "檢視命令日誌的選項,例如顯示/隱藏命令日誌以及聚焦命令日誌。", "ShowingGitDiff": "顯示輸出:", + "ShowingDiffForRange": "顯示範圍差異", "CommitDiff": "提交差異", + "CopyCommitHashToClipboard": "複製縮略提交雜湊值到剪貼簿", "CommitHash": "提交 hash", "CommitURL": "提交 URL", + "PasteCommitMessageFromClipboard": "貼上提交資訊自剪貼簿", + "SurePasteCommitMessage": "貼上將覆蓋目前提交訊息,繼續嗎?", "CommitMessage": "提交訊息", + "CommitMessageBody": "提交資訊正文", + "CommitSubject": "提交主題", "CommitAuthor": "提交者", + "CommitTags": "提交標籤", "CopyCommitAttributeToClipboard": "複製提交屬性", + "CopyCommitAttributeToClipboardTooltip": "複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。", "CopyBranchNameToClipboard": "複製分支名稱到剪貼簿", + "CopyTagToClipboard": "複製標籤到剪貼簿", "CopyPathToClipboard": "複製檔案名稱到剪貼簿", "CommitPrefixPatternError": "commitPrefix 模式錯誤", "CopySelectedTextToClipboard": "複製所選文本至剪貼簿", @@ -409,15 +705,22 @@ "BranchUnknown": "分支未知", "DiscardChangeTitle": "取消預存行", "DiscardChangePrompt": "是否刪除所選行(git reset)?此操作不可逆。\n將「gui.skipDiscardChangeWarning」設為 true 可禁用此警告。", + "DiscardLinesFromCommitTitle": "從提交中丟棄行", + "DiscardLinesFromCommitPrompt": "確定要從此提交中丟棄所選行嗎?", + "DiscardLinesFromCommitPromptWithReset": "確定要從此提交中丟棄所選行嗎?\n\n注意:這將重置活動的自定義補丁!", "CreateNewBranchFromCommit": "從提交建立新分支", "BuildingPatch": "正在建立補丁", "ViewCommits": "檢視提交", + "MinGitVersionError": "Git 版本必須至少為 %s。請升級您的 Git 版本。", "RunningCustomCommandStatus": "正在執行自訂命令", "SubmoduleStashAndReset": "收藏未提交的子模組變更並更新", "AndResetSubmodules": "以及重設子模組", "EnterSubmoduleTooltip": "進入子模組", + "BackToParentRepo": "返回父倉庫", + "Enter": "進入", "CopySubmoduleNameToClipboard": "複製子模組名稱到剪貼簿", "RemoveSubmodule": "移除子模組", + "RemoveSubmoduleTooltip": "刪除選定的子模組及其相應的目錄。", "RemoveSubmodulePrompt": "是否確定要刪除子模組 '%s' 以及它相應的目錄?此操作是不可逆的。", "ResettingSubmoduleStatus": "重設子模型中", "NewSubmoduleName": "子模組名稱:", @@ -430,43 +733,68 @@ "EditSubmoduleUrl": "更新子模組 URL", "InitializingSubmoduleStatus": "正在初始化子模組", "InitSubmoduleTooltip": "初始化子模組", + "Update": "更新", + "Initialize": "初始化", "SubmoduleUpdateTooltip": "更新子模組", "UpdatingSubmoduleStatus": "正在更新子模組", "BulkInitSubmodules": "批量初始化子模組", "BulkUpdateSubmodules": "批量更新子模組", "BulkDeinitSubmodules": "批量解除子模組初始化", + "BulkUpdateRecursiveSubmodules": "批次遞迴初始化並更新子模組", "ViewBulkSubmoduleOptions": "查看批量子模組選項", "BulkSubmoduleOptions": "批量子模組選項", "RunningCommand": "正在執行命令", "SubCommitsTitle": "子提交", + "ExitSubview": "退出子檢視", "SubmodulesTitle": "子模組", "NavigationTitle": "移動", "SuggestionsCheatsheetTitle": "提示", "SuggestionsTitle": "提示(按 %s 進入焦點)", + "SuggestionsSubtitle": "(按 %s 鍵進行刪除, %s 鍵進行編輯)", "ExtrasTitle": "命令記錄", "PullRequestURLCopiedToClipboard": "複製拉取請求 URL 至剪貼簿", "CommitDiffCopiedToClipboard": "已複製提交差異至剪貼簿", "CommitURLCopiedToClipboard": "已複製提交 URL 至剪貼簿", "CommitMessageCopiedToClipboard": "已複製提交訊息至剪貼簿", + "CommitMessageBodyCopiedToClipboard": "提交資訊正文已複製到剪貼簿", + "CommitSubjectCopiedToClipboard": "提交主題已複製到剪貼簿", "CommitAuthorCopiedToClipboard": "已複製提交者至剪貼簿", + "CommitTagsCopiedToClipboard": "提交標籤已複製到剪貼簿", + "CommitHasNoTags": "提交沒有標籤", + "CommitHasNoMessageBody": "提交沒有資訊正文", "PatchCopiedToClipboard": "已複製補丁至剪貼簿", + "MessageCopiedToClipboard": "訊息已複製到剪貼簿", "CopiedToClipboard": "已複製至剪貼簿", "ErrCannotEditDirectory": "無法編輯目錄:你只能編輯單獨的檔案", + "ErrCannotCopyContentOfDirectory": "無法複製目錄內容:只能複製單個檔案的內容", "ErrStageDirWithInlineMergeConflicts": "不能預存/取消預存包含具備內嵌合併衝突的檔案的目錄。請先解決合併衝突", "ErrRepositoryMovedOrDeleted": "找不到版本庫。可能已被移動或刪除", + "ErrWorktreeMovedOrRemoved": "找不到工作樹,它可能被刪除或者移走了。 ¯\\\\_(ツ)_/¯", + "CantSwitchWhileOperationInProgress": "操作進行中時無法切換儲存庫", "CommandLog": "命令記錄", "ToggleShowCommandLog": "切換顯示/隱藏命令記錄", "FocusCommandLog": "聚焦命令記錄", "CommandLogHeader": " '%s' 隱藏/聚焦此面板\n", "RandomTip": "隨機提示", "ToggleWhitespaceInDiffView": "切換是否在差異檢視中顯示空格變更", + "ToggleWhitespaceInDiffViewTooltip": "切換是否在差異檢視中顯示空白字元更改。\n\n預設值可在設定檔中透過鍵 'git.ignoreWhitespaceInDiffView' 更改。", "IgnoreWhitespaceDiffViewSubTitle": "(忽略空格)", "IgnoreWhitespaceNotSupportedHere": "在此檢視中不支援忽略空格", "IncreaseContextInDiffView": "增加差異檢視中顯示變更周圍上下文的大小", + "IncreaseContextInDiffViewTooltip": "增加差異檢視中變更周圍顯示的上下文量。\n\n預設值可在設定檔中透過鍵 'git.diffContextSize' 更改。", "DecreaseContextInDiffView": "減小差異檢視中顯示變更周圍上下文的大小", + "DecreaseContextInDiffViewTooltip": "減少差異檢視中變更周圍顯示的上下文量。\n\n預設值可在設定檔中透過鍵 'git.diffContextSize' 更改。", + "DiffContextSizeChanged": "將diff上下文大小更改為%d", + "IncreaseRenameSimilarityThreshold": "提高重新命名相似度閾值", + "IncreaseRenameSimilarityThresholdTooltip": "提高將刪除和新增對視為重新命名所需的相似度閾值。\n\n預設值可在設定檔中透過鍵 'git.renameSimilarityThreshold' 更改。", + "DecreaseRenameSimilarityThreshold": "降低重新命名相似度閾值", + "DecreaseRenameSimilarityThresholdTooltip": "降低將刪除和新增對視為重新命名所需的相似度閾值。\n\n預設值可在設定檔中透過鍵 'git.renameSimilarityThreshold' 更改。", + "RenameSimilarityThresholdChanged": "已重新命名相似度閾值更改為 %d%%", "CreatePullRequestOptions": "建立拉取請求選項", "DefaultBranch": "預設分支", "SelectBranch": "選擇分支", + "SelectTargetRemote": "選擇目標遠端倉庫", + "NoValidRemoteName": "名為 '%s' 的遠端名稱不存在", "CreatePullRequest": "建立拉取請求", "SelectConfigFile": "選擇設定檔", "NoConfigFileFoundErr": "找不到設定檔", @@ -478,24 +806,34 @@ "AbortTitle": "中止%s", "AbortPrompt": "是否確定要中止當前的%s?", "OpenLogMenu": "開啟記錄選單", + "OpenLogMenuTooltip": "檢視提交日誌的選項,例如更改排序順序、隱藏 git graph、顯示整個 git graph。", "LogMenuTitle": "提交記錄選項", "ToggleShowGitGraphAll": "切換顯示整個 git 圖表(將 `--all` 標誌傳遞給 `git log`)", "ShowGitGraph": "顯示 git 圖表", + "ShowGitGraphTooltip": "在提交日誌中顯示或隱藏 Git 圖形。\n\n預設值可在設定檔中透過鍵 'git.log.showGraph' 更改。", "SortOrder": "排序規則", + "SortOrderPromptLocalBranches": "本地分支的預設排序順序可在設定檔中透過鍵 'git.localBranchSortOrder' 設定。", + "SortOrderPromptRemoteBranches": "遠端分支的預設排序順序可在設定檔中透過鍵 'git.remoteBranchSortOrder' 設定。", "SortAlphabetical": "依字母", "SortByDate": "依時間", "SortByRecency": "依最近使用", "SortBasedOnReflog": "(依據歷史記錄)", "SortCommits": "提交排序順序", + "SortCommitsTooltip": "更改提交日誌中提交的排序順序。\n\n預設值可在設定檔中透過鍵 'git.log.sortOrder' 更改。", "CantChangeContextSizeError": "在製作補丁期間無法更改上下文大小,因為當發布功能時我們太懒了以至於沒有支援它。如果你真的需要它,請告訴我們!", + "CantChangeRenameThresholdError": "處於修補程式建立模式時,無法變更重新命名相似度門檻,因為自訂修補程式無法處理重新命名變成刪除與新增的情況。", "OpenCommitInBrowser": "在瀏覽器中開啟提交", "ViewBisectOptions": "查看二分選項", "ConfirmRevertCommit": "是否還原 {{.selectedCommit}} ?", + "ConfirmRevertCommitRange": "確定要還原選定的提交嗎?", "RewordInEditorTitle": "在編輯器中改寫", "RewordInEditorPrompt": "是否在編輯器中改寫此提交?", + "CheckoutAutostashPrompt": "確定要檢出 '%s' 嗎?必要時將自動儲藏更改。", "HardResetAutostashPrompt": "是否強制重設為 '%s' ?如果需要會進行自動存儲。", + "SoftResetPrompt": "您確定要軟重置到 '%s' 嗎?", "UpstreamGone": "(遠端已經不存在)", "NukeDescription": "如果你想讓所有工作樹上的變更消失,這就是正確的選項。如果有未提交的子模組變更,它們將被收藏在子模組中。", + "NukeTreeConfirmation": "確定要清空工作樹嗎?這將丟棄工作樹中的所有更改(已暫存、未暫存和未跟蹤的),此操作不可撤銷。", "DiscardStagedChangesDescription": "這將創建一個新的存儲條目,其中只包含預存檔案,然後如果存儲條目不需要,將其刪除,因此工作樹僅保留未預存的變更。", "EmptyOutput": "<空輸出>", "Patch": "補丁", @@ -503,29 +841,52 @@ "CommitsCopied": "提交已複製", "CommitCopied": "提交已複製", "ResetPatch": "重設補丁", + "ResetPatchTooltip": "清理目前補丁。", "ApplyPatch": "套用補丁", + "ApplyPatchTooltip": "應用目前補丁到工作樹中。", "ApplyPatchInReverse": "反向套用補丁", + "ApplyPatchInReverseTooltip": "反向應用目前補丁到工作樹中。", "RemovePatchFromOriginalCommit": "從原始提交中刪除補丁(%s)", + "RemovePatchFromOriginalCommitTooltip": "從這些提交中刪除該補丁。這是透過在提交時啟動互動式變基,反向應用補丁,然後繼續變基來實現的。如果之後的提交依賴於補丁,您可能需要解決衝突。", "MovePatchOutIntoIndex": "將補丁移到預存區", + "MovePatchOutIntoIndexTooltip": "將補丁從提交中移出並移入到索引中。這是透過在提交時啟動互動式變基、反向應用補丁、繼續變基直至完成,然後將補丁應用到索引來實現的。如果之後的提交依賴於補丁,您可能需要解決衝突。", + "MovePatchIntoNewCommit": "將補丁移動到原始提交之後的新提交中", + "MovePatchIntoNewCommitTooltip": "將補丁從提交中移出並移至位於原始提交之上的新提交中。這是透過在原始提交處啟動互動式變基,反向應用補丁,然後將補丁應用到索引並將其作為新提交提交,然後繼續變基直至完成來實現的。如果以後的提交依賴於補丁,您可能需要解決衝突。", + "MovePatchIntoNewCommitBefore": "將補丁移動到原始提交之前的新提交中", + "MovePatchIntoNewCommitBeforeTooltip": "將補丁從其提交中移出,並放入原始提交之前的新提交中。當自定義補丁僅包含整個程式碼塊甚至整個檔案時效果最佳;如果包含部分程式碼塊,則很可能出現衝突。", "MovePatchToSelectedCommit": "將補丁移到選定的提交(%s)", + "MovePatchToSelectedCommitTooltip": "將補丁從其原始提交修改到選定的提交中。 實現這一點的方法是在原始提交時啟動互動式重置,反向應用補丁, 然後在應用補丁和修改選定的提交之前,繼續將其重新建立到選定的提交上。 重置將繼續完成。如果原始碼和目的碼提交之間的提交取決於補丁,您可能需要解決衝突。", "CopyPatchToClipboard": "將補丁複製到剪貼簿", + "MustStageFilesAffectedByPatchTitle": "必須暫存檔案", + "MustStageFilesAffectedByPatchWarning": "將補丁應用到索引需要暫存受補丁影響的未暫存檔案。請注意,應用補丁時可能會出現衝突。繼續嗎?", "NoMatchesFor": "沒有找到符合 '%s' %s 的結果", "MatchesFor": "符合 '%s' 的結果(%d/%d)%s", "SearchKeybindings": "%s:下一個結果,%s:上一個結果,%s:退出搜尋模式", "SearchPrefix": "搜尋:", "FilterPrefix": "篩選:", "ExitSearchMode": "%s:退出搜尋模式", + "ExitTextFilterMode": "%s:退出過濾模式", + "Switch": "切換", "SwitchToWorktree": "切換至工作目錄面板", + "SwitchToWorktreeTooltip": "切換到選中的工作樹。", "AlreadyCheckedOutByWorktree": "此分支已被檢出到 {{.worktreeName}} 是否切換到此工作目錄?", "BranchCheckedOutByWorktree": "分支 {{.branchName}} 已被 {{.worktreeName}} 檢出", + "SomeBranchesCheckedOutByWorktreeError": "部分選中分支被其他工作樹檢出。請逐個選擇刪除。", "DetachWorktreeTooltip": "此將在工作目錄中執行 `git checkout --detach` 以解開分支與它的連結,但工作目錄本身將不被更動", "Switching": "切換中", "RemoveWorktree": "刪除工作目錄", "RemoveWorktreeTitle": "刪除工作目錄", + "RemoveWorktreeMenuTitle": "移除工作樹 '{{.worktreeName}}'?", + "RemoveWorktreeAndDeleteBranch": "移除工作樹並刪除分支", + "RemoveWorktreeAndDeleteBothBranches": "移除工作樹並刪除本機與遠端分支", + "WorktreeNotCheckedOutOnBranch": "此工作樹未檢出任何分支", "DetachWorktree": "解開工作目錄連結", + "DetachWorktreeAndDeleteBranch": "中斷工作樹並刪除分支", + "DetachWorktreeAndDeleteBothBranches": "中斷工作樹並刪除本機與遠端分支", "DetachingWorktree": "正在解除工作目錄連結", "WorktreesTitle": "工作目錄", "WorktreeTitle": "工作目錄", + "ForceRemoveWorktreePrompt": "'{{.worktreeName}}' 包含已修改或未跟蹤的檔案,或子模組(或包含所有這些)。確定要移除它嗎?", "RemovingWorktree": "正在刪除工作目錄", "AddingWorktree": "正在建立工作目錄", "CantDeleteCurrentWorktree": "無法刪除當前工作目錄!", @@ -533,53 +894,99 @@ "CantDeleteMainWorktree": "無法刪除主要工作目錄!", "NoWorktreesThisRepo": "無工作目錄", "MissingWorktree": "(失蹤)", + "MainWorktree": "(主工作樹)", + "NewWorktree": "新建工作樹", "NewWorktreePath": "工作目錄路徑", + "RemoveWorktreeTooltip": "刪除選定的工作樹。這將刪除工作樹的目錄以及 .git 目錄中有關工作樹的後設資料。", "NewBranchName": "分支名稱", + "NewWorktreeName": "新增工作樹名稱", + "NewWorktreeForBranchTitle": "為分支新增工作樹", + "NewBranchAndWorktreeName": "新增分支與工作樹名稱", + "NewBranchAndWorktreeFromRef": "從 '{{.ref}}' 建立新分支與工作樹", + "NewLocalBranchAndWorktreeFromRef": "從 '{{.ref}}' 建立新本機分支與工作樹", + "WorktreeForRef": "為 '{{.ref}}' 新增工作樹", + "DetachedWorktreeAtRef": "在 '{{.ref}}' 建立新的分離工作樹", + "WorktreeLocationTitle": "工作樹位置", + "WorktreeLocationOther": "其他…", + "WorktreeLocationPromptNewBranch": "從 '{{.base}}' 建立新分支 '{{.name}}':", + "WorktreeLocationPromptTrackingBranch": "建立追蹤 '{{.ref}}' 的新分支 '{{.name}}':", + "WorktreeLocationPromptCheckout": "分支 '{{.branchName}}' 的工作樹:", + "WorktreeLocationPromptDetached": "在 '{{.ref}}' 建立分離工作樹:", "LcWorktree": "工作目錄", "ChangingDirectoryTo": "切換至 {{.path}}", + "DirenvApprovalTitle": "允許 .envrc?", + "DirenvApprovalPrompt": "按 {{.confirmKey}} 執行 'direnv allow' 並載入環境。\n按 {{.cancelKey}} 跳過。\n\n{{.content}}", "Name": "名稱", "Branch": "分支", "Path": "路徑", "MarkedBaseCommitStatus": "為了變基已標注基準提交", "MarkAsBaseCommit": "為了變基已標注提交為基準提交", "MarkAsBaseCommitTooltip": "請為了下一次變基選擇一項基準提交;此將執行 `git rebase --onto`。", + "CancelMarkedBaseCommit": "取消標記的基礎提交", "MarkedCommitMarker": "↑↑↑ 將由此變基 ↑↑↑", + "FailedToOpenURL": "開啟URL %s 失敗。\n\n錯誤:%v", + "InvalidLazygitEditURL": "無效的lazygit-edit URL格式:%s", "NoCopiedCommits": "未複製提交", "DisabledMenuItemPrefix": "已停用:", "QuickStartInteractiveRebase": "開始互動變基", + "QuickStartInteractiveRebaseTooltip": "為分支上的提交啟動互動式變基。這將包括從 HEAD 提交到第一個合併提交或主分支提交的所有提交。\n如果您想從所選提交啟動互動式變基,請按 `{{.editKey}}`。", + "CannotQuickStartInteractiveRebase": "無法啟動互動式變基:HEAD 提交是合併提交或存在於主分支上,因此沒有適當的主提交來啟動變基。您可以透過選擇提交併按 `{{.editKey}}` 從特定提交啟動互動式變基。", "ToggleRangeSelect": "切換拖曳選擇", + "DismissRangeSelect": "取消範圍選擇", + "RangeSelectUp": "向上擴充套件選擇範圍", + "RangeSelectDown": "向下擴充套件選擇範圍", + "RangeSelectNotSupported": "該操作不支援範圍選擇,請選擇單個專案", + "NoItemSelected": "沒有條目被選中", + "SelectedItemIsNotABranch": "選中的條目不是一個分支", + "SelectedItemDoesNotHaveFiles": "選中的條目中沒有", + "MultiSelectNotSupportedForSubmodules": "子模組不支援多選操作", + "NothingToStageForSubmodule": "沒有可暫存的內容:父儲存庫只能暫存新的子模組提交,無法暫存子模組內尚未提交的變更。請先在子模組內提交。", + "CommandDoesNotSupportOpeningInEditor": "該命令不支援切換到編輯器", + "CustomCommands": "自定義命令", + "NoApplicableCommandsInThisContext": "(目前上下文無可用命令)", + "SelectCommitsOfCurrentBranch": "選擇目前分支的提交", "Actions": { "CheckoutCommit": "檢出提交", + "CheckoutBranchAtCommit": "檢出分支 '%s'", + "CheckoutCommitAsDetachedHead": "將提交 %s 檢出為分離 HEAD", "CheckoutTag": "檢出標籤", "CheckoutBranch": "檢出分支", + "CheckoutBranchOrCommit": "檢出分支或提交", "ForceCheckoutBranch": "強制檢出分支", "DeleteLocalBranch": "刪除本地分支", "Merge": "合併", + "SquashMerge": "壓縮合併", "RebaseBranch": "變基分支", "RenameBranch": "重新命名分支", "CreateBranch": "建立分支", "FastForwardBranch": "快進分支", + "AutoForwardBranches": "自動快轉分支", "CherryPick": "(Cherry-pick)複製提交", "CheckoutFile": "檢出檔案", "SquashCommitDown": "下列次方執行 Squash", "FixupCommit": "修復提交", + "FixupCommitKeepMessage": "Fixup 提交(保留訊息)", "RewordCommit": "改寫提交", "DropCommit": "捨棄提交", "EditCommit": "編輯提交", "AmendCommit": "修改提交", "ResetCommitAuthor": "重設提交作者", "SetCommitAuthor": "設置提交作者", + "AddCommitCoAuthor": "新增提交共同作者", "RevertCommit": "還原提交", "CreateFixupCommit": "建立修改提交", "SquashAllAboveFixupCommits": "Squash 所有上面的修改提交", "MoveCommitUp": "上移提交", "MoveCommitDown": "下移提交", "CopyCommitMessageToClipboard": "將提交訊息複製到剪貼簿", + "CopyCommitMessageBodyToClipboard": "複製提交訊息內文到剪貼簿", + "CopyCommitSubjectToClipboard": "複製提交主旨到剪貼簿", "CopyCommitDiffToClipboard": "將提交差異複製到剪貼簿", "CopyCommitHashToClipboard": "將提交 hash 複製到剪貼簿", "CopyCommitURLToClipboard": "將提交 URL 複製到剪貼簿", "CopyCommitAuthorToClipboard": "將提交作者複製到剪貼簿", "CopyCommitAttributeToClipboard": "複製到剪貼簿", + "CopyCommitTagsToClipboard": "複製提交標籤到剪貼簿", "CopyPatchToClipboard": "將補丁複製到剪貼簿", "CustomCommand": "自定義命令", "DiscardAllChangesInFile": "捨棄檔案中的所有更改", @@ -589,6 +996,14 @@ "UnstageFile": "取消預存檔案", "UnstageAllFiles": "取消預存所有檔案", "StageAllFiles": "預存所有檔案", + "ResolveConflictByKeepingFile": "保留檔案以解決衝突", + "ResolveConflictByDeletingFile": "刪除檔案以解決衝突", + "TakeCurrentSubmoduleCommit": "採用目前提交以解決子模組衝突", + "TakeIncomingSubmoduleCommit": "採用傳入提交以解決子模組衝突", + "NotEnoughContextToStage": "差異內容大小為 0 時無法暫存或取消暫存變更。請使用 '%s' 增加內容。", + "NotEnoughContextToDiscard": "差異內容大小為 0 時無法捨棄變更。請使用 '%s' 增加內容。", + "NotEnoughContextToRemoveLines": "差異內容大小為 0 時無法從提交移除行。請使用 '%s' 增加內容。", + "NotEnoughContextForCustomPatch": "差異內容大小為 0 時無法建立自訂修補程式。請使用 '%s' 增加內容。", "IgnoreExcludeFile": "忽略或排除檔案", "IgnoreFileErr": "無法忽略 .gitignore 檔案", "ExcludeFile": "排除檔案", @@ -613,10 +1028,14 @@ "DeleteRemoteBranch": "刪除遠端分支", "SetBranchUpstream": "設置遠端分支", "AddRemote": "添加遠端", + "AddForkRemote": "新增 fork 遠端", "RemoveRemote": "移除遠端", "UpdateRemote": "更新遠端", "ApplyPatch": "套用補丁", "Stash": "收藏 (Stash)", + "PopStash": "彈出儲藏", + "ApplyStash": "套用儲藏", + "DropStash": "捨棄儲藏", "RenameStash": "重命名暫存", "RemoveSubmodule": "移除子模塊", "ResetSubmodule": "重設子模塊", @@ -626,10 +1045,12 @@ "BulkInitialiseSubmodules": "批量初始化子模塊", "BulkUpdateSubmodules": "批量更新子模塊", "BulkDeinitialiseSubmodules": "批量取消初始化子模塊", + "BulkUpdateRecursiveSubmodules": "批次遞迴初始化並更新子模組", "UpdateSubmodule": "更新子模塊", "CreateLightweightTag": "建立輕量標籤", "CreateAnnotatedTag": "建立附註標籤", "DeleteLocalTag": "刪除本地標籤", + "DeleteRemoteTag": "刪除遠端標籤", "PushTag": "推送標籤", "NukeWorkingTree": "清空工作樹", "DiscardUnstagedFileChanges": "放棄未預存的檔案更改", @@ -647,23 +1068,57 @@ "StartBisect": "開始二分查找", "ResetBisect": "重設二分查找", "BisectSkip": "二分查找跳過", - "BisectMark": "二分查找標記" + "BisectMark": "二分查找標記", + "AddWorktree": "新增工作樹" }, "Bisect": { "MarkStart": "將 %s 標記為 %s(開始二分查找)", "ResetTitle": "重設 `git bisect`", "ResetPrompt": "是否重設 `git bisect`?", "ResetOption": "重設二分查找", + "ChooseTerms": "選擇二分搜尋術語", + "OldTermPrompt": "舊/良好提交的術語:", + "NewTermPrompt": "新/有問題提交的術語:", "BisectMenuTitle": "二分查找", "Mark": "將 %s 標記為 %s", "SkipCurrent": "跳過 %s", + "SkipSelected": "略過所選提交(%s)", "CompleteTitle": "二分查找完成", "CompletePrompt": "二分查找完成!以下提交引入了更改:\n\n%s\n\n是否重設 `git bisect` ?", "CompletePromptIndeterminate": "二分查找完成!有一些提交被跳過,因此以下任何提交皆可能引進更改:\n\n%s\n\n是否重設 `git bisect`?", "Bisecting": "二分查找中" }, "Log": { - "CopyToClipboard": "{{.str}} 已複製" + "EditRebase": "開始在 '{{.ref}}' 進行互動式 rebase", + "HandleUndo": "正在復原上一次衝突解決", + "RemoveFile": "正在刪除路徑 '{{.path}}'", + "RemoveEmptyDir": "正在刪除空目錄 '{{.path}}'", + "CopyToClipboard": "{{.str}} 已複製", + "Remove": "正在移除 '{{.filename}}'", + "CreateFileWithContent": "正在建立檔案 '{{.path}}'", + "AppendingLineToFile": "正在將 '{{.line}}' 附加至檔案 '{{.filename}}'", + "EditRebaseFromBaseCommit": "從 '{{.baseCommit}}' 開始,對 '{{.targetBranchName}}' 進行互動式 rebase", + "DroppingStash": "正在刪除儲藏 %s", + "PoppingStash": "正在彈出儲藏 %s", + "DeletingBranch": "正在刪除分支 '{{.branchName}}'(原為 {{.hash}})" }, - "BreakingChangesByVersion": {} + "BreakingChangesTitle": "重大變化", + "BreakingChangesMessage": "你正在將 lazygit 更新至包含破壞性變更的新版本。請閱讀下列說明,並視需要更新設定。\n如需更多資訊,請查看完整的發行說明:.", + "BreakingChangesByVersion": { + "0.41.0": "- 按 'g' 開啟 git reset 選單時,'mixed' 選項現在是第一個也是預設選項,取代 'soft'。這是因為 'mixed' 最常使用。\n- 提交訊息面板現在預設會自動硬換行(也就是到達邊界時加上換行字元)。可如下調整設定:\n\ngit:\n commit:\n autoWrapCommitMessage: true\n autoWrapWidth: 72\n\n- 'v' 鍵原本只能在暫存檢視中用於開始範圍選取,現在任何檢視皆可使用。可惜這會和用來貼上提交(cherry-pick)的 'v' 快速鍵衝突,因此現在改用 'shift+V' 貼上提交;為求一致,複製提交也改用 'shift+C',不再只是 'c'。請注意,'v' 快速鍵只是開始範圍選取的方法之一:也可使用 shift+向上/向下箭頭。如果想設定 cherry-pick 快速鍵來恢復舊行為,請在設定中加入:\n\nkeybinding:\n universal:\n toggleRangeSelect: \n commits:\n cherryPickCopy: 'c'\n pasteCommits: 'v'\n\n- 使用 'shift-S' 壓縮 fixup 現在會開啟選單;預設選項是壓縮分支中的所有 fixup 提交。原本只壓縮所選提交上方 fixup 提交的行為,仍可在該選單中以第二個選項使用。\n- push/pull/fetch 的載入狀態現在顯示在分支旁,而非彈出式視窗中。這讓你可以同時 fetch 多個分支,並查看每個分支的狀態。\n- 提交檢視中的 git 日誌圖現在預設一律顯示(之前只有在檢視最大化時才顯示)。若覺得太雜亂,可透過 ctrl+L -> 'Show git graph' -> 'when maximised' 改回原設定。\n- 在遠端分支按空白鍵,原本會顯示提示,要求輸入要從遠端分支檢出之新本機分支的名稱。現在會直接檢出遠端分支,讓你選擇建立同名的新本機分支,或使用分離的 HEAD。舊行為仍可透過 'n' 快速鍵使用。\n- 篩選(例如按 '/')現在預設模糊程度較低;它只會比對子字串。可用空白分隔多個子字串來比對。若想恢復舊行為,請在設定中加入:\n\ngui:\n filterMode: 'fuzzy'\n", + "0.44.0": "- gui.branchColors 設定選項已棄用,將在未來版本移除。請改用 gui.branchColorPatterns。\n- 以 \"feature/\"、\"bugfix/\" 或 \"hotfix/\" 開頭的分支不再自動著色;若需要此功能,可透過新的 gui.branchColorPatterns 選項輕鬆設定。", + "0.49.0": "- 執行 shell 指令(使用 ':' 提示)不再使用互動式 shell;因此若想在此提示中使用 shell 別名,需要進行一些設定。詳情請見 https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#using-aliases-or-functions-in-shell-commands", + "0.50.0": "- fetch 後,若主分支落後其 upstream,現在會自動快轉至 upstream。這有助於讓 main 或 master 分支自動保持最新。若不需要此功能,可在設定中加入:\n\ngit:\n autoForwardBranches: none\n\n反之,若連 feature 分支也要使用此功能,可改設為 'allBranches'。", + "0.51.0": "- 自訂指令的 'subprocess'、'stream' 與 'showOutput' 欄位已由單一 'output' 欄位取代。這應可無縫處理;若曾在設定檔使用這些欄位,應已自動為你更新。不過有一項明顯變更:'stream' 欄位原本同時表示指令輸出會串流至指令日誌,以及指令會在虛擬終端機(pty)中執行。我們將其轉換為 'output: log',代表指令輸出會串流至指令日誌,但不會使用 pty,因為大多數人需要的應是這種行為。若確實要在 pty 中執行指令,可改用 'output: logWithPty'。", + "0.54.0": "- 本機與遠端分支的預設排序順序已變更:本機分支原為 'recency'(以 reflog 為依據),遠端分支原為 'alphabetical'。兩者現在都改為 'date'(即 committerdate)。若較喜歡舊預設值,可透過以下設定還原:\n\ngit:\n localBranchSortOrder: recency\n remoteBranchSortOrder: alphabetical\n\n- 暫存檢視與自訂修補程式建立檢視的預設選取模式已改為區塊模式。在大多數情況下這更實用,通常可省下許多按鍵操作。若要改回舊的行模式預設值,可在設定中加入:\n\ngui:\n useHunkModeInStagingView: false\n", + "0.55.0": "- 原本綁定 ctrl-z 的 'redo' 指令現在改綁定 shift-Z。這是因為 ctrl-z 現在用於暫停應用程式;在 Linux 世界中這是廣為人知的快速鍵。若想還原此變更,可在設定中加入:\n\nkeybinding:\n universal:\n suspendApp: \n redo: \n\n- 'git.paging.useConfig' 選項已移除。若原先依賴它設定 pager,必須改用 'git.diffRenderers.*.command' 選項明確設定指令。", + "0.62.0": "- 從提交說明編輯器送出提交的預設快速鍵,已從 alt-enter 改為 Mac 上的 command-enter,或 Linux 與 Windows 上的 ctrl-enter;這些也是許多多行編輯欄位(例如 GitHub 留言)使用的相同快速鍵。可惜不是所有終端機都支援它們;詳情請見 https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md#terminal-compatibility\n若想還原此變更,可在設定中加入:\n\nkeybinding:\n universal:\n confirmInEditor: [, ]\n" + }, + "ViewMergeConflictOptions": "檢視合併衝突選項", + "ViewMergeConflictOptionsTooltip": "檢視用於解決合併衝突的選項。", + "NoFilesWithMergeConflicts": "沒有存在合併衝突的檔案。", + "MergeConflictOptionsTitle": "解決合併衝突", + "UseCurrentChanges": "使用目前更改", + "UseIncomingChanges": "使用傳入的更改", + "UseBothChanges": "兩者都用" } diff --git a/pkg/integration/clients/cli.go b/pkg/integration/clients/cli.go index 34a5f85bd..b8f4bc5cc 100644 --- a/pkg/integration/clients/cli.go +++ b/pkg/integration/clients/cli.go @@ -8,9 +8,9 @@ import ( "strconv" "strings" - "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/tests" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -54,7 +54,7 @@ func runAndPrintFatalError(test *components.IntegrationTest, f func() error) { } func getTestsToRun(testNames []string) []*components.IntegrationTest { - allIntegrationTests := tests.GetTests(utils.GetLazyRootDirectory()) + allIntegrationTests := tests.GetTests(utils.MustFindLazygitRootDirectory()) var testsToRun []*components.IntegrationTest if len(testNames) == 0 { diff --git a/pkg/integration/clients/go_test.go b/pkg/integration/clients/go_test.go index 4c1faa557..30e08bcd8 100644 --- a/pkg/integration/clients/go_test.go +++ b/pkg/integration/clients/go_test.go @@ -16,9 +16,9 @@ import ( "time" "github.com/creack/pty" - "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/tests" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/stretchr/testify/assert" ) @@ -37,8 +37,13 @@ func TestIntegration(t *testing.T) { codeCoverageDir := os.Getenv("LAZYGIT_GOCOVERDIR") testNumber := 0 - err := components.RunTests(components.RunTestArgs{ - Tests: tests.GetTests(utils.GetLazyRootDirectory()), + rootDir, err := utils.FindLazygitRootDirectory() + if err != nil { + t.Fatal(err) + } + + err = components.RunTests(components.RunTestArgs{ + Tests: tests.GetTests(rootDir), Logf: t.Logf, RunCmd: runCmdHeadless, TestWrapper: func(test *components.IntegrationTest, f func() error) { diff --git a/pkg/integration/clients/tui.go b/pkg/integration/clients/tui.go index 0f07b5b19..aa344cb27 100644 --- a/pkg/integration/clients/tui.go +++ b/pkg/integration/clients/tui.go @@ -9,12 +9,13 @@ import ( "path/filepath" "strings" - "github.com/jesseduffield/lazycore/pkg/utils" + lazycoreUtils "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/tests" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -23,7 +24,7 @@ import ( var SLOW_INPUT_DELAY = 600 func RunTUI(raceDetector bool) { - rootDir := utils.GetLazyRootDirectory() + rootDir := utils.MustFindLazygitRootDirectory() testDir := filepath.Join(rootDir, "test", "integration") app := newApp(testDir) @@ -206,7 +207,7 @@ type app struct { } func newApp(testDir string) *app { - return &app{testDir: testDir, allTests: tests.GetTests(utils.GetLazyRootDirectory())} + return &app{testDir: testDir, allTests: tests.GetTests(utils.MustFindLazygitRootDirectory())} } func (self *app) getCurrentTest() *components.IntegrationTest { @@ -224,7 +225,7 @@ func (self *app) loadTests() { } func (self *app) adjustCursor() { - self.itemIdx = utils.Clamp(self.itemIdx, 0, len(self.filteredTests)-1) + self.itemIdx = lazycoreUtils.Clamp(self.itemIdx, 0, len(self.filteredTests)-1) } func (self *app) filterWithString(needle string) { diff --git a/pkg/integration/components/env.go b/pkg/integration/components/env.go index 6306a88ba..39152092e 100644 --- a/pkg/integration/components/env.go +++ b/pkg/integration/components/env.go @@ -3,6 +3,8 @@ package components import ( "fmt" "os" + + "github.com/samber/lo" ) const ( @@ -43,11 +45,9 @@ var hostEnvironmentAllowlist = [...]string{ // Returns a copy of the environment filtered by // hostEnvironmentAllowlist func allowedHostEnvironment() []string { - env := []string{} - for _, envVar := range hostEnvironmentAllowlist { - env = append(env, fmt.Sprintf("%s=%s", envVar, os.Getenv(envVar))) - } - return env + return lo.Map(hostEnvironmentAllowlist[:], func(envVar string, _ int) string { + return fmt.Sprintf("%s=%s", envVar, os.Getenv(envVar)) + }) } func NewTestEnvironment(rootDir string) []string { diff --git a/pkg/integration/components/menu_driver.go b/pkg/integration/components/menu_driver.go index 95f29dcd3..e0d6133e6 100644 --- a/pkg/integration/components/menu_driver.go +++ b/pkg/integration/components/menu_driver.go @@ -56,8 +56,12 @@ func (self *MenuDriver) ContainsLines(matchers ...*TextMatcher) *MenuDriver { return self } +// types the text into the menu's filter row. Only for menus that filter as you +// type; other menus are filtered through the search prompt. func (self *MenuDriver) Filter(text string) *MenuDriver { - self.getViewDriver().FilterOrSearch(text) + self.getViewDriver().IsFocused() + self.t.typeContent(text) + self.t.Views().MenuFilter().IsVisible() return self } diff --git a/pkg/integration/components/runner.go b/pkg/integration/components/runner.go index aaaabd0a0..6e686b17b 100644 --- a/pkg/integration/components/runner.go +++ b/pkg/integration/components/runner.go @@ -9,7 +9,6 @@ import ( "sync" "time" - lazycoreUtils "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/utils" @@ -40,12 +39,15 @@ type RunTestArgs struct { // showing what's actually happening during the test, but it's still good at running // tests in telling you about their results. func RunTests(args RunTestArgs) error { - projectRootDir := lazycoreUtils.GetLazyRootDirectory() - err := os.Chdir(projectRootDir) + projectRootDir, err := utils.FindLazygitRootDirectory() if err != nil { return err } + if err := os.Chdir(projectRootDir); err != nil { + return err + } + testDir := filepath.Join(projectRootDir, "test", "_results") if err := buildLazygit(args); err != nil { return err @@ -256,14 +258,15 @@ func getLazygitCommand( return nil, err } - cmdArgs := []string{tempLazygitPath(), "-debug", "--use-config-dir=" + paths.Config()} - resolvedExtraArgs := lo.Map(test.ExtraCmdArgs(), func(arg string, _ int) string { return utils.ResolvePlaceholderString(arg, map[string]string{ "actualPath": paths.Actual(), "actualRepoPath": paths.ActualRepo(), }) }) + + cmdArgs := make([]string, 0, 3+len(resolvedExtraArgs)) + cmdArgs = append(cmdArgs, tempLazygitPath(), "-debug", "--use-config-dir="+paths.Config()) cmdArgs = append(cmdArgs, resolvedExtraArgs...) // Use a limited environment for test isolation, including pass through diff --git a/pkg/integration/components/test_driver.go b/pkg/integration/components/test_driver.go index d65caee5d..bd5bbfc24 100644 --- a/pkg/integration/components/test_driver.go +++ b/pkg/integration/components/test_driver.go @@ -78,6 +78,12 @@ func (self *TestDriver) repeatMouseMove() { self.mouseMove(self.mouseX, self.mouseY) } +func (self *TestDriver) scrollWheelDown(x, y int) { + self.SetCaption(fmt.Sprintf("Scrolling down at %d, %d", x, y)) + self.gui.ScrollWheelDown(x, y) + self.Wait(self.inputDelay) +} + func (self *TestDriver) mouseRelease() { self.SetCaption(fmt.Sprintf("Releasing mouse at %d, %d", self.mouseX, self.mouseY)) self.gui.MouseRelease(self.mouseX, self.mouseY) @@ -91,6 +97,24 @@ func (self *TestDriver) GlobalPress(key config.Keybinding) { self.press(key[0]) } +// asserts that the terminal's text cursor is shown, i.e. that there is a text +// field to type into +func (self *TestDriver) CursorIsVisible() *TestDriver { + self.assertWithRetries(func() (bool, string) { + return self.gui.CursorVisible(), "Expected the cursor to be visible" + }) + + return self +} + +func (self *TestDriver) CursorIsHidden() *TestDriver { + self.assertWithRetries(func() (bool, string) { + return !self.gui.CursorVisible(), "Expected the cursor to be hidden" + }) + + return self +} + // FocusIn simulates the terminal window regaining focus, which causes lazygit // to reload any config files that changed while it was in the background. func (self *TestDriver) FocusIn() { @@ -136,6 +160,15 @@ func (self *TestDriver) Log(message string) { self.gui.LogUI(message) } +// RefreshInBackground performs the refresh that lazygit's background routines +// perform on a timer, e.g. to pick up changes made by RunCommand. Tests use this +// rather than turning those routines on and waiting for them. +func (self *TestDriver) RefreshInBackground() { + self.SetCaption("Refreshing in the background") + self.gui.RefreshInBackground() + self.Wait(self.inputDelay) +} + // allows the user to run shell commands during the test to emulate background activity func (self *TestDriver) Shell() *Shell { return self.shell diff --git a/pkg/integration/components/test_test.go b/pkg/integration/components/test_test.go index cf0338dec..3b608e698 100644 --- a/pkg/integration/components/test_test.go +++ b/pkg/integration/components/test_test.go @@ -5,13 +5,13 @@ import ( "path/filepath" "testing" - lazycoreUtils "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/stretchr/testify/assert" ) @@ -28,6 +28,9 @@ type fakeGuiDriver struct { heldCoordinates []coordinate movedCoordinates []coordinate releasedCoordinates []coordinate + scrolledCoordinates []coordinate + onUIThread bool + onUIThreadCallCount int } var _ integrationTypes.GuiDriver = &fakeGuiDriver{} @@ -56,8 +59,18 @@ func (self *fakeGuiDriver) MouseRelease(x, y int) { self.releasedCoordinates = append(self.releasedCoordinates, coordinate{x: x, y: y}) } +func (self *fakeGuiDriver) ScrollWheelDown(x, y int) { + self.scrolledCoordinates = append(self.scrolledCoordinates, coordinate{x: x, y: y}) +} + +func (self *fakeGuiDriver) RefreshInBackground() { +} + func (self *fakeGuiDriver) OnUIThreadAndWait(f func()) { + self.onUIThreadCallCount++ + self.onUIThread = true f() + self.onUIThread = false } func (self *fakeGuiDriver) FocusIn() { @@ -75,6 +88,10 @@ func (self *fakeGuiDriver) CurrentContext() types.Context { return nil } +func (self *fakeGuiDriver) CursorVisible() bool { + return false +} + func (self *fakeGuiDriver) ContextForView(viewName string) types.Context { return nil } @@ -161,6 +178,42 @@ func TestSuccess(t *testing.T) { assert.Equal(t, "", driver.failureMessage) } +func TestViewDriverPointerCoordinates(t *testing.T) { + guiDriver := &fakeGuiDriver{} + testDriver := NewTestDriver(guiDriver, nil, config.KeybindingConfig{}, 0) + view := gocui.NewView("source", 10, 20, 30, 31, gocui.OutputNormal) + targetView := gocui.NewView("target", 40, 50, 60, 61, gocui.OutputNormal) + viewDriver := &ViewDriver{ + getView: func() *gocui.View { + assert.True(t, guiDriver.onUIThread) + return view + }, + t: testDriver, + } + targetViewDriver := &ViewDriver{ + getView: func() *gocui.View { + assert.True(t, guiDriver.onUIThread) + return targetView + }, + t: testDriver, + } + + viewDriver. + Click(1, 2). + FocusInAndClick(3, 4). + ClickAndHold(5, 6). + MouseMove(7, 8). + MouseMoveToBottom(9). + MouseMoveToView(targetViewDriver, 10, 11). + ScrollWheelDown() + + assert.Equal(t, []coordinate{{12, 23}, {14, 25}}, guiDriver.clickedCoordinates) + assert.Equal(t, []coordinate{{16, 27}}, guiDriver.heldCoordinates) + assert.Equal(t, []coordinate{{18, 29}, {20, 30}, {51, 62}}, guiDriver.movedCoordinates) + assert.Equal(t, []coordinate{{11, 21}}, guiDriver.scrolledCoordinates) + assert.Equal(t, 7, guiDriver.onUIThreadCallCount) +} + func TestFailingFixture(t *testing.T) { test := NewIntegrationTest(NewIntegrationTestArgs{ Description: unitTestDescription, @@ -174,7 +227,12 @@ func TestFailingFixture(t *testing.T) { paths := NewPaths(t.TempDir()) assert.NoError(t, os.MkdirAll(paths.ActualRepo(), 0o777)) - workingDir, err := createFixture(test, paths, lazycoreUtils.GetLazyRootDirectory()) + rootDir, err := utils.FindLazygitRootDirectory() + if err != nil { + t.Fatal(err) + } + + workingDir, err := createFixture(test, paths, rootDir) assert.ErrorContains(t, err, "git checkout no-such-branch") assert.Empty(t, workingDir) diff --git a/pkg/integration/components/view_driver.go b/pkg/integration/components/view_driver.go index 937c19b7b..dfae58c4b 100644 --- a/pkg/integration/components/view_driver.go +++ b/pkg/integration/components/view_driver.go @@ -41,6 +41,53 @@ func (self *ViewDriver) Title(expected *TextMatcher) *ViewDriver { return self } +// asserts that the view has the expected footer, i.e. the "x of y" text on its +// bottom border +func (self *ViewDriver) Footer(expected *TextMatcher) *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + actual := self.getView().Footer + return expected.context(fmt.Sprintf("%s footer", self.context)).test(actual) + }) + + return self +} + +// asserts that the view has the expected subtitle +func (self *ViewDriver) Subtitle(expected *TextMatcher) *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + actual := self.getView().Subtitle + return expected.context(fmt.Sprintf("%s subtitle", self.context)).test(actual) + }) + + return self +} + +// asserts that the view hangs off the bottom of the given one, sharing a border +// with it +func (self *ViewDriver) SharesTopBorderWithBottomOf(upper *ViewDriver) *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + _, _, _, upperY1 := upper.getView().Dimensions() + _, y0, _, _ := self.getView().Dimensions() + return y0 == upperY1, fmt.Sprintf( + "%s: Expected view to start on row %d, where the view above it ends, but it starts on row %d", + self.context, upperY1, y0) + }) + + return self +} + +// asserts that the view starts on the row below the given one +func (self *ViewDriver) IsImmediatelyBelow(upper *ViewDriver) *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + _, _, _, upperY1 := upper.getView().Dimensions() + _, y0, _, _ := self.getView().Dimensions() + return y0 == upperY1+1, fmt.Sprintf( + "%s: Expected view to start on row %d, but it starts on row %d", self.context, upperY1+1, y0) + }) + + return self +} + func (self *ViewDriver) Clear() *ViewDriver { // clearing multiple times in case there's multiple lines // (the clear button only clears a single line at a time) @@ -313,6 +360,42 @@ func (self *ViewDriver) Content(matcher *TextMatcher) *ViewDriver { return self } +// SelectionIsActive asserts that the view draws its selection as the one the user +// is working in. These three assertions read the highlight flags rather than the +// selected lines, which say nothing about whether the selection is drawn at all. +func (self *ViewDriver) SelectionIsActive() *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + view := self.getView() + ok := view.Highlight && !view.HighlightInactive + return ok, fmt.Sprintf("%s: expected an active selection to be shown, but it wasn't", self.context) + }) + + return self +} + +// SelectionIsInactive asserts that the view draws its selection dimmed, as a panel +// does while the focus is somewhere else. +func (self *ViewDriver) SelectionIsInactive() *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + view := self.getView() + ok := view.Highlight && view.HighlightInactive + return ok, fmt.Sprintf("%s: expected an inactive selection to be shown, but it wasn't", self.context) + }) + + return self +} + +// SelectionIsHidden asserts that the view draws no selection at all, e.g. a list +// with nothing in it, where there is nothing to select. +func (self *ViewDriver) SelectionIsHidden() *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + ok := !self.getView().Highlight + return ok, fmt.Sprintf("%s: expected no selection to be shown, but one was", self.context) + }) + + return self +} + // asserts on the selected line of the view. If you are selecting a range, // you should use the SelectedLines method instead. func (self *ViewDriver) SelectedLine(matcher *TextMatcher) *ViewDriver { @@ -355,6 +438,31 @@ func (self *ViewDriver) SelectedLineIdxAtLeast(expected int) *ViewDriver { return self } +// asserts on the scroll position of the view, i.e. the index of the line that +// is shown at the top of the view. +func (self *ViewDriver) OriginY(expected int) *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + actual := self.getView().OriginY() + return expected == actual, fmt.Sprintf("%s: Expected origin Y to be %d, got %d", self.context, expected, actual) + }) + + return self +} + +// asserts that the selected line is inside the visible area of the view +func (self *ViewDriver) SelectedLineIsVisible() *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + view := self.getView() + firstVisible, lastVisible := view.OriginY(), view.OriginY()+view.InnerHeight()-1 + actual := view.SelectedLineIdx() + return actual >= firstVisible && actual <= lastVisible, + fmt.Sprintf("%s: Expected the selected line (%d) to be visible, but only lines %d to %d are", + self.context, actual, firstVisible, lastVisible) + }) + + return self +} + func (self *ViewDriver) OriginYAtLeast(expected int) *ViewDriver { self.t.assertEventually(func() (bool, string) { var actual int @@ -492,7 +600,7 @@ func (self *ViewDriver) PressRapidly(keys ...config.Keybinding) *ViewDriver { } func (self *ViewDriver) Click(x, y int) *ViewDriver { - offsetX, offsetY, _, _ := self.getView().Dimensions() + offsetX, offsetY, _ := self.viewGeometry() self.t.click(offsetX+1+x, offsetY+1+y) @@ -500,7 +608,7 @@ func (self *ViewDriver) Click(x, y int) *ViewDriver { } func (self *ViewDriver) FocusInAndClick(x, y int) *ViewDriver { - offsetX, offsetY, _, _ := self.getView().Dimensions() + offsetX, offsetY, _ := self.viewGeometry() self.t.focusInAndClick(offsetX+1+x, offsetY+1+y) @@ -508,7 +616,7 @@ func (self *ViewDriver) FocusInAndClick(x, y int) *ViewDriver { } func (self *ViewDriver) MouseMoveToView(target *ViewDriver, x, y int) *ViewDriver { - offsetX, offsetY, _, _ := target.getView().Dimensions() + offsetX, offsetY, _ := target.viewGeometry() self.t.mouseMove(offsetX+1+x, offsetY+1+y) return self } @@ -518,19 +626,40 @@ func (self *ViewDriver) Drag(fromX, fromY, toX, toY int) *ViewDriver { } func (self *ViewDriver) ClickAndHold(x, y int) *ViewDriver { - offsetX, offsetY, _, _ := self.getView().Dimensions() + offsetX, offsetY, _ := self.viewGeometry() self.t.clickAndHold(offsetX+1+x, offsetY+1+y) return self } func (self *ViewDriver) MouseMove(x, y int) *ViewDriver { - offsetX, offsetY, _, _ := self.getView().Dimensions() + offsetX, offsetY, _ := self.viewGeometry() self.t.mouseMove(offsetX+1+x, offsetY+1+y) return self } func (self *ViewDriver) MouseMoveToBottom(x int) *ViewDriver { - return self.MouseMove(x, self.getView().InnerHeight()-1) + offsetX, offsetY, innerHeight := self.viewGeometry() + self.t.mouseMove(offsetX+1+x, offsetY+innerHeight) + return self +} + +// scrolls the view down by one notch of the mouse wheel, i.e. by +// gui.scrollHeight lines. This moves the scroll position without moving the +// selection. +func (self *ViewDriver) ScrollWheelDown() *ViewDriver { + offsetX, offsetY, _ := self.viewGeometry() + self.t.scrollWheelDown(offsetX+1, offsetY+1) + return self +} + +func (self *ViewDriver) viewGeometry() (offsetX int, offsetY int, innerHeight int) { + self.t.gui.OnUIThreadAndWait(func() { + view := self.getView() + offsetX, offsetY, _, _ = view.Dimensions() + innerHeight = view.InnerHeight() + }) + + return offsetX, offsetY, innerHeight } func (self *ViewDriver) RepeatMouseMove() *ViewDriver { diff --git a/pkg/integration/components/views.go b/pkg/integration/components/views.go index 90795d942..5c91b6937 100644 --- a/pkg/integration/components/views.go +++ b/pkg/integration/components/views.go @@ -124,6 +124,14 @@ func (self *Views) Menu() *ViewDriver { return self.regularView("menu") } +func (self *Views) MenuFilter() *ViewDriver { + return self.regularView("menuFilter") +} + +func (self *Views) MenuFilterFrame() *ViewDriver { + return self.regularView("menuFilterFrame") +} + func (self *Views) Confirmation() *ViewDriver { return self.regularView("confirmation") } diff --git a/pkg/integration/tests/branch/rebase_and_drop.go b/pkg/integration/tests/branch/rebase_and_drop.go index 2e8ef5c39..bbb8abf00 100644 --- a/pkg/integration/tests/branch/rebase_and_drop.go +++ b/pkg/integration/tests/branch/rebase_and_drop.go @@ -54,15 +54,15 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{ Focus(). TopLines( Contains("─── Pending rebase todos"), - MatchesRegexp(`pick.*to keep`).IsSelected(), + MatchesRegexp(`pick.*to keep`), MatchesRegexp(`pick.*to remove`), - MatchesRegexp(`pick.*CONFLICT.*first change`), + MatchesRegexp(`pick.*CONFLICT.*first change`).IsSelected(), Contains("─── Commits"), MatchesRegexp("second-change-branch unrelated change"), MatchesRegexp("second change"), MatchesRegexp("original"), ). - SelectNextItem(). + NavigateToLine(Contains("to remove")). Press(keys.Universal.Remove). TopLines( Contains("─── Pending rebase todos"), diff --git a/pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go b/pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go index 1b95fd316..29db1a212 100644 --- a/pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go +++ b/pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go @@ -75,8 +75,8 @@ var RebaseConflictsFixBuildErrorsWithOutOfDateSubmodule = NewIntegrationTest(New t.Views().Files(). Lines( - Equals("▼ /").IsSelected(), - Equals(" MM file"), + Equals("▼ /"), + Equals(" MM file").IsSelected(), Equals(" M submodule (submodule)"), Equals(" ?? untracked-file"), ) @@ -90,8 +90,8 @@ var RebaseConflictsFixBuildErrorsWithOutOfDateSubmodule = NewIntegrationTest(New t.Views().Files(). Lines( - Equals("▼ /").IsSelected(), - Equals(" M submodule (submodule)"), + Equals("▼ /"), + Equals(" M submodule (submodule)").IsSelected(), Equals(" ?? untracked-file"), ) diff --git a/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go b/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go index 7468f921c..abdfcae82 100644 --- a/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go +++ b/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go @@ -79,10 +79,9 @@ var CherryPickConflicts = NewIntegrationTest(NewIntegrationTestArgs{ Focus(). TopLines( Contains("second-change-branch unrelated change"), - Contains("second change"), - Contains("first change").IsSelected(), + Contains("second change").IsSelected(), + Contains("first change"), ). - SelectPreviousItem(). Tap(func() { // because we picked 'Second change' when resolving the conflict, // we now see this commit as having replaced First Change with Second Change, diff --git a/pkg/integration/tests/commit/amend_when_there_are_conflicts_and_amend.go b/pkg/integration/tests/commit/amend_when_there_are_conflicts_and_amend.go index 7b8bf1ca1..8ef3368d6 100644 --- a/pkg/integration/tests/commit/amend_when_there_are_conflicts_and_amend.go +++ b/pkg/integration/tests/commit/amend_when_there_are_conflicts_and_amend.go @@ -31,7 +31,7 @@ var AmendWhenThereAreConflictsAndAmend = NewIntegrationTest(NewIntegrationTestAr Lines( Contains("─── Pending rebase todos"), Contains("pick").Contains("commit three"), - Contains("pick").Contains("<-- CONFLICT --- file1 changed in branch"), + Contains("pick").Contains("<-- CONFLICT --- file1 changed in branch").IsSelected(), Contains("─── Commits"), Contains("commit two"), Contains("file1 changed in master"), diff --git a/pkg/integration/tests/commit/amend_when_there_are_conflicts_and_cancel.go b/pkg/integration/tests/commit/amend_when_there_are_conflicts_and_cancel.go index 91eff7d59..fe7c67ddf 100644 --- a/pkg/integration/tests/commit/amend_when_there_are_conflicts_and_cancel.go +++ b/pkg/integration/tests/commit/amend_when_there_are_conflicts_and_cancel.go @@ -35,7 +35,7 @@ var AmendWhenThereAreConflictsAndCancel = NewIntegrationTest(NewIntegrationTestA Lines( Contains("─── Pending rebase todos"), Contains("pick").Contains("commit three"), - Contains("pick").Contains("<-- CONFLICT --- file1 changed in branch"), + Contains("pick").Contains("<-- CONFLICT --- file1 changed in branch").IsSelected(), Contains("─── Commits"), Contains("commit two"), Contains("file1 changed in master"), diff --git a/pkg/integration/tests/commit/directory_diff_with_renamed_files.go b/pkg/integration/tests/commit/directory_diff_with_renamed_files.go new file mode 100644 index 000000000..f849056f7 --- /dev/null +++ b/pkg/integration/tests/commit/directory_diff_with_renamed_files.go @@ -0,0 +1,90 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DirectoryDiffWithRenamedFiles = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Selecting a directory in the commit files panel shows the renames of files that were moved into or out of it", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateDir("dir") + shell.CreateDir("dir/nested") + shell.CreateFileAndAdd("file1", "file1 content\n") + shell.CreateFileAndAdd("dir/file2", "file2 content\n") + shell.CreateFileAndAdd("dir/nested/file3", "file3 content\n") + shell.Commit("initial commit") + shell.RenameFileInGit("file1", "dir/file1") + shell.RenameFileInGit("dir/file2", "dir/file2-renamed") + shell.RenameFileInGit("dir/nested/file3", "file3") + shell.Commit("move files") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("move files").IsSelected(), + Contains("initial commit"), + ). + PressEnter() + + t.Views().CommitFiles(). + IsFocused(). + Lines( + Equals("▼ /").IsSelected(), + Equals(" ▼ dir"), + Equals(" R file1 → file1"), + Equals(" R file2 → file2-renamed"), + Equals(" R dir/nested/file3 → file3"), + ) + + t.Views().Main().ContainsLines( + Equals("diff --git a/file1 b/dir/file1"), + Equals("similarity index 100%"), + Equals("rename from file1"), + Equals("rename to dir/file1"), + Equals("diff --git a/dir/file2 b/dir/file2-renamed"), + Equals("similarity index 100%"), + Equals("rename from dir/file2"), + Equals("rename to dir/file2-renamed"), + Equals("diff --git a/dir/nested/file3 b/file3"), + Equals("similarity index 100%"), + Equals("rename from dir/nested/file3"), + Equals("rename to file3"), + ) + + t.Views().CommitFiles(). + SelectNextItem(). + SelectedLine(Equals(" ▼ dir")) + + t.Views().Main(). + ContainsLines( + Equals("diff --git a/file1 b/dir/file1"), + Equals("similarity index 100%"), + Equals("rename from file1"), + Equals("rename to dir/file1"), + Equals("diff --git a/dir/file2 b/dir/file2-renamed"), + Equals("similarity index 100%"), + Equals("rename from dir/file2"), + Equals("rename to dir/file2-renamed"), + Equals("diff --git a/dir/nested/file3 b/file3"), + Equals("similarity index 100%"), + Equals("rename from dir/nested/file3"), + Equals("rename to file3"), + ) + + t.Views().CommitFiles(). + SelectNextItem(). + SelectedLine(Equals(" R file1 → file1")) + + t.Views().Main().ContainsLines( + Equals("diff --git a/file1 b/dir/file1"), + Equals("similarity index 100%"), + Equals("rename from file1"), + Equals("rename to dir/file1"), + ) + }, +}) diff --git a/pkg/integration/tests/commit/revert_with_conflict_multiple_commits.go b/pkg/integration/tests/commit/revert_with_conflict_multiple_commits.go index 6e25a8496..5ad60aad5 100644 --- a/pkg/integration/tests/commit/revert_with_conflict_multiple_commits.go +++ b/pkg/integration/tests/commit/revert_with_conflict_multiple_commits.go @@ -46,7 +46,7 @@ var RevertWithConflictMultipleCommits = NewIntegrationTest(NewIntegrationTestArg Lines( Contains("─── Pending reverts"), Contains("revert").Contains("CI unrelated change"), - Contains("revert").Contains("CI <-- CONFLICT --- add first line"), + Contains("revert").Contains("CI <-- CONFLICT --- add first line").IsSelected(), Contains("─── Commits"), Contains("CI ○ add second line"), Contains("CI ○ add first line"), diff --git a/pkg/integration/tests/commit/revert_with_conflict_single_commit.go b/pkg/integration/tests/commit/revert_with_conflict_single_commit.go index 57679160d..374b40338 100644 --- a/pkg/integration/tests/commit/revert_with_conflict_single_commit.go +++ b/pkg/integration/tests/commit/revert_with_conflict_single_commit.go @@ -40,7 +40,7 @@ var RevertWithConflictSingleCommit = NewIntegrationTest(NewIntegrationTestArgs{ }). Lines( Contains("─── Pending reverts"), - Contains("revert").Contains("CI <-- CONFLICT --- add first line"), + Contains("revert").Contains("CI <-- CONFLICT --- add first line").IsSelected(), Contains("─── Commits"), Contains("CI ○ add second line"), Contains("CI ○ add first line"), diff --git a/pkg/integration/tests/commit/shared.go b/pkg/integration/tests/commit/shared.go index e143eb7a0..c1a66c13e 100644 --- a/pkg/integration/tests/commit/shared.go +++ b/pkg/integration/tests/commit/shared.go @@ -45,7 +45,7 @@ func doTheRebaseForAmendTests(t *TestDriver, keys config.KeybindingConfig) { Lines( Contains("─── Pending rebase todos"), Contains("pick").Contains("commit three"), - Contains("pick").Contains("<-- CONFLICT --- file1 changed in branch"), + Contains("pick").Contains("<-- CONFLICT --- file1 changed in branch").IsSelected(), Contains("─── Commits"), Contains("commit two"), Contains("file1 changed in master"), diff --git a/pkg/integration/tests/conflicts/resolve_multiple_files.go b/pkg/integration/tests/conflicts/resolve_multiple_files.go index 5a8f9447e..66d0f8ae4 100644 --- a/pkg/integration/tests/conflicts/resolve_multiple_files.go +++ b/pkg/integration/tests/conflicts/resolve_multiple_files.go @@ -7,7 +7,7 @@ import ( ) var ResolveMultipleFiles = NewIntegrationTest(NewIntegrationTestArgs{ - Description: "Ensures that upon resolving conflicts for one file, the next file is selected", + Description: "Ensures that a file whose conflicts have been resolved keeps being shown while other files still have conflicts", ExtraCmdArgs: []string{}, Skip: false, SetupConfig: func(config *config.AppConfig) {}, @@ -34,25 +34,40 @@ var ResolveMultipleFiles = NewIntegrationTest(NewIntegrationTestArgs{ Contains("First Change"), Contains("======="), ). + SelectNextItem(). PressPrimaryAction() + // The resolved file is still shown, and stays selected so that its diff + // can be reviewed t.Views().Files(). IsFocused(). Lines( - Equals("UU file2").IsSelected(), + Equals("▼ /"), + Equals(" M file1").IsSelected(), + Equals(" UU file2"), ). + SelectNextItem(). PressEnter() // coincidentally these files have the same conflict t.Views().MergeConflicts(). IsFocused(). SelectedLines( - Contains("<<<<<<< HEAD"), - Contains("First Change"), Contains("======="), + Contains("Second Change"), + Contains(">>>>>>>"), ). PressPrimaryAction() + // Now that all conflicts are resolved, the filter is turned off again + t.Views().Files(). + Lines( + Equals("▼ /"), + Equals(" M file1"), + Equals(" M file2").IsSelected(), + Equals(" A file3"), + ) + t.Common().ContinueOnConflictsResolved("merge") }, }) diff --git a/pkg/integration/tests/file/directory_diff_with_renamed_files.go b/pkg/integration/tests/file/directory_diff_with_renamed_files.go new file mode 100644 index 000000000..18906bf03 --- /dev/null +++ b/pkg/integration/tests/file/directory_diff_with_renamed_files.go @@ -0,0 +1,86 @@ +package file + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DirectoryDiffWithRenamedFiles = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Selecting a directory in the files panel shows the renames of files that were moved into or out of it", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateDir("dir") + shell.CreateDir("dir/nested") + shell.CreateFileAndAdd("file1", "file1 content\n") + shell.CreateFileAndAdd("dir/file2", "file2 content\n") + shell.CreateFileAndAdd("dir/nested/file3", "file3 content\n") + shell.Commit("initial commit") + shell.RenameFileInGit("file1", "dir/file1") + shell.RenameFileInGit("dir/file2", "dir/file2-renamed") + shell.RenameFileInGit("dir/nested/file3", "file3") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Equals("▼ /").IsSelected(), + Equals(" ▼ dir"), + Equals(" R file1 → file1"), + Equals(" R file2 → file2-renamed"), + Equals(" R dir/nested/file3 → file3"), + ) + + t.Views().Main().ContainsLines( + Equals("diff --git a/file1 b/dir/file1"), + Equals("similarity index 100%"), + Equals("rename from file1"), + Equals("rename to dir/file1"), + Equals("diff --git a/dir/file2 b/dir/file2-renamed"), + Equals("similarity index 100%"), + Equals("rename from dir/file2"), + Equals("rename to dir/file2-renamed"), + Equals("diff --git a/dir/nested/file3 b/file3"), + Equals("similarity index 100%"), + Equals("rename from dir/nested/file3"), + Equals("rename to file3"), + ) + + t.Views().Files(). + SelectNextItem(). + SelectedLine(Equals(" ▼ dir")) + + t.Views().Main(). + ContainsLines( + Equals("diff --git a/file1 b/dir/file1"), + Equals("similarity index 100%"), + Equals("rename from file1"), + Equals("rename to dir/file1"), + Equals("diff --git a/dir/file2 b/dir/file2-renamed"), + Equals("similarity index 100%"), + Equals("rename from dir/file2"), + Equals("rename to dir/file2-renamed"), + Equals("diff --git a/dir/nested/file3 b/file3"), + Equals("similarity index 100%"), + Equals("rename from dir/nested/file3"), + Equals("rename to file3"), + ) + + // The same applies when a filter reduces the directory to a single file + t.Views().Files(). + FilterOrSearch("file1"). + Lines( + Equals("▼ dir").IsSelected(), + Equals(" R file1 → file1"), + ) + + t.Views().Main(). + ContainsLines( + Equals("diff --git a/file1 b/dir/file1"), + Equals("similarity index 100%"), + Equals("rename from file1"), + Equals("rename to dir/file1"), + ) + }, +}) diff --git a/pkg/integration/tests/file/discard_various_changes_range_select.go b/pkg/integration/tests/file/discard_various_changes_range_select.go index 16ecedd04..2199f1278 100644 --- a/pkg/integration/tests/file/discard_various_changes_range_select.go +++ b/pkg/integration/tests/file/discard_various_changes_range_select.go @@ -46,12 +46,12 @@ var DiscardVariousChangesRangeSelect = NewIntegrationTest(NewIntegrationTestArgs Cancel() }). Lines( - Equals("▼ /").IsSelected(), + Equals("▼ /"), Equals(" AM added-changed.txt"), Equals(" MD change-delete.txt"), Equals(" D delete-change.txt"), Equals(" D deleted-staged.txt"), - Equals(" D deleted.txt"), + Equals(" D deleted.txt").IsSelected(), Equals(" MM double-modded.txt"), Equals(" M modded-staged.txt"), Equals(" M modded.txt"), @@ -59,6 +59,7 @@ var DiscardVariousChangesRangeSelect = NewIntegrationTest(NewIntegrationTestArgs Equals(" ?? new.txt"), Equals(" R renamed.txt → renamed2.txt"), ). + NavigateToLine(Equals("▼ /")). Press(keys.Universal.ToggleRangeSelect). NavigateToLine(Contains("renamed.txt")). Press(keys.Universal.Remove). diff --git a/pkg/integration/tests/file/stage_all_without_changed_files.go b/pkg/integration/tests/file/stage_all_without_changed_files.go new file mode 100644 index 000000000..bae54dffe --- /dev/null +++ b/pkg/integration/tests/file/stage_all_without_changed_files.go @@ -0,0 +1,25 @@ +package file + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var StageAllWithoutChangedFiles = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Pressing the stage-all key when there are no changed files says that there are none", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("first commit") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + IsEmpty(). + Press(keys.Files.ToggleStagedAll). + Tap(func() { + t.ExpectToast(Contains("No changed files")) + }) + }, +}) diff --git a/pkg/integration/tests/filter_and_search/filter_menu_as_you_type.go b/pkg/integration/tests/filter_and_search/filter_menu_as_you_type.go new file mode 100644 index 000000000..13b39e5eb --- /dev/null +++ b/pkg/integration/tests/filter_and_search/filter_menu_as_you_type.go @@ -0,0 +1,94 @@ +package filter_and_search + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FilterMenuAsYouType = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Filtering a menu by typing into the filter row that appears as you type", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) {}, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files().IsFocused().Press(keys.Universal.OptionMenu) + + // The menu offers the filter, but stays as it is until we take it up on it + t.Views().Menu(). + IsFocused(). + Subtitle(Equals("(Type to filter)")). + Footer(Contains(" of ")) + t.Views().MenuFilter().IsInvisible() + t.Views().MenuFilterFrame().IsInvisible() + t.CursorIsHidden() + + t.ExpectPopup().Menu().Filter("whitespace") + + t.Views().Menu(). + Lines( + Contains("─── Global"), + Contains("Toggle whitespace").IsSelected(), + ). + // the row covers the border the footer was on, so it moves there + Subtitle(Equals("")). + Footer(Equals("")) + t.Views().MenuFilterFrame(). + IsVisible(). + Content(Equals("Filter ('@' for keybindings): ")). + Footer(Equals("1 of 1")). + SharesTopBorderWithBottomOf(t.Views().Menu()) + t.Views().MenuFilter().IsVisible().Content(Equals("whitespace")) + t.Views().Tooltip(). + IsVisible(). + Content(Contains("Toggle whether or not whitespace changes are shown")). + IsImmediatelyBelow(t.Views().MenuFilterFrame()) + t.CursorIsVisible() + + // Emptying the filter shows all the items again, and keeps the row + t.GlobalPress(config.Keybinding{""}) + t.Views().MenuFilter().IsVisible().Content(Equals("")) + t.Views().Menu().LineCount(GreaterThan(2)) + t.CursorIsVisible() + + // Moving the text cursor within the filter leaves the menu's selection alone + t.ExpectPopup().Menu().Filter("co") + t.Views().Menu().LineCount(GreaterThan(2)) + t.GlobalPress(config.Keybinding{""}) + t.Views().Menu().SelectedLineIdxAtLeast(2) + t.GlobalPress(config.Keybinding{""}) + t.Views().Menu().SelectedLineIdxAtLeast(2) + t.GlobalPress(config.Keybinding{""}) + + // Clicking an item selects it and leaves the filter where it is + t.Views().Menu().Click(0, 1).SelectedLineIdx(1) + t.GlobalPress(config.Keybinding{"m"}) + t.Views().MenuFilter().Content(Equals("com")) + + t.GlobalPress(config.Keybinding{""}) + + // Escape gives up the filter, keeping the item that was selected + t.ExpectPopup().Menu().Filter("whitespace") + t.Views().Menu().SelectedLine(Contains("Toggle whitespace")) + t.GlobalPress(keys.Universal.Return) + t.Views().Menu(). + IsFocused(). + SelectedLine(Contains("Toggle whitespace")). + Subtitle(Equals("(Type to filter)")). + Footer(Contains(" of ")) + t.Views().MenuFilter().IsInvisible() + t.Views().MenuFilterFrame().IsInvisible() + t.CursorIsHidden() + + // The next escape closes the menu + t.GlobalPress(keys.Universal.Return) + t.Views().Files().IsFocused() + + // A menu opened afterwards starts with no filter + t.Views().Files().Press(keys.Universal.OptionMenu) + t.ExpectPopup().Menu(). + Title(Equals("Keybindings")). + LineCount(GreaterThan(2)). + Cancel() + }, +}) diff --git a/pkg/integration/tests/filter_and_search/filter_menu_key_handling.go b/pkg/integration/tests/filter_and_search/filter_menu_key_handling.go new file mode 100644 index 000000000..7ab25cbb8 --- /dev/null +++ b/pkg/integration/tests/filter_and_search/filter_menu_key_handling.go @@ -0,0 +1,71 @@ +package filter_and_search + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FilterMenuKeyHandling = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Which keys drive a menu that filters as you type, and which ones are filter text", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + // so that quitting is observable instead of ending the test + cfg.GetUserConfig().ConfirmOnQuit = true + }, + SetupRepo: func(shell *Shell) {}, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + // presses a key that is expected to move the selection away from the first + // item, and one that is expected to bring it back + navigates := func(forward string, back string) { + t.GlobalPress(config.Keybinding{forward}) + t.Views().Menu().SelectedLineIdxAtLeast(2) + t.GlobalPress(config.Keybinding{back}) + t.Views().Menu().SelectedLineIdx(1) + } + + t.Views().Files().IsFocused().Press(keys.Universal.OptionMenu) + t.Views().Menu().IsFocused().SelectedLineIdx(1) + + // Until there is a filter, the configured navigation keys drive the menu, + // printable or not + navigates("", "") + navigates("j", "k") + navigates(".", ",") + navigates(">", "<") + t.Views().MenuFilter().IsInvisible() + + // A menu item's own key is filter text; it doesn't execute the item. 'c' + // commits when the files view has the focus. + t.ExpectPopup().Menu().Filter("c") + t.Views().Menu().IsFocused() + t.Views().MenuFilter().Content(Equals("c")) + + // So is the key that filters other lists + t.GlobalPress(keys.Universal.StartSearch) + t.Views().MenuFilter().Content(Equals("c/")) + t.Views().Search().IsInvisible() + + // And so are the printable navigation keys, now that there is somewhere for + // them to go + t.GlobalPress(config.Keybinding{""}) + t.GlobalPress(config.Keybinding{"j"}) + t.GlobalPress(config.Keybinding{"."}) + t.GlobalPress(config.Keybinding{">"}) + t.Views().MenuFilter().Content(Equals("j.>")) + + // The keys that can't be typed keep driving the menu + t.GlobalPress(config.Keybinding{""}) + navigates("", "") + navigates("", "") + navigates("", "") + + // Keys that the filter doesn't take and the menu doesn't handle reach the + // global keybindings + t.GlobalPress(config.Keybinding{""}) + t.ExpectPopup().Confirmation(). + Title(Equals("")). + Content(Contains("Are you sure you want to quit?")). + Confirm() + }, +}) diff --git a/pkg/integration/tests/filter_and_search/filter_menu_with_printable_keybindings.go b/pkg/integration/tests/filter_and_search/filter_menu_with_printable_keybindings.go new file mode 100644 index 000000000..991aa12e2 --- /dev/null +++ b/pkg/integration/tests/filter_and_search/filter_menu_with_printable_keybindings.go @@ -0,0 +1,66 @@ +package filter_and_search + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FilterMenuWithPrintableKeybindings = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Driving a menu that filters as you type when every key configured for it is printable", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Keybinding.Universal.ConfirmMenu = config.Keybinding{"x"} + cfg.GetUserConfig().Keybinding.Universal.Return = config.Keybinding{"q"} + cfg.GetUserConfig().Keybinding.Universal.PrevItem = config.Keybinding{"k"} + cfg.GetUserConfig().Keybinding.Universal.NextItem = config.Keybinding{"j"} + cfg.GetUserConfig().Keybinding.Universal.PrevPage = config.Keybinding{"u"} + cfg.GetUserConfig().Keybinding.Universal.NextPage = config.Keybinding{"d"} + cfg.GetUserConfig().Keybinding.Universal.GotoTop = config.Keybinding{"g"} + cfg.GetUserConfig().Keybinding.Universal.GotoBottom = config.Keybinding{"G"} + }, + SetupRepo: func(shell *Shell) {}, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + navigates := func(forward string, back string) { + t.GlobalPress(config.Keybinding{forward}) + t.Views().Menu().SelectedLineIdxAtLeast(2) + t.GlobalPress(config.Keybinding{back}) + t.Views().Menu().SelectedLineIdx(1) + } + + t.Views().Files().IsFocused().Press(keys.Universal.OptionMenu) + t.Views().Menu().IsFocused().SelectedLineIdx(1) + + // Until there is a filter, the configured keys drive the menu + navigates("j", "k") + navigates("d", "u") + navigates("G", "g") + + // Once there is one, they are all filter text. It takes a key that isn't a + // navigation key to get there. + t.ExpectPopup().Menu().Filter("a") + t.GlobalPress(config.Keybinding{"j"}) + t.GlobalPress(config.Keybinding{"k"}) + t.GlobalPress(config.Keybinding{"d"}) + t.GlobalPress(config.Keybinding{"u"}) + t.Views().MenuFilter().Content(Equals("ajkdu")) + t.GlobalPress(config.Keybinding{""}) + + // The menu is still navigable, because the physical keys drive it whatever + // the configuration says + navigates("", "") + navigates("", "") + navigates("", "") + + // And so are confirming and cancelling. Escape gives up the filter first. + t.ExpectPopup().Menu().Filter("Toggle whitespace") + t.GlobalPress(config.Keybinding{""}) + t.Views().MenuFilter().IsInvisible() + t.Views().Menu().IsFocused().SelectedLine(Contains("Toggle whitespace")) + + t.ExpectPopup().Menu().Filter("Toggle whitespace") + t.Views().Menu().SelectedLine(Contains("Toggle whitespace")) + t.GlobalPress(config.Keybinding{""}) + t.Views().Files().IsFocused() + }, +}) diff --git a/pkg/integration/tests/filter_and_search/filter_worktrees.go b/pkg/integration/tests/filter_and_search/filter_worktrees.go new file mode 100644 index 000000000..77dbcc744 --- /dev/null +++ b/pkg/integration/tests/filter_and_search/filter_worktrees.go @@ -0,0 +1,35 @@ +package filter_and_search + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FilterWorktrees = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Filtering worktrees by branch name", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("initial commit") + shell.NewBranch("branch-aaa") + shell.NewBranch("branch-xxx") + shell.Checkout("master") + shell.AddWorktreeCheckout("branch-aaa", "../worktree-xxx") + shell.AddWorktreeCheckout("branch-xxx", "../worktree-1") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Worktrees(). + Focus(). + Lines( + Contains("(main worktree)").IsSelected(), + Contains("worktree-1 branch-xxx"), + Contains("worktree-xxx branch-aaa"), + ). + FilterOrSearch("xxx"). + Lines( + Contains("worktree-1 branch-xxx"), + Contains("worktree-xxx branch-aaa"), + ) + }, +}) diff --git a/pkg/integration/tests/filter_and_search/rerender_the_searched_main_view.go b/pkg/integration/tests/filter_and_search/rerender_the_searched_main_view.go new file mode 100644 index 000000000..3d631e650 --- /dev/null +++ b/pkg/integration/tests/filter_and_search/rerender_the_searched_main_view.go @@ -0,0 +1,37 @@ +package filter_and_search + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var RerenderTheSearchedMainView = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "A refresh renders the focused main view again even while it is being searched", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n") + shell.Commit("one") + + shell.UpdateFile("file1", "one\nNEEDLE\nthree\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + FilterOrSearch("NEEDLE"). + Content(Contains("+NEEDLE")). + Tap(func() { + t.Shell().UpdateFile("file1", "one\nOTHER\nthree\n") + }). + Press(keys.Universal.Refresh). + Content(Contains("+OTHER")). + Content(DoesNotContain("+NEEDLE")) + + t.Views().Search().Content(Contains("No matches for 'NEEDLE'")) + }, +}) diff --git a/pkg/integration/tests/filter_and_search/search_a_long_diff.go b/pkg/integration/tests/filter_and_search/search_a_long_diff.go new file mode 100644 index 000000000..7f2ebf156 --- /dev/null +++ b/pkg/integration/tests/filter_and_search/search_a_long_diff.go @@ -0,0 +1,66 @@ +package filter_and_search + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +// longFileWithThreeMatches is long enough that a render of its diff stops well short +// of the end, with two of the three matches for the search below the point it stops at. +func longFileWithThreeMatches() string { + lines := make([]string, 0, 2000) + for i := range 2000 { + switch i { + case 100: + lines = append(lines, "NEEDLE first") + case 1000: + lines = append(lines, "NEEDLE middle") + case 1900: + lines = append(lines, "NEEDLE last") + default: + lines = append(lines, fmt.Sprintf("line %d", i)) + } + } + return strings.Join(lines, "\n") + "\n" +} + +var SearchALongDiff = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Search a diff that is longer than a single render of it reads", + ExtraCmdArgs: []string{}, + Skip: false, + // A small window, so that a render stops well short of 2000 lines. + Width: 120, + Height: 30, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "") + shell.Commit("one") + + shell.UpdateFile("file1", longFileWithThreeMatches()) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.FocusMainView) + + // All three matches are counted: opening the prompt reads the whole diff + // first, however much of it the render had got to. + t.Views().Main(). + IsFocused(). + FilterOrSearch("NEEDLE") + + t.Views().Search().Content(Contains("matches for 'NEEDLE' (1 of 3)")) + + // Rendering the diff again reads it from the start, and it is read all the + // way down to the matches the search already knows about. + t.Views().Main(). + Press(keys.Universal.IncreaseContextInDiffView). + Tap(func() { + t.ExpectToast(Equals("Changed diff context size to 4")) + }). + Content(Contains("+NEEDLE last")) + }, +}) diff --git a/pkg/integration/tests/filter_and_search/search_status_after_a_rerender.go b/pkg/integration/tests/filter_and_search/search_status_after_a_rerender.go new file mode 100644 index 000000000..8116049b9 --- /dev/null +++ b/pkg/integration/tests/filter_and_search/search_status_after_a_rerender.go @@ -0,0 +1,45 @@ +package filter_and_search + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var SearchStatusAfterARerender = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "The search status counts the matches in a diff that has been rendered again", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", + "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nNEEDLE\nline 11\nline 12\nline 13\nline 14\n") + shell.Commit("one") + + // Four lines above NEEDLE, so that it is context at a context size of 4 but + // not at 3. + shell.UpdateFile("file1", + "line 1\nline 2\nline 3\nline 4\nline 5\nchanged\nline 7\nline 8\nline 9\nNEEDLE\nline 11\nline 12\nline 13\nline 14\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + Content(DoesNotContain("NEEDLE")). + FilterOrSearch("NEEDLE") + + t.Views().Search().Content(Contains("No matches for 'NEEDLE'")) + + // A wider context brings NEEDLE into the diff, and the search counts it. + t.Views().Main(). + Press(keys.Universal.IncreaseContextInDiffView). + Tap(func() { + t.ExpectToast(Equals("Changed diff context size to 4")) + }). + Content(Contains("NEEDLE")) + + t.Views().Search().Content(Contains("matches for 'NEEDLE' (1 of 1)")) + }, +}) diff --git a/pkg/integration/tests/interactive_rebase/amend_commit_with_conflict.go b/pkg/integration/tests/interactive_rebase/amend_commit_with_conflict.go index 497db50d7..ade48d389 100644 --- a/pkg/integration/tests/interactive_rebase/amend_commit_with_conflict.go +++ b/pkg/integration/tests/interactive_rebase/amend_commit_with_conflict.go @@ -36,7 +36,7 @@ var AmendCommitWithConflict = NewIntegrationTest(NewIntegrationTestArgs{ Lines( Contains("─── Pending rebase todos"), Contains("pick").Contains("three"), - Contains("fixup").Contains("<-- CONFLICT --- fixup! two"), + Contains("fixup").Contains("<-- CONFLICT --- fixup! two").IsSelected(), Contains("─── Commits"), Contains("two"), Contains("one"), @@ -69,7 +69,7 @@ var AmendCommitWithConflict = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Commits(). Lines( Contains("─── Pending rebase todos"), - Contains("<-- CONFLICT --- three"), + Contains("<-- CONFLICT --- three").IsSelected(), Contains("─── Commits"), Contains("two"), Contains("one"), diff --git a/pkg/integration/tests/interactive_rebase/drag_to_reorder_with_autoscroll.go b/pkg/integration/tests/interactive_rebase/drag_to_reorder_with_autoscroll.go index d8086e0a3..fdfdb8bc8 100644 --- a/pkg/integration/tests/interactive_rebase/drag_to_reorder_with_autoscroll.go +++ b/pkg/integration/tests/interactive_rebase/drag_to_reorder_with_autoscroll.go @@ -19,14 +19,22 @@ var DragToReorderWithAutoscroll = NewIntegrationTest(NewIntegrationTestArgs{ TopLines( Contains("commit-40").IsSelected(), ). + // Click and hold the first commit ClickAndHold(1, 0). + // Move the mouse to the bottom of the panel to trigger autoscroll MouseMoveToBottom(1). + // Verify that the view scrolls + OriginYAtLeast(3). + // Move the mouse back into the viewport + MouseMove(1, 1). + // This keeps the scroll as it was OriginYAtLeast(3). MouseRelease(). SelectedLines( Contains("commit-40"), ). SelectedLineIdxAtLeast(3). + // Scroll back to verify that the original commit is no longer at the top GotoTop(). TopLines( Contains("commit-39").IsSelected(), diff --git a/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go b/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go index 81ddf685a..b395e4747 100644 --- a/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go +++ b/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go @@ -33,7 +33,7 @@ var EditTheConflCommit = NewIntegrationTest(NewIntegrationTestArgs{ Focus(). Lines( Contains("─── Pending rebase todos"), - Contains("pick").Contains("commit two"), + Contains("pick").Contains("commit two").IsSelected(), Contains("pick").Contains("<-- CONFLICT --- commit three"), Contains("─── Commits"), Contains("commit one"), diff --git a/pkg/integration/tests/interactive_rebase/revert_multiple_commits_in_interactive_rebase.go b/pkg/integration/tests/interactive_rebase/revert_multiple_commits_in_interactive_rebase.go index da3285f04..630edc823 100644 --- a/pkg/integration/tests/interactive_rebase/revert_multiple_commits_in_interactive_rebase.go +++ b/pkg/integration/tests/interactive_rebase/revert_multiple_commits_in_interactive_rebase.go @@ -55,7 +55,7 @@ var RevertMultipleCommitsInInteractiveRebase = NewIntegrationTest(NewIntegration Contains("CI unrelated change 2"), Contains("─── Pending reverts"), Contains("revert").Contains("CI unrelated change 1"), - Contains("revert").Contains("CI <-- CONFLICT --- add first line"), + Contains("revert").Contains("CI <-- CONFLICT --- add first line").IsSelected(), Contains("─── Commits"), Contains("CI ○ add second line"), Contains("CI ○ add first line"), diff --git a/pkg/integration/tests/interactive_rebase/revert_single_commit_in_interactive_rebase.go b/pkg/integration/tests/interactive_rebase/revert_single_commit_in_interactive_rebase.go index 722fa95f2..d4ba4312d 100644 --- a/pkg/integration/tests/interactive_rebase/revert_single_commit_in_interactive_rebase.go +++ b/pkg/integration/tests/interactive_rebase/revert_single_commit_in_interactive_rebase.go @@ -49,10 +49,10 @@ var RevertSingleCommitInInteractiveRebase = NewIntegrationTest(NewIntegrationTes Contains("CI unrelated change 2"), Contains("CI unrelated change 1"), Contains("─── Pending reverts"), - Contains("revert").Contains("CI <-- CONFLICT --- add first line"), + Contains("revert").Contains("CI <-- CONFLICT --- add first line").IsSelected(), Contains("─── Commits"), Contains("CI ○ add second line"), - Contains("CI ○ add first line").IsSelected(), + Contains("CI ○ add first line"), Contains("CI ○ add empty file"), ). Press(keys.Commits.MoveDownCommit). diff --git a/pkg/integration/tests/interactive_rebase/shared.go b/pkg/integration/tests/interactive_rebase/shared.go index d1d80fafb..522f425c1 100644 --- a/pkg/integration/tests/interactive_rebase/shared.go +++ b/pkg/integration/tests/interactive_rebase/shared.go @@ -4,14 +4,25 @@ import ( . "github.com/jesseduffield/lazygit/pkg/integration/components" ) -func handleConflictsFromSwap(t *TestDriver, expectedCommand string) { +func handleConflictsFromSwap(t *TestDriver, expectedCommand string, selectConflict bool) { t.Common().AcknowledgeConflicts() + // If the conflict comes from directly moving a commit, we want to keep the moved commit + // selected, so selectConflict is false. In other cases (e.g. a conflict after "continue + // rebase") we want to select the conflict commit. + commitTwoMatcher := Contains("pick").Contains("commit two") + conflictMatcher := Contains(expectedCommand).Contains("<-- CONFLICT --- commit three") + if selectConflict { + conflictMatcher.IsSelected() + } else { + commitTwoMatcher.IsSelected() + } + t.Views().Commits(). Lines( Contains("─── Pending rebase todos"), - Contains("pick").Contains("commit two"), - Contains(expectedCommand).Contains("<-- CONFLICT --- commit three"), + commitTwoMatcher, + conflictMatcher, Contains("─── Commits"), Contains("commit one"), ) diff --git a/pkg/integration/tests/interactive_rebase/swap_in_rebase_with_conflict.go b/pkg/integration/tests/interactive_rebase/swap_in_rebase_with_conflict.go index 20b77c03f..693c37a9b 100644 --- a/pkg/integration/tests/interactive_rebase/swap_in_rebase_with_conflict.go +++ b/pkg/integration/tests/interactive_rebase/swap_in_rebase_with_conflict.go @@ -48,6 +48,6 @@ var SwapInRebaseWithConflict = NewIntegrationTest(NewIntegrationTestArgs{ t.Common().ContinueRebase() }) - handleConflictsFromSwap(t, "pick") + handleConflictsFromSwap(t, "pick", true) }, }) diff --git a/pkg/integration/tests/interactive_rebase/swap_in_rebase_with_conflict_and_edit.go b/pkg/integration/tests/interactive_rebase/swap_in_rebase_with_conflict_and_edit.go index 1e9ff4934..7ee710ebe 100644 --- a/pkg/integration/tests/interactive_rebase/swap_in_rebase_with_conflict_and_edit.go +++ b/pkg/integration/tests/interactive_rebase/swap_in_rebase_with_conflict_and_edit.go @@ -51,6 +51,6 @@ var SwapInRebaseWithConflictAndEdit = NewIntegrationTest(NewIntegrationTestArgs{ t.Common().ContinueRebase() }) - handleConflictsFromSwap(t, "edit") + handleConflictsFromSwap(t, "edit", true) }, }) diff --git a/pkg/integration/tests/interactive_rebase/swap_with_conflict.go b/pkg/integration/tests/interactive_rebase/swap_with_conflict.go index 1ea71356e..5f91d9f04 100644 --- a/pkg/integration/tests/interactive_rebase/swap_with_conflict.go +++ b/pkg/integration/tests/interactive_rebase/swap_with_conflict.go @@ -28,6 +28,6 @@ var SwapWithConflict = NewIntegrationTest(NewIntegrationTestArgs{ ). Press(keys.Commits.MoveDownCommit) - handleConflictsFromSwap(t, "pick") + handleConflictsFromSwap(t, "pick", false) }, }) diff --git a/pkg/integration/tests/misc/filter_recent_repos.go b/pkg/integration/tests/misc/filter_recent_repos.go new file mode 100644 index 000000000..a54ca51db --- /dev/null +++ b/pkg/integration/tests/misc/filter_recent_repos.go @@ -0,0 +1,37 @@ +package misc + +import ( + "path/filepath" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FilterRecentRepos = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Switching to a recent repository by typing part of its name", + ExtraCmdArgs: []string{}, + ExtraEnvVars: map[string]string{ + "SHOW_RECENT_REPOS": "true", + }, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + // the first entry is the repo we're in, so it isn't offered + current, _ := filepath.Abs(".") + other, _ := filepath.Abs("../other") + target, _ := filepath.Abs("../target") + cfg.GetAppState().RecentRepos = []string{current, other, target} + }, + SetupRepo: func(shell *Shell) { + shell.CloneNonBare("other") + shell.CloneNonBare("target") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.ExpectPopup().Menu(). + Title(Equals("Recent repositories")). + Filter("target"). + Lines(Contains("target").IsSelected()). + Confirm() + + t.Views().Status().Content(Contains("target → master")) + }, +}) diff --git a/pkg/integration/tests/patch_building/apply_in_reverse_with_conflict.go b/pkg/integration/tests/patch_building/apply_in_reverse_with_conflict.go index 1a09cea7a..d9f99a703 100644 --- a/pkg/integration/tests/patch_building/apply_in_reverse_with_conflict.go +++ b/pkg/integration/tests/patch_building/apply_in_reverse_with_conflict.go @@ -83,11 +83,10 @@ var ApplyInReverseWithConflict = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Files(). Focus(). Lines( - Equals("▼ /").IsSelected(), - Equals(" M file1"), + Equals("▼ /"), + Equals(" M file1").IsSelected(), Equals(" M file2"), - ). - SelectNextItem() + ) t.Views().Main(). ContainsLines( diff --git a/pkg/integration/tests/patch_building/move_to_earlier_commit_from_added_file.go b/pkg/integration/tests/patch_building/move_to_earlier_commit_from_added_file.go index a619128fa..7f0d3584f 100644 --- a/pkg/integration/tests/patch_building/move_to_earlier_commit_from_added_file.go +++ b/pkg/integration/tests/patch_building/move_to_earlier_commit_from_added_file.go @@ -76,10 +76,11 @@ var MoveToEarlierCommitFromAddedFile = NewIntegrationTest(NewIntegrationTestArgs t.Views().Commits(). Focus(). Lines( - Contains("commit to move from"), - Contains("destination commit").IsSelected(), + Contains("commit to move from").IsSelected(), + Contains("destination commit"), Contains("first commit"), ). + NavigateToLine(Contains("destination commit")). PressEnter() t.Views().CommitFiles(). diff --git a/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go b/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go index 050ae2e2f..2e08688df 100644 --- a/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go +++ b/pkg/integration/tests/sync/pull_rebase_interactive_conflict.go @@ -50,7 +50,7 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{ Lines( Contains("─── Pending rebase todos"), Contains("pick").Contains("five"), - Contains("pick").Contains("CONFLICT").Contains("four"), + Contains("pick").Contains("CONFLICT").Contains("four").IsSelected(), Contains("─── Commits"), Contains("three"), Contains("two"), @@ -83,13 +83,12 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Commits(). Focus(). Lines( - Contains("five").IsSelected(), - Contains("four"), + Contains("five"), + Contains("four").IsSelected(), Contains("three"), Contains("two"), Contains("one"), - ). - SelectNextItem() + ) t.Views().Main(). Content( diff --git a/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go b/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go index 707564a8d..38b63608e 100644 --- a/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go +++ b/pkg/integration/tests/sync/pull_rebase_interactive_conflict_drop.go @@ -50,13 +50,14 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg Focus(). Lines( Contains("─── Pending rebase todos"), - Contains("pick").Contains("five").IsSelected(), - Contains("pick").Contains("CONFLICT").Contains("four"), + Contains("pick").Contains("five"), + Contains("pick").Contains("CONFLICT").Contains("four").IsSelected(), Contains("─── Commits"), Contains("three"), Contains("two"), Contains("one"), ). + NavigateToLine(Contains("five")). Press(keys.Universal.Remove). Lines( Contains("─── Pending rebase todos"), diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 15ef6f8c7..a4e732cf0 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -126,6 +126,7 @@ var tests = []*components.IntegrationTest{ commit.CreateAmendCommit, commit.CreateFixupCommitInBranchStack, commit.CreateTag, + commit.DirectoryDiffWithRenamedFiles, commit.DisableCopyCommitMessageBody, commit.DiscardOldFileChanges, commit.DiscardRenamedFile, @@ -231,6 +232,7 @@ var tests = []*components.IntegrationTest{ file.CollapseExpand, file.CopyMenu, file.DirWithUntrackedFile, + file.DirectoryDiffWithRenamedFiles, file.DiscardAllDirChanges, file.DiscardAllDirChangesWhenFiltering, file.DiscardRangeSelect, @@ -248,6 +250,7 @@ var tests = []*components.IntegrationTest{ file.RenameSimilarityThresholdChange, file.RenamedFiles, file.RenamedFilesNoRootItem, + file.StageAllWithoutChangedFiles, file.StageChildrenRangeSelect, file.StageDeletedRangeSelect, file.StageRangeSelect, @@ -259,17 +262,24 @@ var tests = []*components.IntegrationTest{ filter_and_search.FilterFilesStageDirectory, filter_and_search.FilterFuzzy, filter_and_search.FilterMenu, + filter_and_search.FilterMenuAsYouType, filter_and_search.FilterMenuByKeybinding, filter_and_search.FilterMenuCancelFilterWithEscape, + filter_and_search.FilterMenuKeyHandling, filter_and_search.FilterMenuWithNoKeybindings, + filter_and_search.FilterMenuWithPrintableKeybindings, filter_and_search.FilterPreservesSelectionOnModelChange, filter_and_search.FilterRemoteBranches, filter_and_search.FilterRemotes, filter_and_search.FilterSearchHistory, filter_and_search.FilterUpdatesWhenModelChanges, + filter_and_search.FilterWorktrees, filter_and_search.NestedFilter, filter_and_search.NestedFilterTransient, filter_and_search.NewSearch, + filter_and_search.RerenderTheSearchedMainView, + filter_and_search.SearchALongDiff, + filter_and_search.SearchStatusAfterARerender, filter_and_search.StageAllStagesOnlyTrackedFilesInTrackedOnlyFilter, filter_and_search.StagingFolderStagesOnlyTrackedFilesInTrackedOnlyFilter, filter_by_author.SelectAuthor, @@ -353,6 +363,7 @@ var tests = []*components.IntegrationTest{ misc.DirenvApprovesEnvrc, misc.DirenvLoadedOnRepoSwitch, misc.DirenvUnloadsOnBlockedEnvrc, + misc.FilterRecentRepos, misc.InitialOpen, misc.RecentReposOnLaunch, misc.StartInGitDir, @@ -496,22 +507,35 @@ var tests = []*components.IntegrationTest{ tag.Reset, tag.ResetToDuplicateNamedBranch, ui.Accordion, + ui.BackgroundRefreshKeepsScrollPosition, ui.BranchesNotFirstTab, ui.CommitsNotFirstTab, ui.DisableSwitchTabWithPanelJumpKeys, + ui.DragBeyondViewport, ui.EmptyMenu, + ui.FilteringScrollsSelectionIntoView, + ui.FindBaseCommitForFixupScrollsIntoView, ui.HideSidePanel, ui.KeybindingSuggestionsDontCrashOnDisabledBindings, ui.KeybindingSuggestionsWhenSwitchingRepos, + ui.MenuScrollPositionIsReset, ui.ModeSpecificKeybindingSuggestions, + ui.MoveCommitScrollsSelectionIntoView, ui.OpenLinkFailure, + ui.PageUpAndDown, ui.PromoteTabToSidePanel, ui.RangeSelect, ui.RangeSelectWithAutoscroll, ui.ReloadSidePanels, ui.ReorderSidePanels, + ui.SubCommitsScrollPositionIsReset, + ui.SuggestionsSelectionFollowsTheFocus, + ui.SwitchRepoMovesTheSelection, ui.SwitchTabFromMenu, ui.SwitchTabWithPanelJumpKeys, + ui.ToggleWhitespaceKeepsUnfocusedSelectionDimmed, + ui.UnfocusedListHidesSelectionWhenEmptied, + ui.UnfocusedListShowsSelectionWhenFilled, undo.UndoCheckoutAndDrop, undo.UndoCommit, undo.UndoDrop, @@ -548,4 +572,5 @@ var tests = []*components.IntegrationTest{ worktree.SeparateWorkTreeConfig, worktree.SymlinkIntoRepoSubdir, worktree.WorktreeInRepo, + worktree.WorktreeInsideRepo, } diff --git a/pkg/integration/tests/ui/background_refresh_keeps_scroll_position.go b/pkg/integration/tests/ui/background_refresh_keeps_scroll_position.go new file mode 100644 index 000000000..e2cf6ee1e --- /dev/null +++ b/pkg/integration/tests/ui/background_refresh_keeps_scroll_position.go @@ -0,0 +1,41 @@ +package ui + +import ( + "fmt" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var BackgroundRefreshKeepsScrollPosition = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "A background refresh doesn't scroll the selection back into view", + ExtraCmdArgs: []string{}, + Skip: false, + Width: 120, + Height: 30, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("initial commit") + for i := range 20 { + shell.CreateFile(fmt.Sprintf("file%02d", i), "") + } + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + Focus(). + SelectNextItem(). + SelectedLine(Contains("file00")). + // Scroll the selection out of view with the mouse wheel + ScrollWheelDown(). + ScrollWheelDown(). + OriginY(4). + Tap(func() { + t.Shell().CreateFile("aaa", "") + t.RefreshInBackground() + }). + // The new file sorts before the selected one, so the selection has + // moved down a line; the view must stay where the user left it though + SelectedLineIdx(2). + OriginY(4) + }, +}) diff --git a/pkg/integration/tests/ui/drag_beyond_viewport.go b/pkg/integration/tests/ui/drag_beyond_viewport.go new file mode 100644 index 000000000..f756a783c --- /dev/null +++ b/pkg/integration/tests/ui/drag_beyond_viewport.go @@ -0,0 +1,37 @@ +package ui + +import ( + "fmt" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DragBeyondViewport = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Dragging a range selection beyond the bottom of the panel doesn't scroll the view", + ExtraCmdArgs: []string{}, + Skip: false, + Width: 120, + Height: 30, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("initial commit") + for i := range 20 { + shell.CreateFile(fmt.Sprintf("file%02d", i), "") + } + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + Focus(). + OriginY(0). + // The pointer ends up below the panel, so the range extends to a line + // that isn't visible. Scrolling there is the drag autoscroller's job, + // which scrolls line by line for as long as the pointer stays there; + // the drag itself must leave the scroll position alone. + ClickAndHold(1, 1). + MouseMove(1, 8). + MouseRelease(). + SelectedLineIdx(8). + OriginY(0) + }, +}) diff --git a/pkg/integration/tests/ui/empty_menu.go b/pkg/integration/tests/ui/empty_menu.go index 35c3d4560..971bcb3c8 100644 --- a/pkg/integration/tests/ui/empty_menu.go +++ b/pkg/integration/tests/ui/empty_menu.go @@ -17,16 +17,19 @@ var EmptyMenu = NewIntegrationTest(NewIntegrationTestArgs{ IsFocused(). Press(keys.Universal.OptionMenu) + t.ExpectPopup().Menu(). + // a string that filters everything out + Filter("ljasldkjaslkdjalskdjalsdjaslkd") + t.Views().Menu(). IsFocused(). - // a string that filters everything out - FilterOrSearch("ljasldkjaslkdjalskdjalsdjaslkd"). IsEmpty(). - Press(keys.Universal.Select). + // space is filter text in this menu, so we confirm with enter + Press(keys.Universal.ConfirmMenu). Tap(func() { t.ExpectToast(Equals("Disabled: No item selected")) }). - // escape the search + // escape the filter PressEscape(). // escape the view PressEscape() diff --git a/pkg/integration/tests/ui/filtering_scrolls_selection_into_view.go b/pkg/integration/tests/ui/filtering_scrolls_selection_into_view.go new file mode 100644 index 000000000..3f1ab4d84 --- /dev/null +++ b/pkg/integration/tests/ui/filtering_scrolls_selection_into_view.go @@ -0,0 +1,62 @@ +package ui + +import ( + "fmt" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FilteringScrollsSelectionIntoView = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Entering and leaving filtering mode scrolls the selected commit into view", + ExtraCmdArgs: []string{}, + Skip: false, + Width: 120, + Height: 30, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + for i := range 40 { + file := "otherFile" + if i%2 == 0 { + file = "filterFile" + } + shell.UpdateFileAndAdd(file, fmt.Sprintf("content %02d", i)) + shell.Commit(fmt.Sprintf("commit %02d", i)) + } + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit 00")). + OriginYAtLeast(1). + Press(keys.Universal.FilteringMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Filtering")). + Select(Contains("Enter path to filter by")). + Confirm() + t.ExpectPopup().Prompt(). + Title(Equals("Enter path:")). + Type("filterFile"). + Confirm() + + // The filtered list has nothing to do with the one that was showing, so + // its scroll position doesn't either: we start at the top again + t.Views().Commits(). + IsFocused(). + SelectedLine(Contains("commit 38")). + SelectedLineIdx(0). + OriginY(0). + Press(keys.Universal.GotoBottom). + SelectedLine(Contains("commit 00")). + PressEscape() + + // Leaving filtering mode keeps the commit selected, at its position in + // the full list, which needs scrolling to again + t.Views().Commits(). + IsFocused(). + SelectedLine(Contains("commit 00")). + SelectedLineIsVisible() + }, +}) diff --git a/pkg/integration/tests/ui/find_base_commit_for_fixup_scrolls_into_view.go b/pkg/integration/tests/ui/find_base_commit_for_fixup_scrolls_into_view.go new file mode 100644 index 000000000..b0c63dcf5 --- /dev/null +++ b/pkg/integration/tests/ui/find_base_commit_for_fixup_scrolls_into_view.go @@ -0,0 +1,35 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FindBaseCommitForFixupScrollsIntoView = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Finding the base commit for a fixup scrolls it into view", + ExtraCmdArgs: []string{}, + Skip: false, + Width: 120, + Height: 30, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.NewBranch("mybranch"). + EmptyCommit("1st commit"). + CreateFileAndAdd("file1", "line 1\nline 2\nline 3\n"). + Commit("base commit"). + CreateNCommits(40). + UpdateFile("file1", "line 1\nline 2 changed\nline 3\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + Focus(). + Press(keys.Files.FindBaseCommitForFixup) + + // The base commit is at the very bottom of the list, far below the + // visible area + t.Views().Commits(). + IsFocused(). + SelectedLine(Contains("base commit")). + SelectedLineIsVisible() + }, +}) diff --git a/pkg/integration/tests/ui/menu_scroll_position_is_reset.go b/pkg/integration/tests/ui/menu_scroll_position_is_reset.go new file mode 100644 index 000000000..448e0b995 --- /dev/null +++ b/pkg/integration/tests/ui/menu_scroll_position_is_reset.go @@ -0,0 +1,39 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var MenuScrollPositionIsReset = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "A menu that is opened after a scrolled down one starts at the top again", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFile("myfile", "myfile") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.OptionMenu) + + t.Views().Menu(). + IsFocused(). + // The first line is a section header, so the first item is at index 1 + SelectedLineIdx(1). + OriginY(0). + Press(keys.Universal.GotoBottom). + OriginYAtLeast(1). + PressEscape() + + t.Views().Files(). + IsFocused(). + Press(keys.Universal.OptionMenu) + + t.Views().Menu(). + IsFocused(). + SelectedLineIdx(1). + OriginY(0) + }, +}) diff --git a/pkg/integration/tests/ui/move_commit_scrolls_selection_into_view.go b/pkg/integration/tests/ui/move_commit_scrolls_selection_into_view.go new file mode 100644 index 000000000..a665b548d --- /dev/null +++ b/pkg/integration/tests/ui/move_commit_scrolls_selection_into_view.go @@ -0,0 +1,31 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var MoveCommitScrollsSelectionIntoView = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Moving a commit down scrolls it into view if it isn't visible", + ExtraCmdArgs: []string{}, + Skip: false, + Width: 120, + Height: 30, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateNCommits(40) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + SelectedLine(Contains("commit-40")). + // Scroll the selected commit out of view with the mouse wheel + ScrollWheelDown(). + ScrollWheelDown(). + OriginY(4). + Press(keys.Commits.MoveDownCommit). + SelectedLine(Contains("commit-40")). + SelectedLineIdx(1). + SelectedLineIsVisible() + }, +}) diff --git a/pkg/integration/tests/ui/page_up_and_down.go b/pkg/integration/tests/ui/page_up_and_down.go new file mode 100644 index 000000000..603edfd92 --- /dev/null +++ b/pkg/integration/tests/ui/page_up_and_down.go @@ -0,0 +1,47 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +const ( + // The height of the commits panel in this test's window, in lines. + commitsPanelHeight = 5 + // Paging keeps one line of overlap between the old and the new page. + pageDelta = commitsPanelHeight - 1 +) + +var PageUpAndDown = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Paging down and up keeps the selection at the edge of the viewport", + ExtraCmdArgs: []string{}, + Skip: false, + Width: 120, + Height: 30, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateNCommits(40) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + SelectedLineIdx(0). + OriginY(0). + Press(keys.Universal.NextPage). + // The selection moves to the bottom of the viewport; nothing scrolls yet + SelectedLineIdx(commitsPanelHeight - 1). + OriginY(0). + Press(keys.Universal.NextPage). + // Now the view scrolls by a page, and the selection stays at the bottom + SelectedLineIdx(commitsPanelHeight - 1 + pageDelta). + OriginY(pageDelta). + Press(keys.Universal.PrevPage). + // The selection moves to the top of the viewport; nothing scrolls + SelectedLineIdx(pageDelta). + OriginY(pageDelta). + Press(keys.Universal.PrevPage). + // And back a page, with the selection staying at the top + SelectedLineIdx(0). + OriginY(0) + }, +}) diff --git a/pkg/integration/tests/ui/sub_commits_scroll_position_is_reset.go b/pkg/integration/tests/ui/sub_commits_scroll_position_is_reset.go new file mode 100644 index 000000000..1de8acb70 --- /dev/null +++ b/pkg/integration/tests/ui/sub_commits_scroll_position_is_reset.go @@ -0,0 +1,39 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var SubCommitsScrollPositionIsReset = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Viewing the commits of a branch again after scrolling down starts at the top again", + ExtraCmdArgs: []string{}, + Skip: false, + Width: 120, + Height: 30, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateNCommits(40) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + PressEnter() + + t.Views().SubCommits(). + IsFocused(). + OriginY(0). + Press(keys.Universal.GotoBottom). + OriginYAtLeast(1). + PressEscape() + + t.Views().Branches(). + IsFocused(). + PressEnter() + + t.Views().SubCommits(). + IsFocused(). + SelectedLineIdx(0). + OriginY(0) + }, +}) diff --git a/pkg/integration/tests/ui/suggestions_selection_follows_the_focus.go b/pkg/integration/tests/ui/suggestions_selection_follows_the_focus.go new file mode 100644 index 000000000..793c58bf8 --- /dev/null +++ b/pkg/integration/tests/ui/suggestions_selection_follows_the_focus.go @@ -0,0 +1,42 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var SuggestionsSelectionFollowsTheFocus = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "The suggestions list only shows a selection while it, rather than the prompt, has the focus", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell. + EmptyCommit("one"). + NewBranch("branch-to-checkout") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + Press(keys.Branches.CheckoutBranchByName) + + t.ExpectPopup().Prompt(). + Title(Equals("Branch name:")). + Type("branch-to"). + SuggestionTopLines(Contains("branch-to-checkout")) + + t.Views().Suggestions().SelectionIsHidden() + + t.Views().Prompt().Press(keys.Universal.TogglePanel) + t.Views().Suggestions(). + IsFocused(). + SelectionIsActive(). + Press(keys.Universal.TogglePanel) + + t.Views().Prompt().IsFocused() + t.Views().Suggestions().SelectionIsHidden() + + t.Views().Prompt().Press(keys.Universal.Return) + t.Views().Branches().IsFocused() + }, +}) diff --git a/pkg/integration/tests/ui/switch_repo_moves_the_selection.go b/pkg/integration/tests/ui/switch_repo_moves_the_selection.go new file mode 100644 index 000000000..eb7c4b2df --- /dev/null +++ b/pkg/integration/tests/ui/switch_repo_moves_the_selection.go @@ -0,0 +1,48 @@ +package ui + +import ( + "path/filepath" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var SwitchRepoMovesTheSelection = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "The selection follows the focus of the repo being switched to, rather than the one being left", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + otherRepo, _ := filepath.Abs("../other") + config.GetAppState().RecentRepos = []string{otherRepo} + }, + SetupRepo: func(shell *Shell) { + shell.CloneNonBare("other") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + switchToRepo := func(repo string) { + t.GlobalPress(keys.Universal.OpenRecentRepos) + t.ExpectPopup().Menu().Title(Equals("Recent repositories")). + Lines( + Contains(repo).IsSelected(), + Contains("Cancel"), + ).Confirm() + t.Views().Status().Content(Contains(repo + " → master")) + } + + t.Views().Branches(). + Focus(). + SelectionIsActive() + + // The other repo has its own focus, which is the files panel it starts in + switchToRepo("other") + t.Views().Files().IsFocused() + t.Views().Branches().SelectionIsHidden() + + // And coming back, this repo still has the focus we left it with + switchToRepo("repo") + t.Views().Branches(). + IsFocused(). + SelectionIsActive() + t.Views().Files().SelectionIsHidden() + }, +}) diff --git a/pkg/integration/tests/ui/toggle_whitespace_keeps_unfocused_selection_dimmed.go b/pkg/integration/tests/ui/toggle_whitespace_keeps_unfocused_selection_dimmed.go new file mode 100644 index 000000000..9ffbd2878 --- /dev/null +++ b/pkg/integration/tests/ui/toggle_whitespace_keeps_unfocused_selection_dimmed.go @@ -0,0 +1,31 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ToggleWhitespaceKeepsUnfocusedSelectionDimmed = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Toggling whitespace from the main view leaves the panel beneath it showing a dimmed selection", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\n") + shell.Commit("one") + shell.UpdateFile("file1", " one\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines(Contains("file1")). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + Press(keys.Universal.ToggleWhitespaceInDiffView) + + t.Views().Files(). + SelectionIsInactive() + }, +}) diff --git a/pkg/integration/tests/ui/unfocused_list_hides_selection_when_emptied.go b/pkg/integration/tests/ui/unfocused_list_hides_selection_when_emptied.go new file mode 100644 index 000000000..eef409cb2 --- /dev/null +++ b/pkg/integration/tests/ui/unfocused_list_hides_selection_when_emptied.go @@ -0,0 +1,36 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var UnfocusedListHidesSelectionWhenEmptied = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "A list that loses its last item while the focus is elsewhere stops showing a selection", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\n") + shell.Commit("one") + shell.UpdateFile("file1", "two\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines(Contains("file1")). + SelectionIsActive(). + Press(keys.Universal.FocusMainView) + + t.Views().Main().IsFocused() + + t.Views().Files(). + SelectionIsInactive(). + Tap(func() { + t.Shell().RunCommand([]string{"git", "checkout", "--", "file1"}) + t.RefreshInBackground() + }). + IsEmpty(). + SelectionIsHidden() + }, +}) diff --git a/pkg/integration/tests/ui/unfocused_list_shows_selection_when_filled.go b/pkg/integration/tests/ui/unfocused_list_shows_selection_when_filled.go new file mode 100644 index 000000000..78a8f210d --- /dev/null +++ b/pkg/integration/tests/ui/unfocused_list_shows_selection_when_filled.go @@ -0,0 +1,34 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var UnfocusedListShowsSelectionWhenFilled = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "A list that gets its first item while the focus is elsewhere starts showing a selection", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\n") + shell.Commit("one") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + IsEmpty(). + SelectionIsHidden(). + Press(keys.Universal.FocusMainView) + + t.Views().Main().IsFocused() + + t.Views().Files(). + Tap(func() { + t.Shell().CreateFile("file2", "two\n") + t.RefreshInBackground() + }). + Lines(Contains("file2")). + SelectionIsInactive() + }, +}) diff --git a/pkg/integration/tests/worktree/worktree_inside_repo.go b/pkg/integration/tests/worktree/worktree_inside_repo.go new file mode 100644 index 000000000..bae2ff8f1 --- /dev/null +++ b/pkg/integration/tests/worktree/worktree_inside_repo.go @@ -0,0 +1,28 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var WorktreeInsideRepo = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "A worktree that lives inside the repo's working tree is shown as a single item in the files panel", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Gui.NerdFontsVersion = "3" + }, + SetupRepo: func(shell *Shell) { + shell.NewBranch("mybranch") + shell.CreateFileAndAdd("README.md", "hello world") + shell.Commit("initial commit") + shell.AddWorktree("mybranch", "nested-worktree", "newbranch") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Equals("?? 󰌹 nested-worktree").IsSelected(), + ) + }, +}) diff --git a/pkg/integration/types/types.go b/pkg/integration/types/types.go index 4d2da7602..325f4ea38 100644 --- a/pkg/integration/types/types.go +++ b/pkg/integration/types/types.go @@ -31,6 +31,9 @@ type GuiDriver interface { ClickAndHold(int, int) MouseMove(int, int) MouseRelease(int, int) + ScrollWheelDown(int, int) + // Perform the refresh that a background routine would perform on a timer + RefreshInBackground() // Can be used to avoid data races with the UI thread in the uncommon cases that // the test driver needs to assert state while the gui is not idle. OnUIThreadAndWait(func()) @@ -42,6 +45,8 @@ type GuiDriver interface { FocusInAndClick(int, int) Keys() config.KeybindingConfig CurrentContext() types.Context + // Whether the terminal's text cursor is currently shown + CursorVisible() bool ContextForView(viewName string) types.Context Fail(message string) // These two log methods are for the sake of debugging while testing. There's no need to actually diff --git a/pkg/jsonschema/generate.go b/pkg/jsonschema/generate.go index dc5045025..4f22b04bd 100644 --- a/pkg/jsonschema/generate.go +++ b/pkg/jsonschema/generate.go @@ -9,14 +9,14 @@ import ( "reflect" "strings" - "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/karimkhaleel/jsonschema" "github.com/samber/lo" ) func GetSchemaDir() string { - return utils.GetLazyRootDirectory() + "/schema-master" + return utils.MustFindLazygitRootDirectory() + "/schema-master" } func GenerateSchema() *jsonschema.Schema { @@ -144,7 +144,7 @@ func setDefaultVals(rootSchema, schema *jsonschema.Schema, defaults any) { t := reflect.TypeOf(defaults) v := reflect.ValueOf(defaults) - if t.Kind() == reflect.Ptr || t.Kind() == reflect.Interface { + if t.Kind() == reflect.Pointer || t.Kind() == reflect.Interface { t = t.Elem() v = v.Elem() } @@ -202,7 +202,7 @@ func isZeroValue(v any) bool { switch rv.Kind() { case reflect.Slice, reflect.Map: return rv.Len() == 0 - case reflect.Ptr, reflect.Interface: + case reflect.Pointer, reflect.Interface: return rv.IsNil() case reflect.Struct: for i := range rv.NumField() { diff --git a/pkg/jsonschema/generate_config_docs.go b/pkg/jsonschema/generate_config_docs.go index ba245e99a..0caa13db9 100644 --- a/pkg/jsonschema/generate_config_docs.go +++ b/pkg/jsonschema/generate_config_docs.go @@ -7,7 +7,7 @@ import ( "os" "strings" - "github.com/jesseduffield/lazycore/pkg/utils" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/karimkhaleel/jsonschema" "github.com/samber/lo" @@ -163,7 +163,7 @@ func (n *Node) MarshalYAML() (any, error) { } func writeToConfigDocs(config []byte) error { - configPath := utils.GetLazyRootDirectory() + "/docs-master/Config.md" + configPath := utils.MustFindLazygitRootDirectory() + "/docs-master/Config.md" markdown, err := os.ReadFile(configPath) if err != nil { return fmt.Errorf("Error reading Config.md file %w", err) diff --git a/pkg/logs/logs.go b/pkg/logs/logs.go index 7ec1b91b4..40fc207c5 100644 --- a/pkg/logs/logs.go +++ b/pkg/logs/logs.go @@ -4,6 +4,8 @@ import ( "io" "log" "os" + "sync" + "time" "github.com/sirupsen/logrus" ) @@ -34,6 +36,11 @@ func NewProductionLogger() *logrus.Entry { return formatted(logger) } +// Separates one run's log entries from the previous run's. Only the first +// logger of a run writes it: with LAZYGIT_LOG_PATH set there are two of them +// for the same file, the global one and the app's. +var runSeparator sync.Once + func NewDevelopmentLogger(logPath string) *logrus.Entry { logger := logrus.New() logger.SetLevel(getLogLevel()) @@ -42,6 +49,9 @@ func NewDevelopmentLogger(logPath string) *logrus.Entry { if err != nil { log.Fatalf("Unable to log to log file: %v", err) } + runSeparator.Do(func() { + _, _ = file.WriteString("\n") + }) logger.SetOutput(file) return formatted(logger) } @@ -49,7 +59,7 @@ func NewDevelopmentLogger(logPath string) *logrus.Entry { func formatted(log *logrus.Logger) *logrus.Entry { // highly recommended: tail -f development.log | humanlog // https://github.com/aybabtme/humanlog - log.Formatter = &logrus.JSONFormatter{} + log.Formatter = &logrus.JSONFormatter{TimestampFormat: time.RFC3339Nano} return log.WithFields(logrus.Fields{}) } diff --git a/pkg/logs/tail/tail.go b/pkg/logs/tail/tail.go index b21bc21e4..1cc5ef05e 100644 --- a/pkg/logs/tail/tail.go +++ b/pkg/logs/tail/tail.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "os" + "time" "github.com/aybabtme/humanlog" ) @@ -15,6 +16,7 @@ func TailLogs(logFilePath string) { opts := humanlog.DefaultOptions opts.Truncates = false + opts.TimeFormat = time.StampMilli _, err := os.Stat(logFilePath) if err != nil { diff --git a/pkg/snake/snake.go b/pkg/snake/snake.go index 62fc0ddfd..7dd2e079c 100644 --- a/pkg/snake/snake.go +++ b/pkg/snake/snake.go @@ -20,7 +20,7 @@ type Game struct { exit chan (struct{}) // channel for specifying the direction the player wants the snake to go in - setNewDir chan (Direction) + setNewDir chan Direction // allows logging for debugging logger func(string) diff --git a/pkg/tasks/read_request_queue.go b/pkg/tasks/read_request_queue.go new file mode 100644 index 000000000..444e514fd --- /dev/null +++ b/pkg/tasks/read_request_queue.go @@ -0,0 +1,101 @@ +package tasks + +import "sync" + +// readRequestQueue is an unbounded, order-preserving FIFO of the read requests a +// view's running command task serves (see LinesToRead), with a reader that comes +// and goes. +// +// It's unbounded, rather than a fixed-size channel, for the same reasons as the +// user-event queue in gocui. Requests are handed over from the UI thread, where a +// blocking send would deadlock against the task that is waiting to be let go, and +// a fixed channel that fills up leaves only bad choices: blocking, dropping, +// reordering, or panicking on overflow. Appending to a slice does none of those. +// +// The reader coming and going is the other half of what it's for. A request is +// how a caller asks for content to be read and hears, through the request's Then, +// that it has been; a request nobody answers leaves that caller waiting for good. +// So asking whether a task is there and handing it the request are one step, and +// so are taking the task away and handing back what it never answered. A request +// made in between finds no task and goes back to its caller to answer. +// +// enqueue appends under the mutex and rings the doorbell; the task selects on the +// doorbell to wake, then takes requests until there are none left. The doorbell is +// buffered(1) and rung with a non-blocking send, so it's a coalescing "work +// pending" flag rather than a per-request signal: a burst of appends leaves at +// most one token, and the task takes everything the token stands for on a single +// wake. A token left over after the queue empties causes one harmless empty wake. +type readRequestQueue struct { + mutex sync.Mutex + requests []LinesToRead + doorbell chan struct{} + + // Whether a task is there to serve the requests. False before the first task + // starts, and between one task ending and the next starting. + serving bool +} + +func newReadRequestQueue() *readRequestQueue { + return &readRequestQueue{doorbell: make(chan struct{}, 1)} +} + +// beginServing says that a task is now there to serve the queue, and returns the +// doorbell that tells it when there is something to serve. +func (self *readRequestQueue) beginServing() <-chan struct{} { + self.mutex.Lock() + defer self.mutex.Unlock() + + self.serving = true + return self.doorbell +} + +// stopServing takes the task away and hands back the requests it never answered, +// for the caller to answer in its place. +func (self *readRequestQueue) stopServing() []LinesToRead { + self.mutex.Lock() + defer self.mutex.Unlock() + + self.serving = false + unanswered := self.requests + self.requests = nil + return unanswered +} + +// enqueue gives a request to the task serving the queue, and reports whether +// there was one to give it to. When there wasn't, the request is the caller's to +// answer. +func (self *readRequestQueue) enqueue(request LinesToRead) bool { + self.mutex.Lock() + if !self.serving { + self.mutex.Unlock() + return false + } + self.requests = append(self.requests, request) + self.mutex.Unlock() + + select { + case self.doorbell <- struct{}{}: + default: + } + return true +} + +// dequeue takes the oldest request, reporting false when there are none. +func (self *readRequestQueue) dequeue() (LinesToRead, bool) { + self.mutex.Lock() + defer self.mutex.Unlock() + + if len(self.requests) == 0 { + return LinesToRead{}, false + } + request := self.requests[0] + if len(self.requests) == 1 { + // Release the backing array whenever the queue drains, so a one-off burst + // doesn't pin its peak size for the rest of the session. + self.requests = nil + } else { + self.requests[0] = LinesToRead{} + self.requests = self.requests[1:] + } + return request, true +} diff --git a/pkg/tasks/read_request_queue_test.go b/pkg/tasks/read_request_queue_test.go new file mode 100644 index 000000000..423b89d09 --- /dev/null +++ b/pkg/tasks/read_request_queue_test.go @@ -0,0 +1,58 @@ +package tasks + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadRequestQueueHandsBackWhatNoTaskWillServe(t *testing.T) { + queue := newReadRequestQueue() + + // Nothing has begun serving, so the request comes straight back to its caller. + assert.False(t, queue.enqueue(LinesToRead{Total: 1})) + + queue.beginServing() + assert.True(t, queue.enqueue(LinesToRead{Total: 1})) + assert.True(t, queue.enqueue(LinesToRead{Total: 2})) + + request, ok := queue.dequeue() + assert.True(t, ok) + assert.Equal(t, 1, request.Total) + + // What the task never got to comes back when it stops, and nothing is taken + // from a caller after that. + unanswered := queue.stopServing() + assert.Len(t, unanswered, 1) + assert.Equal(t, 2, unanswered[0].Total) + + assert.False(t, queue.enqueue(LinesToRead{Total: 3})) + _, ok = queue.dequeue() + assert.False(t, ok) +} + +func TestReadRequestQueueRingsTheDoorbell(t *testing.T) { + queue := newReadRequestQueue() + doorbell := queue.beginServing() + + select { + case <-doorbell: + assert.Fail(t, "the doorbell rang before anything was queued") + default: + } + + // A burst leaves one token, which stands for everything queued. + queue.enqueue(LinesToRead{Total: 1}) + queue.enqueue(LinesToRead{Total: 2}) + + select { + case <-doorbell: + default: + assert.Fail(t, "the doorbell didn't ring") + } + select { + case <-doorbell: + assert.Fail(t, "the doorbell rang twice for one wake") + default: + } +} diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go index 3768e0c19..deaeee042 100644 --- a/pkg/tasks/tasks.go +++ b/pkg/tasks/tasks.go @@ -4,7 +4,9 @@ import ( "bufio" "fmt" "io" + "os" "os/exec" + "strconv" "sync" "sync/atomic" "time" @@ -61,22 +63,58 @@ type ViewBufferManager struct { writer io.Writer waitingMutex deadlock.Mutex - taskIDMutex deadlock.Mutex - Log *logrus.Entry - newTaskID int - // The channel by which the currently-running task is told to read more - // lines (e.g. as the user scrolls). Held in an atomic because it's swapped - // out as tasks come and go while ReadLines/ReadToEnd read it from the UI - // thread; nil when no task is running. - readLines atomic.Pointer[chan LinesToRead] - taskKey string - onNewKey func() + // Guards newTaskID and taskKey, which identify the most recently requested + // task. Both are written on the goroutine NewTask spawns, and taskKey is + // read from the UI thread (GetTaskKey), so neither may be touched without + // holding this. + taskIDMutex deadlock.Mutex + Log *logrus.Entry + newTaskID int + // The requests by which the currently-running task is told to read more lines + // (e.g. as the user scrolls), and which it answers once it has. The task + // serving them comes and goes; see readRequestQueue. + readRequests *readRequestQueue + taskKey string + + // Resets the view's scroll position to the top. A render whose content is + // different from what the view last showed (a different command key) calls + // this — but at its *first paint*, not when the task starts: the off-screen + // render leaves the previous content displayed until the swap, so resetting + // the origin up front would scroll that still-displayed content to the top + // before the new content replaces it. See newContentPending. + resetOrigin func() + + // Whether the content the running task is rendering differs from what the + // view is currently showing (i.e. the command key changed). Two things key + // off it: the loading indicator only takes the view over when it is set, + // since there is no point clearing content we are about to render + // identically; and the first paint that reveals the content resets the + // scroll to the top and clears it. + // + // It deliberately outlives the task that set it: a task can be stopped and + // replaced before it ever paints — a background refresh landing just after + // the user clicked a different item, say — and the replacement, which + // renders the same content and so sets nothing of its own, still has to do + // what that task was owed. + newContentPending atomic.Bool + + // Whether a command task is currently reading content into the view. While + // this is true the content is still growing, so callers (e.g. the layout) + // must not clamp the view's scroll position to the amount loaded so far. + loading atomic.Bool // beforeStart is the function that is called before starting a new task beforeStart func() refreshView func() onEndOfInput func() + // beginRender starts an off-screen render: the new content is built without + // disturbing what's displayed. swapInRender then promotes it to the display + // in one step. Together they keep the view showing the previous render until + // the new one has read enough to paint, instead of revealing it line by line. + beginRender func() + swapInRender func() + // see docs/dev/Busy.md // A gocui task is not the same thing as the tasks defined in this file. // A gocui task simply represents the fact that lazygit is busy doing something, @@ -87,7 +125,7 @@ type ViewBufferManager struct { // of the view happen through this, so that the view is only ever touched on // the UI thread (where it is also laid out and drawn), never on the task's // own goroutine. - onUIThread func(f func() error) error + onUIThread func(f func()) error // if the user flicks through a heap of items, with each one // spawning a process to render something to the main view, @@ -115,6 +153,9 @@ type LinesToRead struct { } func (self *ViewBufferManager) GetTaskKey() string { + self.taskIDMutex.Lock() + defer self.taskIDMutex.Unlock() + return self.taskKey } @@ -124,17 +165,22 @@ func NewViewBufferManager( beforeStart func(), refreshView func(), onEndOfInput func(), - onNewKey func(), + resetOrigin func(), + beginRender func(), + swapInRender func(), newGocuiTask func() gocui.Task, - onUIThread func(f func() error) error, + onUIThread func(f func()) error, ) *ViewBufferManager { return &ViewBufferManager{ + readRequests: newReadRequestQueue(), Log: log, writer: writer, beforeStart: beforeStart, refreshView: refreshView, onEndOfInput: onEndOfInput, - onNewKey: onNewKey, + resetOrigin: resetOrigin, + beginRender: beginRender, + swapInRender: swapInRender, newGocuiTask: newGocuiTask, onUIThread: onUIThread, } @@ -145,22 +191,52 @@ func NewViewBufferManager( // (e.g. as the user scrolls down, back up, and down again) don't re-read lines // that have already been read: the task only ever reads the shortfall. func (self *ViewBufferManager) ReadLines(totalLines int) { - if ch := self.readLines.Load(); ch != nil { - readLines := *ch - go utils.Safe(func() { - readLines <- LinesToRead{Total: totalLines, InitialRefreshAfter: -1} - }) - } + // A request with no Then needs no answer, so there is nothing to do when no + // task is there to take it. + self.readRequests.enqueue(LinesToRead{Total: totalLines, InitialRefreshAfter: -1}) +} + +// IsLoading reports whether a command task is currently reading content into the +// view, meaning the content is still growing. +func (self *ViewBufferManager) IsLoading() bool { + return self.loading.Load() +} + +// StartLoading marks the view as loading content. It must be called +// synchronously when a command/pty task is started, before the task's goroutine +// runs, so that a layout pass happening in between doesn't clamp the scroll +// position to the not-yet-loaded content. It is cleared when the task reaches +// the end of its input. +func (self *ViewBufferManager) StartLoading() { + self.loading.Store(true) } func (self *ViewBufferManager) ReadToEnd(then func()) { - if ch := self.readLines.Load(); ch != nil { - readLines := *ch - go utils.Safe(func() { - readLines <- LinesToRead{Total: -1, InitialRefreshAfter: -1, Then: then} - }) - } else if then != nil { - then() + // The reading happens on the task's own goroutine, and the caller hears about + // it through then, so lazygit must not count as idle in between. + task := self.newGocuiTask() + answered := func() { + task.Done() + if then != nil { + then() + } + } + + request := LinesToRead{Total: -1, InitialRefreshAfter: -1, Then: answered} + if !self.readRequests.enqueue(request) { + // With no task reading, everything there is to read has been read. + answered() + } +} + +// stopServingReadRequests takes the task away from the read-request queue and +// answers whatever it never got to, so that nobody is left waiting for a callback +// that isn't coming. +func (self *ViewBufferManager) stopServingReadRequests() { + for _, request := range self.readRequests.stopServing() { + if request.Then != nil { + request.Then() + } } } @@ -228,8 +304,9 @@ func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix loadingMutex := deadlock.Mutex{} - readLines := make(chan LinesToRead, 1024) - self.readLines.Store(&readLines) + // Begin serving before any goroutine starts, so that the first request below + // can't arrive before there is a task to take it. + readRequests := self.readRequests.beginServing() scanner := bufio.NewScanner(r) scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize)) @@ -271,8 +348,21 @@ func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix return case <-ticker.C: loadingMutex.Lock() - if !loaded { + // Only take the view over to say "loading..." when the content coming + // is different from what's on screen. A re-render of the same content + // leaves the view showing exactly what it should already, so clearing + // it for the message and then rendering the same thing back is a + // visible flicker for nothing — and a slow re-render of unchanged + // content is common (a background refresh over a repo with submodules + // that have uncommitted changes, say). The pending flag isn't consumed + // here; the first paint still owes the scroll reset. + if !loaded && self.newContentPending.Load() { self.beforeStart() + // beforeStart cleared the previous content to show "loading...", so + // put the view back at the top for it (beforeStart doesn't touch the + // origin). The origin is view state the UI thread reads while laying + // out, so write it there. + _ = self.onUIThread(self.resetOrigin) _, _ = self.writer.Write([]byte("loading...")) self.refreshView() } @@ -297,8 +387,9 @@ func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix // closed the selects below could still service a ready data channel // instead of bailing. Check stop explicitly first to give it priority: // a task that's been stopped (it's being replaced by a newer one) must - // not touch the view here — beforeStart clears it and the prefix gets - // written, clobbering what the incoming task is about to render. + // not touch the view here — it would start an off-screen render and + // write the prefix into it, clobbering what the incoming task is about + // to render. stopped := func() bool { select { case <-opts.Stop: @@ -313,15 +404,52 @@ func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix // this to work out how many more lines, if any, we still need to read. linesRead := 0 + // The first paint swaps the off-screen render in to reveal the new + // content, and settles the scroll position in the same step — so the new + // content first appears already where it belongs, and no draw can land + // between the two and show it at the previous render's scroll. It happens + // once, either when we've read far enough (below) or at end of input for + // content shorter than that. Callers run it on the UI thread: it writes + // the view's origin. + painted := false + firstPaint := func() { + if painted { + return + } + painted = true + self.swapInRender() + if self.newContentPending.Swap(false) { + self.resetOrigin() + } + } + + // Set LAZYGIT_SLOW_RENDER= to sleep that long after each + // line is written to the view, stretching async loads out so the frames + // of a re-render become visible. Useful for debugging scroll/flicker + // behaviour; has no effect when the variable is unset. + var slowRenderPerLine time.Duration + if v := os.Getenv("LAZYGIT_SLOW_RENDER"); v != "" { + if ms, err := strconv.Atoi(v); err == nil { + slowRenderPerLine = time.Duration(ms) * time.Millisecond + } + } + outer: for { if stopped() { break outer } - select { - case <-opts.Stop: - break outer - case linesToRead := <-readLines: + linesToRead, ok := self.readRequests.dequeue() + if !ok { + // Nothing to read yet: wait to be told there is, or to be stopped. + select { + case <-opts.Stop: + break outer + case <-readRequests: + } + continue + } + { callThen := func() { if linesToRead.Then != nil { linesToRead.Then() @@ -344,7 +472,10 @@ func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix loadingMutex.Lock() if !loaded { - self.beforeStart() + // Build the new content off-screen, leaving the previous render + // displayed until we swap in below; this is what keeps an async + // re-render from showing a half-loaded buffer. + self.beginRender() if prefix != "" { writeToView([]byte(prefix)) } @@ -353,15 +484,37 @@ func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix loadingMutex.Unlock() if !ok { - // if we're here then there's nothing left to scan from the source - // so we're at the EOF and can flush the stale content. - // onEndOfInput reads the view's dimensions (to decide - // whether to scroll) and sets the origin, both of which - // are UI-thread-only, so run it there. - _ = self.onUIThread(func() error { + // lineChan is closed. At a genuine end of input we swap in what we + // read and finalize. But lineChan is also closed when this task has + // been stopped to make way for a newer one: stopping closes + // opts.Stop, and the scanner goroutine then closes lineChan, so the + // select above can land here instead of on the opts.Stop case. A + // stopped task is being replaced and must leave the view to the + // incoming task — swapping in its half-read buffer, clamping the + // origin, or clearing `loading` would all corrupt what that task is + // about to render. So bail out here, the same as the explicit stop + // case above. + select { + case <-opts.Stop: + callThen() + break outer + default: + } + // Genuine end of input: do the first paint now if it hasn't happened + // yet (the content was shorter than a screenful, so we never reached + // the point below), and flush the stale content. onEndOfInput reads + // the view's dimensions (to decide whether to scroll) and sets the + // origin, both of which are UI-thread-only, so run it there — as is + // firstPaint, which also writes the origin. + _ = self.onUIThread(func() { + firstPaint() self.onEndOfInput() - return nil }) + // The content is fully loaded now, so it's safe again for the + // layout to clamp the scroll position to it. We deliberately + // don't clear this when stopped (rather than EOF'd), because that + // means a newer task is taking over and is still loading. + self.loading.Store(false) callThen() break outer } @@ -369,10 +522,15 @@ func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix lineWrittenChan <- struct{}{} linesRead++ + if slowRenderPerLine > 0 { + time.Sleep(slowRenderPerLine) + } + if linesRead == linesToRead.InitialRefreshAfter { - // We have read enough lines to fill the view, so do a first refresh - // here to show what we have. Continue reading and refresh again at - // the end to make sure the scrollbar has the right size. + // We have read enough lines to fill the view, so do the first paint + // and refresh to show it. Continue reading and refresh again at the + // end to make sure the scrollbar has the right size. + _ = self.onUIThread(firstPaint) refreshViewIfStale() } } @@ -382,7 +540,11 @@ func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix } } - self.readLines.Store(nil) + // Whoever made a request the loop never got to is waiting to hear that the + // content it asked for has been read, and there is nothing here to read it + // any more: at end of input it has all been read already, and a task that + // was stopped is handing the view over to the one replacing it. + self.stopServingReadRequests() refreshViewIfStale() @@ -406,7 +568,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix close(lineWrittenChan) }) - readLines <- linesToRead + self.readRequests.enqueue(linesToRead) <-done @@ -491,23 +653,19 @@ func (self *ViewBufferManager) NewTask(f func(TaskOpts) error, key string) error return } - resetOrigin := self.GetTaskKey() != key && self.onNewKey != nil + // Note we don't reset the origin here even when the command key changed: + // that's deferred to the first paint that reveals the new content (see + // newContentPending), so the previous content — left displayed until the + // swap — doesn't visibly jump to the top before the new content appears. + // Read taskKey directly: we already hold the mutex that guards it, and + // GetTaskKey would take it again. + if self.taskKey != key && self.resetOrigin != nil { + self.newContentPending.Store(true) + } self.taskKey = key self.taskIDMutex.Unlock() - if resetOrigin { - // onNewKey resets the view's scroll origin, which is view state the - // UI thread reads while laying out and drawing, so do it there. This - // must happen after releasing taskIDMutex: it blocks until the UI - // thread runs it, and a NewTask call on the UI thread takes - // taskIDMutex, so holding it here would deadlock. - _ = self.onUIThread(func() error { - self.onNewKey() - return nil - }) - } - self.waitingMutex.Lock() // Re-check staleness after acquiring waitingMutex: a newer task @@ -524,7 +682,8 @@ func (self *ViewBufferManager) NewTask(f func(TaskOpts) error, key string) error self.stopCurrentTask() } - self.readLines.Store(nil) + // Nothing serves read requests between one task and the next. + self.stopServingReadRequests() stop := make(chan struct{}) notifyStopped := make(chan struct{}) diff --git a/pkg/tasks/tasks_test.go b/pkg/tasks/tasks_test.go index 2cea139e8..c50a54cac 100644 --- a/pkg/tasks/tasks_test.go +++ b/pkg/tasks/tasks_test.go @@ -7,11 +7,13 @@ import ( "reflect" "strings" "sync" + "sync/atomic" "testing" "time" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/stretchr/testify/assert" ) func getCounter() (func(), func() int) { @@ -24,7 +26,9 @@ func TestNewCmdTaskInstantStop(t *testing.T) { beforeStart, getBeforeStartCallCount := getCounter() refreshView, getRefreshViewCallCount := getCounter() onEndOfInput, getOnEndOfInputCallCount := getCounter() - onNewKey, getOnNewKeyCallCount := getCounter() + resetOrigin, getResetOriginCallCount := getCounter() + beginRender, getBeginRenderCallCount := getCounter() + swapInRender, getSwapInRenderCallCount := getCounter() onDone, getOnDoneCallCount := getCounter() task := gocui.NewFakeTask() newTask := func() gocui.Task { @@ -37,10 +41,12 @@ func TestNewCmdTaskInstantStop(t *testing.T) { beforeStart, refreshView, onEndOfInput, - onNewKey, + resetOrigin, + beginRender, + swapInRender, newTask, // no UI thread in the test; run the view mutations inline - func(f func() error) error { return f() }, + func(f func()) error { f(); return nil }, ) stop := make(chan struct{}) @@ -66,7 +72,9 @@ func TestNewCmdTaskInstantStop(t *testing.T) { {0, getBeforeStartCallCount(), "beforeStart"}, {1, getRefreshViewCallCount(), "refreshView"}, {0, getOnEndOfInputCallCount(), "onEndOfInput"}, - {0, getOnNewKeyCallCount(), "onNewKey"}, + {0, getResetOriginCallCount(), "resetOrigin"}, + {0, getBeginRenderCallCount(), "beginRender"}, + {0, getSwapInRenderCallCount(), "swapInRender"}, {1, getOnDoneCallCount(), "onDone"}, } for _, expectation := range callCountExpectations { @@ -91,7 +99,9 @@ func TestNewCmdTask(t *testing.T) { beforeStart, getBeforeStartCallCount := getCounter() refreshView, getRefreshViewCallCount := getCounter() onEndOfInput, getOnEndOfInputCallCount := getCounter() - onNewKey, getOnNewKeyCallCount := getCounter() + resetOrigin, getResetOriginCallCount := getCounter() + beginRender, getBeginRenderCallCount := getCounter() + swapInRender, getSwapInRenderCallCount := getCounter() onDone, getOnDoneCallCount := getCounter() task := gocui.NewFakeTask() newTask := func() gocui.Task { @@ -104,10 +114,12 @@ func TestNewCmdTask(t *testing.T) { beforeStart, refreshView, onEndOfInput, - onNewKey, + resetOrigin, + beginRender, + swapInRender, newTask, // no UI thread in the test; run the view mutations inline - func(f func() error) error { return f() }, + func(f func()) error { f(); return nil }, ) stop := make(chan struct{}) @@ -134,10 +146,12 @@ func TestNewCmdTask(t *testing.T) { actual int name string }{ - {1, getBeforeStartCallCount(), "beforeStart"}, + {0, getBeforeStartCallCount(), "beforeStart"}, {1, getRefreshViewCallCount(), "refreshView"}, {1, getOnEndOfInputCallCount(), "onEndOfInput"}, - {0, getOnNewKeyCallCount(), "onNewKey"}, + {0, getResetOriginCallCount(), "resetOrigin"}, + {1, getBeginRenderCallCount(), "beginRender"}, + {1, getSwapInRenderCallCount(), "swapInRender"}, {1, getOnDoneCallCount(), "onDone"}, } for _, expectation := range callCountExpectations { @@ -174,6 +188,203 @@ func (d *BlankLineReader) Read(p []byte) (n int, err error) { return 1, nil } +// A dummy reader that yields the given number of blank lines and then blocks +// until unblock is closed, at which point it reports EOF. This lets a test hold +// a task in its "still loading" state for as long as it needs to. +type BlockingLineReader struct { + linesToYield int + linesYielded int + reachedEnd bool + blocked chan struct{} + unblock chan struct{} +} + +func (d *BlockingLineReader) Read(p []byte) (n int, err error) { + if d.linesYielded == d.linesToYield { + if !d.reachedEnd { + d.reachedEnd = true + close(d.blocked) + } + <-d.unblock + return 0, io.EOF + } + + d.linesYielded++ + p[0] = '\n' + return 1, nil +} + +func TestNewCmdTaskQueuedReadAtEndOfInput(t *testing.T) { + writer := bytes.NewBuffer(nil) + task := gocui.NewFakeTask() + + manager := NewViewBufferManager( + utils.NewDummyLog(), + writer, + func() {}, + func() {}, + func() {}, + func() {}, + func() {}, + func() {}, + func() gocui.Task { return task }, + // no UI thread in the test; run the view mutations inline + func(f func()) error { f(); return nil }, + ) + + reader := BlockingLineReader{ + linesToYield: 5, + blocked: make(chan struct{}), + unblock: make(chan struct{}), + } + start := func() (Cmd, io.Reader) { + // not actually starting this because it's not necessary + return ExecCmd{Cmd: exec.Command("blah")}, &reader + } + + // The initial request asks for far more lines than the reader has, so the + // task reaches EOF while that request is still the one being served. + fn := manager.NewCmdTask(start, "", LinesToRead{100, -1, nil}, func() {}) + + thenCalled := false + wg := sync.WaitGroup{} + wg.Go(func() { + _ = fn(TaskOpts{Stop: make(chan struct{}), InitialContentLoaded: func() { task.Done() }}) + }) + + <-reader.blocked + // The request is queued by the time this returns, so it is outstanding when we + // let the task reach EOF below. + manager.ReadToEnd(func() { thenCalled = true }) + close(reader.unblock) + + wg.Wait() + + assert.True(t, thenCalled) +} + +// A task rendering content the view wasn't already showing resets the scroll +// position to the top, at its first paint. If it is stopped and replaced before +// it ever paints — a background refresh landing just after the user clicked a +// different item, say — the replacement renders the same content and so decides +// on no reset of its own; it has to perform the one the stopped task was owed, +// or the view keeps the scroll position of the content it showed before. +func TestResetOriginSurvivesTaskReplacement(t *testing.T) { + resetOrigin, getResetOriginCallCount := getCounter() + + manager := NewViewBufferManager( + utils.NewDummyLog(), + bytes.NewBuffer(nil), + func() {}, + func() {}, + func() {}, + resetOrigin, + func() {}, + func() {}, + func() gocui.Task { return gocui.NewFakeTask() }, + // no UI thread in the test; run the view mutations inline + func(f func()) error { f(); return nil }, + ) + + startTask := func(key string, reader io.Reader, onDone func()) { + start := func() (Cmd, io.Reader) { + // not actually starting this because it's not necessary + return ExecCmd{Cmd: exec.Command("blah")}, reader + } + // The first-paint point is far beyond what any of these readers yield, so + // only reaching EOF paints. + _ = manager.NewTask(manager.NewCmdTask(start, "", LinesToRead{100, 50, nil}, onDone), key) + } + runTaskToCompletion := func(key string) { + done := make(chan struct{}) + startTask(key, &BlankLineReader{totalLinesToYield: 3}, func() { close(done) }) + <-done + } + + // A render of content the view wasn't showing resets the scroll position. + runTaskToCompletion("cmd1") + assert.Equal(t, 1, getResetOriginCallCount()) + + // Different content again, but this task stalls before it can paint. + stalled := BlockingLineReader{ + linesToYield: 3, + blocked: make(chan struct{}), + unblock: make(chan struct{}), + } + defer close(stalled.unblock) + startTask("cmd2", &stalled, nil) + <-stalled.blocked + + // The replacement shows the same content as the stalled task, so it has no + // reset of its own to do — but it must still do that task's. + runTaskToCompletion("cmd2") + assert.Equal(t, 2, getResetOriginCallCount()) +} + +// A render that takes long enough to start takes the view over to say +// "loading...", which means blanking whatever it was showing. That is only worth +// doing when the content coming is different from what's on screen: re-rendering +// the same content (a background refresh, say) would otherwise blank the view and +// paint the same thing back, a visible flicker for nothing. +func TestLoadingIndicatorOnlyTakesOverForNewContent(t *testing.T) { + var beforeStartCount atomic.Int32 + + manager := NewViewBufferManager( + utils.NewDummyLog(), + io.Discard, + func() { beforeStartCount.Add(1) }, + func() {}, + func() {}, + func() {}, + func() {}, + func() {}, + func() gocui.Task { return gocui.NewFakeTask() }, + // no UI thread in the test; run the view mutations inline + func(f func()) error { f(); return nil }, + ) + + startTask := func(key string, reader io.Reader, onDone func()) { + start := func() (Cmd, io.Reader) { + // not actually starting this because it's not necessary + return ExecCmd{Cmd: exec.Command("blah")}, reader + } + _ = manager.NewTask(manager.NewCmdTask(start, "", LinesToRead{100, 50, nil}, onDone), key) + } + // Starts a task whose command produces nothing at all, so that it is still + // waiting for its first line when the loading indicator falls due. Returns + // the reader so the caller can let it finish. + startStalledTask := func(key string) *BlockingLineReader { + reader := &BlockingLineReader{ + blocked: make(chan struct{}), + unblock: make(chan struct{}), + } + startTask(key, reader, nil) + <-reader.blocked + return reader + } + + // Get some content on screen first: the indicator is only due when a render + // is slow, and this one isn't. + done := make(chan struct{}) + startTask("cmd1", &BlankLineReader{totalLinesToYield: 3}, func() { close(done) }) + <-done + assert.EqualValues(t, 0, beforeStartCount.Load()) + + // A slow re-render of that same content must leave the view alone however + // long it takes. The indicator is due 200ms in, so give it well past that. + sameContent := startStalledTask("cmd1") + defer close(sameContent.unblock) + time.Sleep(500 * time.Millisecond) + assert.EqualValues(t, 0, beforeStartCount.Load()) + + // Different content, though, is worth taking the view over for. + newContent := startStalledTask("cmd2") + defer close(newContent.unblock) + assert.Eventually(t, + func() bool { return beforeStartCount.Load() == 1 }, + 2*time.Second, 10*time.Millisecond) +} + func TestNewCmdTaskRefresh(t *testing.T) { type scenario struct { name string @@ -240,9 +451,11 @@ func TestNewCmdTaskRefresh(t *testing.T) { refreshView, func() {}, func() {}, + func() {}, + func() {}, newTask, // no UI thread in the test; run the view mutations inline - func(f func() error) error { return f() }, + func(f func()) error { f(); return nil }, ) stop := make(chan struct{}) @@ -270,3 +483,42 @@ func TestNewCmdTaskRefresh(t *testing.T) { } } } + +// A read request is answered by the task that serves it calling the request's Then. +// This checks that requests still waiting when the task is stopped are answered too, +// which is what happens when a re-render replaces the task. +func TestQueuedReadRequestsAreAnsweredWhenTheTaskStops(t *testing.T) { + noop := func() {} + task := gocui.NewFakeTask() + + // A pipe the task blocks on, so that the requests are still waiting when it stops. + pipeReader, pipeWriter := io.Pipe() + defer pipeWriter.Close() + + manager := NewViewBufferManager( + utils.NewDummyLog(), bytes.NewBuffer(nil), noop, noop, noop, noop, noop, noop, + func() gocui.Task { return task }, + func(f func()) error { f(); return nil }, + ) + + stop := make(chan struct{}) + fn := manager.NewCmdTask( + func() (Cmd, io.Reader) { return ExecCmd{Cmd: exec.Command("true")}, pipeReader }, + "", LinesToRead{Total: 1, InitialRefreshAfter: -1}, noop) + go func() { _, _ = pipeWriter.Write([]byte("first line\n")) }() + go func() { _ = fn(TaskOpts{Stop: stop, InitialContentLoaded: noop}) }() + // Let the task start and read the line it was asked for, so that the requests + // below are handed to a task that is waiting for them. + time.Sleep(50 * time.Millisecond) + + answered := atomic.Int32{} + manager.ReadToEnd(func() { answered.Add(1) }) + manager.ReadToEnd(func() { answered.Add(1) }) + + // Let the first request be picked up and block on the pipe, then stop the task. + time.Sleep(50 * time.Millisecond) + close(stop) + time.Sleep(50 * time.Millisecond) + + assert.EqualValues(t, 2, answered.Load()) +} diff --git a/pkg/utils/project_root.go b/pkg/utils/project_root.go new file mode 100644 index 000000000..54dd9f616 --- /dev/null +++ b/pkg/utils/project_root.go @@ -0,0 +1,69 @@ +package utils + +import ( + "fmt" + "log" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/samber/lo" +) + +// FindLazygitRootDirectory returns the root directory of the lazygit source +// tree, by searching the working directory and its parents for the go.mod file +// that declares lazygit's module. Only development tools use it: the +// integration test runner, the cheatsheet generator, and the JSON schema +// generator. Not to be confused with finding the root directory of the +// repository that lazygit is being run in. +// +// We search upwards rather than expect to be called from the root directory, +// because `go test` runs each test binary in the source directory of its +// package, not in the directory that `go test` was invoked from. +func FindLazygitRootDirectory() (string, error) { + startDir, err := os.Getwd() + if err != nil { + return "", err + } + + dir := startDir + for { + if declaresLazygitModule(filepath.Join(dir, "go.mod")) { + return dir, nil + } + + parent := filepath.Dir(dir) + if parent == dir { + return "", fmt.Errorf( + "failed to find the lazygit root directory: there is no go.mod for a lazygit module in %s or any of its parent directories", + startDir) + } + dir = parent + } +} + +// MustFindLazygitRootDirectory is FindLazygitRootDirectory for tools that can't +// do anything useful if the directory isn't found. +func MustFindLazygitRootDirectory() string { + dir, err := FindLazygitRootDirectory() + if err != nil { + log.Fatal(err) + } + return dir +} + +// A fork is free to rename the module, so we accept any module path with a +// "lazygit" element in it, e.g. github.com/jesseduffield/lazygit. +func declaresLazygitModule(goModPath string) bool { + contents, err := os.ReadFile(goModPath) + if err != nil { + return false + } + + return lo.SomeBy(strings.Split(string(contents), "\n"), func(line string) bool { + fields := strings.Fields(line) + return len(fields) >= 2 && fields[0] == "module" && + slices.Contains(strings.Split(fields[1], "/"), "lazygit") + }) +} diff --git a/pkg/utils/project_root_test.go b/pkg/utils/project_root_test.go new file mode 100644 index 000000000..c5b1c8d28 --- /dev/null +++ b/pkg/utils/project_root_test.go @@ -0,0 +1,86 @@ +package utils + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFindLazygitRootDirectory(t *testing.T) { + // This test runs in the pkg/utils directory, so we expect the function to + // search two levels up for the project root. + expectedRootDir, err := filepath.Abs(filepath.Join("..", "..")) + assert.NoError(t, err) + + rootDir, err := FindLazygitRootDirectory() + + assert.NoError(t, err) + assert.Equal(t, expectedRootDir, rootDir) +} + +func TestFindLazygitRootDirectoryOutsideProject(t *testing.T) { + t.Chdir(t.TempDir()) + + _, err := FindLazygitRootDirectory() + + assert.ErrorContains(t, err, "there is no go.mod for a lazygit module") +} + +func TestDeclaresLazygitModule(t *testing.T) { + scenarios := []struct { + testName string + contents string + expected bool + }{ + { + testName: "lazygit's go.mod", + contents: "module github.com/jesseduffield/lazygit\n\ngo 1.25.0\n", + expected: true, + }, + { + testName: "a fork's go.mod", + contents: "module gitlab.com/somebody-else/lazygit\n\ngo 1.25.0\n", + expected: true, + }, + { + testName: "a fork's go.mod with a major version suffix", + contents: "module github.com/somebody-else/lazygit/v2\n\ngo 1.25.0\n", + expected: true, + }, + { + testName: "module declaration preceded by a comment", + contents: "// a comment\n\nmodule github.com/jesseduffield/lazygit\n", + expected: true, + }, + { + testName: "module declaration followed by a comment", + contents: "module github.com/jesseduffield/lazygit // comment\n\ngo 1.25.0\n", + expected: true, + }, + { + testName: "another project's go.mod", + contents: "module github.com/jesseduffield/lazydocker\n\ngo 1.25.0\n", + expected: false, + }, + { + testName: "no module declaration", + contents: "go 1.25.0\n", + expected: false, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.testName, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "go.mod") + assert.NoError(t, os.WriteFile(path, []byte(scenario.contents), 0o644)) + + assert.Equal(t, scenario.expected, declaresLazygitModule(path)) + }) + } +} + +func TestDeclaresLazygitModuleWithoutAGoModFile(t *testing.T) { + assert.False(t, declaresLazygitModule(filepath.Join(t.TempDir(), "go.mod"))) +} diff --git a/scripts/golangci-lint-shim.sh b/scripts/golangci-lint-shim.sh index a85ccc4d7..6cb3e007c 100755 --- a/scripts/golangci-lint-shim.sh +++ b/scripts/golangci-lint-shim.sh @@ -3,6 +3,6 @@ set -e # Must be kept in sync with the version in .github/workflows/ci.yml -version="v2.4.0" +version="v2.12.2" go run "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$version" "$@" diff --git a/vendor/github.com/gdamore/tcell/v3/cell.go b/vendor/github.com/gdamore/tcell/v3/cell.go index b3be03b13..73756b37d 100644 --- a/vendor/github.com/gdamore/tcell/v3/cell.go +++ b/vendor/github.com/gdamore/tcell/v3/cell.go @@ -14,6 +14,8 @@ package tcell +import "unicode/utf8" + type cell struct { currStr string lastStr string @@ -76,6 +78,10 @@ func (cb *CellBuffer) put(x int, y int, str string, style Style) (string, int) { // Identical re-Put (a full-screen redraw): the grapheme split is // unchanged, so reuse the measured width instead of segmenting. cl, width, str = str, c.width, "" + } else if len(str) > 0 && str[0] >= ' ' && str[0] <= '~' && (len(str) == 1 || str[1] < utf8.RuneSelf) { + // Printable ASCII followed by ASCII cannot be part of a larger + // grapheme cluster, so avoid constructing a grapheme iterator. + cl, width, str = str[:1], 1, str[1:] } else { g := textWidthOptions.StringGraphemes(str) for width == 0 && g.Next() { diff --git a/vendor/github.com/gdamore/tcell/v3/input.go b/vendor/github.com/gdamore/tcell/v3/input.go index 74e234d1e..0c279f998 100644 --- a/vendor/github.com/gdamore/tcell/v3/input.go +++ b/vendor/github.com/gdamore/tcell/v3/input.go @@ -63,10 +63,22 @@ const ( // before they can grow without bound while waiting for a string terminator. const defaultControlStringLimit = 64 * 1024 +const ( + // loneEscapeTimeout keeps bare Escape responsive when using legacy + // keyboard reporting, where ESC can also prefix an Alt-modified key. + loneEscapeTimeout = 200 * time.Millisecond + + // escapeSequenceTimeout bounds incomplete escape sequences. Once a + // sequence introducer has arrived, it is no longer ambiguous with a lone + // Escape and can tolerate a substantially longer inter-byte delay. + escapeSequenceTimeout = time.Second +) + func newInputParser(eq chan<- Event) *inputParser { return &inputParser{ evch: eq, buf: make([]rune, 0, 128), + legacy: true, controlStringMax: defaultControlStringLimit, } } @@ -91,6 +103,7 @@ type inputParser struct { nested *inputParser // for buggy win32-input-mode implementations surrogate rune // high surrogate pair seen (for Win32 input mode) advanced bool // use advanced key reporting semantics + legacy bool // keyboard protocol has ambiguous ESC prefixes controlStringMax int // maximum inbound OSC/XDA payload size; 0 means unlimited discardString bool // drop the rest of an over-limit OSC/XDA sequence } @@ -129,6 +142,35 @@ func (ip *inputParser) Waiting() bool { return ip.state != istInit } +// waitDuration reports how long to wait for the next byte before resetting an +// incomplete escape sequence. A bare ESC is only ambiguous with legacy +// keyboard reporting; other protocols can use the longer sequence deadline. +func (ip *inputParser) waitDuration() time.Duration { + if ip.state == istInit { + return 0 + } + if ip.state == istEsc && ip.legacy { + return loneEscapeTimeout + } + return escapeSequenceTimeout +} + +func (ip *inputParser) WaitDuration() time.Duration { + ip.l.Lock() + defer ip.l.Unlock() + return ip.waitDuration() +} + +func (ip *inputParser) SetKeyboardProtocol(protocol KeyProtocol) { + ip.l.Lock() + ip.legacy = protocol == LegacyKeyboard + nested := ip.nested + ip.l.Unlock() + if nested != nil { + nested.SetKeyboardProtocol(protocol) + } +} + // SetPixelMouse toggles whether SGR mouse reports are interpreted as // pixel coordinates (CSI ?1016h) rather than character cells (CSI ?1006h). // When enabled, mouse coordinates are not clipped to the screen size. @@ -488,25 +530,46 @@ var winKeys = map[int]Key{ 0x87: KeyF24, // vkF24 } +type ss3Key struct { + key Key + str string +} + // keys by their SS3 - used in application mode usually (legacy VT-style) -var ss3Keys = map[rune]Key{ - 'A': KeyUp, - 'B': KeyDown, - 'C': KeyRight, - 'D': KeyLeft, - 'E': KeyClear, - 'F': KeyEnd, - 'H': KeyHome, - 'P': KeyF1, - 'Q': KeyF2, - 'R': KeyF3, - 'S': KeyF4, - 't': KeyF5, - 'u': KeyF6, - 'v': KeyF7, - 'l': KeyF8, - 'w': KeyF9, - 'x': KeyF10, +var ss3Keys = map[rune]ss3Key{ + 'A': {key: KeyUp}, + 'B': {key: KeyDown}, + 'C': {key: KeyRight}, + 'D': {key: KeyLeft}, + 'E': {key: KeyClear}, + 'F': {key: KeyEnd}, + 'H': {key: KeyHome}, + 'P': {key: KeyF1}, + 'Q': {key: KeyF2}, + 'R': {key: KeyF3}, + 'S': {key: KeyF4}, + + // DEC application-keypad sequences. The VT100 terminfo entry calls some + // of these F5-F10, but that is a terminfo naming artifact: a VT100 has + // only PF1-PF4. Decode them by their PC keypad navigation meanings. + 'p': {key: KeyInsert}, + 'q': {key: KeyEnd}, + 'r': {key: KeyDown}, + 's': {key: KeyPgDn}, + 't': {key: KeyLeft}, + 'u': {key: KeyClear}, + 'v': {key: KeyRight}, + 'w': {key: KeyHome}, + 'x': {key: KeyUp}, + 'y': {key: KeyPgUp}, + 'M': {key: KeyEnter}, + 'n': {key: KeyDelete}, + 'j': {key: KeyRune, str: "*"}, + 'k': {key: KeyRune, str: "+"}, + 'l': {key: KeyRune, str: ","}, + 'm': {key: KeyRune, str: "-"}, + 'o': {key: KeyRune, str: "/"}, + 'X': {key: KeyRune, str: "="}, } // linux terminal uses these non ECMA keys prefixed by CSI-[ @@ -669,16 +732,16 @@ func (ip *inputParser) scan() { // parameters that do not match one of these forms, we just discard it. if len(ip.csiParams) == 0 { // simple SS3 case - ip.postKey(k, "", ModNone) + ip.postKey(k.key, k.str, ModNone) } else if parts := strings.Split(string(ip.csiParams), ";"); len(parts) >= 1 { // SS3 with modifier (old style). Note old terminfo would declare these as high // numbered function keys, but we encode as modified since that's how they are entered. if len(parts) >= 2 { if m, err := strconv.Atoi(parts[1]); err == nil && (parts[0] == "1" || parts[0] == "") { - ip.postKey(k, "", calcModifier(m)) + ip.postKey(k.key, k.str, calcModifier(m)) } } else if m, err := strconv.Atoi(parts[0]); err == nil { - ip.postKey(k, "", calcModifier(m)) + ip.postKey(k.key, k.str, calcModifier(m)) } } } @@ -757,7 +820,7 @@ func (ip *inputParser) scan() { } } - if ip.state != istInit && time.Since(ip.keyTime) > time.Millisecond*50 { + if timeout := ip.waitDuration(); timeout > 0 && time.Since(ip.keyTime) > timeout { if ip.state == istEsc { ip.postKey(KeyEscape, "", ModNone) } else if ec := ip.escChar; ec != 0 { @@ -1056,6 +1119,7 @@ func (ip *inputParser) handleWinKey(P []int) { rows: ip.rows, cols: ip.cols, advanced: ip.advanced, + legacy: ip.legacy, pixelMouse: ip.pixelMouse, controlStringMax: ip.controlStringMax, } @@ -1425,7 +1489,7 @@ func (ip *inputParser) handleCsi(mode rune, params []byte, intermediate []byte) // this might have been an SS3 style key with modifiers applied if k, ok := ss3Keys[mode]; ok && P0 == 1 && len(P) > 1 { - ip.postKeyEx(k, "", calcModifier(P[1]), pressed, 0, repeat) + ip.postKeyEx(k.key, k.str, calcModifier(P[1]), pressed, 0, repeat) return } // if we got here we just swallow the unknown sequence diff --git a/vendor/github.com/gdamore/tcell/v3/tscreen.go b/vendor/github.com/gdamore/tcell/v3/tscreen.go index d50101903..bbd31e316 100644 --- a/vendor/github.com/gdamore/tcell/v3/tscreen.go +++ b/vendor/github.com/gdamore/tcell/v3/tscreen.go @@ -286,6 +286,11 @@ type tScreen struct { advancedKeys bool controlStringLimit int input *inputParser + compat struct { + mouseUnsupported bool + focusUnsupported bool + clipboardReadUnsupported bool + } sync.Mutex } @@ -780,6 +785,10 @@ func (t *tScreen) emitAttrs(attrs AttrMask) { // The assumption is that sgr0 was already printed ahead of this. func (t *tScreen) emitUnderline(us UnderlineStyle, uc Color) { if us != UnderlineStyleNone { + if t.legacy { + t.Print(underline) + return + } // NB: under color should have been reset by sgr0 if uc.IsRGB() { r, g, b := uc.RGB() @@ -1100,6 +1109,9 @@ func (t *tScreen) enableMouse(f MouseFlags) { // so we enable the mouse unconditionally unless we get a report // that says we have mouse, but not SGR mouse. This is suboptimal, but // a concession forced by the sorry state of terminal emulators. + if t.compat.mouseUnsupported { + return + } if t.mouseDisabled { f = 0 } @@ -1203,10 +1215,16 @@ func (t *tScreen) DisableFocus() { } func (t *tScreen) enableFocusReporting() { + if t.compat.focusUnsupported { + return + } t.Print(vt.PmFocusReports.Enable()) } func (t *tScreen) disableFocusReporting() { + if t.compat.focusUnsupported { + return + } t.Print(vt.PmFocusReports.Disable()) } @@ -1336,13 +1354,18 @@ func (t *tScreen) mainLoop(stopQ chan struct{}) { case chunk := <-t.keyQ: buf.Write(chunk) t.scanInput(buf) - if t.input.Waiting() { - ta = time.After(time.Millisecond * 100) + if timeout := t.input.WaitDuration(); timeout > 0 { + ta = time.After(timeout) } else { ta = nil } case <-ta: t.input.Scan() + if timeout := t.input.WaitDuration(); timeout > 0 { + ta = time.After(timeout) + } else { + ta = nil + } } } } @@ -1373,7 +1396,11 @@ func (t *tScreen) inputLoop(stopQ chan struct{}) { return } if n > 0 { - t.keyQ <- chunk[:n] + select { + case t.keyQ <- chunk[:n]: + case <-t.quit: + return + } } } } @@ -1449,7 +1476,31 @@ func (t *tScreen) Tty() (Tty, bool) { return t.tty, true } -func (t *tScreen) applyKnownTerminalProfile(goos, termProgram string) bool { +func isSTTerminal(term string) bool { + return term == "st" || strings.HasPrefix(term, "st-") +} + +func (t *tScreen) applyKnownTerminalProfile(goos, term, termProgram string) bool { + if isSTTerminal(term) { + // st implements a small subset of xterm extensions. In particular, + // it has neither an advanced keyboard protocol nor SGR mouse or focus + // reporting. It also reports unsupported CSI and OSC sequences to + // stderr, so avoid probing or using extensions it does not implement. + t.legacy = true + t.compat.mouseUnsupported = true + t.compat.focusUnsupported = true + t.enterUrl = "" + t.exitUrl = "" + t.setWinSize = "" + t.saveTitle = "" + t.restoreTitle = "" + t.setTitle = "\x1b]2;%s\x1b\\" + t.notifyDesktop = "" + t.compat.clipboardReadUnsupported = true + t.termName = "st" + return true + } + switch termProgram { case "Apple_Terminal": // macOS Terminal.app cannot handle the startup queries, but it does @@ -1525,7 +1576,7 @@ func (t *tScreen) engageLocked() error { // Eventually they'll hopefully fix this. As the environment variable // does not convey by default via ssh, remote sessions might see spurious characters // emitted during startup. See the blog post for alternatives. - if !t.applyKnownTerminalProfile(runtime.GOOS, os.Getenv("TERM_PROGRAM")) && t.negotiate { + if !t.applyKnownTerminalProfile(runtime.GOOS, t.term, os.Getenv("TERM_PROGRAM")) && t.negotiate { if useVTWindowSizeQuery(runtime.GOOS) { t.Print(requestWindowSize) } @@ -1551,6 +1602,7 @@ func (t *tScreen) engageLocked() error { } t.processInitQ() t.applyKeyboardProtocolOverride() + t.input.SetKeyboardProtocol(t.keyboardProtocol()) if t.useAltScreen() { // Technically this may not be right, but every terminal we know about // (even Wyse 60) uses this to enter the alternate screen buffer, and @@ -1589,7 +1641,7 @@ func (t *tScreen) engageLocked() error { if t.title != "" && t.setTitle != "" { t.Printf(t.setTitle, t.title) } - if t.negotiate && useVTWindowSizeQuery(runtime.GOOS) { + if t.negotiate && !t.legacy && useVTWindowSizeQuery(runtime.GOOS) { t.Print(requestWindowSize) } @@ -1744,7 +1796,7 @@ func (t *tScreen) GetClipboard() { t.Unlock() return } - if t.setClipboard != "" { + if !t.compat.clipboardReadUnsupported && t.setClipboard != "" { t.Printf(t.setClipboard, "?") } t.Unlock() @@ -1773,6 +1825,11 @@ func (t *tScreen) Terminal() (string, string) { func (t *tScreen) KeyboardProtocol() KeyProtocol { t.Lock() defer t.Unlock() + return t.keyboardProtocol() +} + +// keyboardProtocol reports the selected keyboard protocol while t is locked. +func (t *tScreen) keyboardProtocol() KeyProtocol { if t.haveWin32Kbd { return Win32Keyboard } diff --git a/vendor/github.com/google/go-cmp/LICENSE b/vendor/github.com/google/go-cmp/LICENSE deleted file mode 100644 index 32017f8fa..000000000 --- a/vendor/github.com/google/go-cmp/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2017 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/go-cmp/cmp/compare.go b/vendor/github.com/google/go-cmp/cmp/compare.go deleted file mode 100644 index 0f5b8a48c..000000000 --- a/vendor/github.com/google/go-cmp/cmp/compare.go +++ /dev/null @@ -1,671 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package cmp determines equality of values. -// -// This package is intended to be a more powerful and safer alternative to -// [reflect.DeepEqual] for comparing whether two values are semantically equal. -// It is intended to only be used in tests, as performance is not a goal and -// it may panic if it cannot compare the values. Its propensity towards -// panicking means that its unsuitable for production environments where a -// spurious panic may be fatal. -// -// The primary features of cmp are: -// -// - When the default behavior of equality does not suit the test's needs, -// custom equality functions can override the equality operation. -// For example, an equality function may report floats as equal so long as -// they are within some tolerance of each other. -// -// - Types with an Equal method (e.g., [time.Time.Equal]) may use that method -// to determine equality. This allows package authors to determine -// the equality operation for the types that they define. -// -// - If no custom equality functions are used and no Equal method is defined, -// equality is determined by recursively comparing the primitive kinds on -// both values, much like [reflect.DeepEqual]. Unlike [reflect.DeepEqual], -// unexported fields are not compared by default; they result in panics -// unless suppressed by using an [Ignore] option -// (see [github.com/google/go-cmp/cmp/cmpopts.IgnoreUnexported]) -// or explicitly compared using the [Exporter] option. -package cmp - -import ( - "fmt" - "reflect" - "strings" - - "github.com/google/go-cmp/cmp/internal/diff" - "github.com/google/go-cmp/cmp/internal/function" - "github.com/google/go-cmp/cmp/internal/value" -) - -// TODO(≥go1.18): Use any instead of interface{}. - -// Equal reports whether x and y are equal by recursively applying the -// following rules in the given order to x and y and all of their sub-values: -// -// - Let S be the set of all [Ignore], [Transformer], and [Comparer] options that -// remain after applying all path filters, value filters, and type filters. -// If at least one [Ignore] exists in S, then the comparison is ignored. -// If the number of [Transformer] and [Comparer] options in S is non-zero, -// then Equal panics because it is ambiguous which option to use. -// If S contains a single [Transformer], then use that to transform -// the current values and recursively call Equal on the output values. -// If S contains a single [Comparer], then use that to compare the current values. -// Otherwise, evaluation proceeds to the next rule. -// -// - If the values have an Equal method of the form "(T) Equal(T) bool" or -// "(T) Equal(I) bool" where T is assignable to I, then use the result of -// x.Equal(y) even if x or y is nil. Otherwise, no such method exists and -// evaluation proceeds to the next rule. -// -// - Lastly, try to compare x and y based on their basic kinds. -// Simple kinds like booleans, integers, floats, complex numbers, strings, -// and channels are compared using the equivalent of the == operator in Go. -// Functions are only equal if they are both nil, otherwise they are unequal. -// -// Structs are equal if recursively calling Equal on all fields report equal. -// If a struct contains unexported fields, Equal panics unless an [Ignore] option -// (e.g., [github.com/google/go-cmp/cmp/cmpopts.IgnoreUnexported]) ignores that field -// or the [Exporter] option explicitly permits comparing the unexported field. -// -// Slices are equal if they are both nil or both non-nil, where recursively -// calling Equal on all non-ignored slice or array elements report equal. -// Empty non-nil slices and nil slices are not equal; to equate empty slices, -// consider using [github.com/google/go-cmp/cmp/cmpopts.EquateEmpty]. -// -// Maps are equal if they are both nil or both non-nil, where recursively -// calling Equal on all non-ignored map entries report equal. -// Map keys are equal according to the == operator. -// To use custom comparisons for map keys, consider using -// [github.com/google/go-cmp/cmp/cmpopts.SortMaps]. -// Empty non-nil maps and nil maps are not equal; to equate empty maps, -// consider using [github.com/google/go-cmp/cmp/cmpopts.EquateEmpty]. -// -// Pointers and interfaces are equal if they are both nil or both non-nil, -// where they have the same underlying concrete type and recursively -// calling Equal on the underlying values reports equal. -// -// Before recursing into a pointer, slice element, or map, the current path -// is checked to detect whether the address has already been visited. -// If there is a cycle, then the pointed at values are considered equal -// only if both addresses were previously visited in the same path step. -func Equal(x, y interface{}, opts ...Option) bool { - s := newState(opts) - s.compareAny(rootStep(x, y)) - return s.result.Equal() -} - -// Diff returns a human-readable report of the differences between two values: -// y - x. It returns an empty string if and only if Equal returns true for the -// same input values and options. -// -// The output is displayed as a literal in pseudo-Go syntax. -// At the start of each line, a "-" prefix indicates an element removed from x, -// a "+" prefix to indicates an element added from y, and the lack of a prefix -// indicates an element common to both x and y. If possible, the output -// uses fmt.Stringer.String or error.Error methods to produce more humanly -// readable outputs. In such cases, the string is prefixed with either an -// 's' or 'e' character, respectively, to indicate that the method was called. -// -// Do not depend on this output being stable. If you need the ability to -// programmatically interpret the difference, consider using a custom Reporter. -func Diff(x, y interface{}, opts ...Option) string { - s := newState(opts) - - // Optimization: If there are no other reporters, we can optimize for the - // common case where the result is equal (and thus no reported difference). - // This avoids the expensive construction of a difference tree. - if len(s.reporters) == 0 { - s.compareAny(rootStep(x, y)) - if s.result.Equal() { - return "" - } - s.result = diff.Result{} // Reset results - } - - r := new(defaultReporter) - s.reporters = append(s.reporters, reporter{r}) - s.compareAny(rootStep(x, y)) - d := r.String() - if (d == "") != s.result.Equal() { - panic("inconsistent difference and equality results") - } - return d -} - -// rootStep constructs the first path step. If x and y have differing types, -// then they are stored within an empty interface type. -func rootStep(x, y interface{}) PathStep { - vx := reflect.ValueOf(x) - vy := reflect.ValueOf(y) - - // If the inputs are different types, auto-wrap them in an empty interface - // so that they have the same parent type. - var t reflect.Type - if !vx.IsValid() || !vy.IsValid() || vx.Type() != vy.Type() { - t = anyType - if vx.IsValid() { - vvx := reflect.New(t).Elem() - vvx.Set(vx) - vx = vvx - } - if vy.IsValid() { - vvy := reflect.New(t).Elem() - vvy.Set(vy) - vy = vvy - } - } else { - t = vx.Type() - } - - return &pathStep{t, vx, vy} -} - -type state struct { - // These fields represent the "comparison state". - // Calling statelessCompare must not result in observable changes to these. - result diff.Result // The current result of comparison - curPath Path // The current path in the value tree - curPtrs pointerPath // The current set of visited pointers - reporters []reporter // Optional reporters - - // recChecker checks for infinite cycles applying the same set of - // transformers upon the output of itself. - recChecker recChecker - - // dynChecker triggers pseudo-random checks for option correctness. - // It is safe for statelessCompare to mutate this value. - dynChecker dynChecker - - // These fields, once set by processOption, will not change. - exporters []exporter // List of exporters for structs with unexported fields - opts Options // List of all fundamental and filter options -} - -func newState(opts []Option) *state { - // Always ensure a validator option exists to validate the inputs. - s := &state{opts: Options{validator{}}} - s.curPtrs.Init() - s.processOption(Options(opts)) - return s -} - -func (s *state) processOption(opt Option) { - switch opt := opt.(type) { - case nil: - case Options: - for _, o := range opt { - s.processOption(o) - } - case coreOption: - type filtered interface { - isFiltered() bool - } - if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() { - panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt)) - } - s.opts = append(s.opts, opt) - case exporter: - s.exporters = append(s.exporters, opt) - case reporter: - s.reporters = append(s.reporters, opt) - default: - panic(fmt.Sprintf("unknown option %T", opt)) - } -} - -// statelessCompare compares two values and returns the result. -// This function is stateless in that it does not alter the current result, -// or output to any registered reporters. -func (s *state) statelessCompare(step PathStep) diff.Result { - // We do not save and restore curPath and curPtrs because all of the - // compareX methods should properly push and pop from them. - // It is an implementation bug if the contents of the paths differ from - // when calling this function to when returning from it. - - oldResult, oldReporters := s.result, s.reporters - s.result = diff.Result{} // Reset result - s.reporters = nil // Remove reporters to avoid spurious printouts - s.compareAny(step) - res := s.result - s.result, s.reporters = oldResult, oldReporters - return res -} - -func (s *state) compareAny(step PathStep) { - // Update the path stack. - s.curPath.push(step) - defer s.curPath.pop() - for _, r := range s.reporters { - r.PushStep(step) - defer r.PopStep() - } - s.recChecker.Check(s.curPath) - - // Cycle-detection for slice elements (see NOTE in compareSlice). - t := step.Type() - vx, vy := step.Values() - if si, ok := step.(SliceIndex); ok && si.isSlice && vx.IsValid() && vy.IsValid() { - px, py := vx.Addr(), vy.Addr() - if eq, visited := s.curPtrs.Push(px, py); visited { - s.report(eq, reportByCycle) - return - } - defer s.curPtrs.Pop(px, py) - } - - // Rule 1: Check whether an option applies on this node in the value tree. - if s.tryOptions(t, vx, vy) { - return - } - - // Rule 2: Check whether the type has a valid Equal method. - if s.tryMethod(t, vx, vy) { - return - } - - // Rule 3: Compare based on the underlying kind. - switch t.Kind() { - case reflect.Bool: - s.report(vx.Bool() == vy.Bool(), 0) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - s.report(vx.Int() == vy.Int(), 0) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - s.report(vx.Uint() == vy.Uint(), 0) - case reflect.Float32, reflect.Float64: - s.report(vx.Float() == vy.Float(), 0) - case reflect.Complex64, reflect.Complex128: - s.report(vx.Complex() == vy.Complex(), 0) - case reflect.String: - s.report(vx.String() == vy.String(), 0) - case reflect.Chan, reflect.UnsafePointer: - s.report(vx.Pointer() == vy.Pointer(), 0) - case reflect.Func: - s.report(vx.IsNil() && vy.IsNil(), 0) - case reflect.Struct: - s.compareStruct(t, vx, vy) - case reflect.Slice, reflect.Array: - s.compareSlice(t, vx, vy) - case reflect.Map: - s.compareMap(t, vx, vy) - case reflect.Ptr: - s.comparePtr(t, vx, vy) - case reflect.Interface: - s.compareInterface(t, vx, vy) - default: - panic(fmt.Sprintf("%v kind not handled", t.Kind())) - } -} - -func (s *state) tryOptions(t reflect.Type, vx, vy reflect.Value) bool { - // Evaluate all filters and apply the remaining options. - if opt := s.opts.filter(s, t, vx, vy); opt != nil { - opt.apply(s, vx, vy) - return true - } - return false -} - -func (s *state) tryMethod(t reflect.Type, vx, vy reflect.Value) bool { - // Check if this type even has an Equal method. - m, ok := t.MethodByName("Equal") - if !ok || !function.IsType(m.Type, function.EqualAssignable) { - return false - } - - eq := s.callTTBFunc(m.Func, vx, vy) - s.report(eq, reportByMethod) - return true -} - -func (s *state) callTRFunc(f, v reflect.Value, step Transform) reflect.Value { - if !s.dynChecker.Next() { - return f.Call([]reflect.Value{v})[0] - } - - // Run the function twice and ensure that we get the same results back. - // We run in goroutines so that the race detector (if enabled) can detect - // unsafe mutations to the input. - c := make(chan reflect.Value) - go detectRaces(c, f, v) - got := <-c - want := f.Call([]reflect.Value{v})[0] - if step.vx, step.vy = got, want; !s.statelessCompare(step).Equal() { - // To avoid false-positives with non-reflexive equality operations, - // we sanity check whether a value is equal to itself. - if step.vx, step.vy = want, want; !s.statelessCompare(step).Equal() { - return want - } - panic(fmt.Sprintf("non-deterministic function detected: %s", function.NameOf(f))) - } - return want -} - -func (s *state) callTTBFunc(f, x, y reflect.Value) bool { - if !s.dynChecker.Next() { - return f.Call([]reflect.Value{x, y})[0].Bool() - } - - // Swapping the input arguments is sufficient to check that - // f is symmetric and deterministic. - // We run in goroutines so that the race detector (if enabled) can detect - // unsafe mutations to the input. - c := make(chan reflect.Value) - go detectRaces(c, f, y, x) - got := <-c - want := f.Call([]reflect.Value{x, y})[0].Bool() - if !got.IsValid() || got.Bool() != want { - panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", function.NameOf(f))) - } - return want -} - -func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { - var ret reflect.Value - defer func() { - recover() // Ignore panics, let the other call to f panic instead - c <- ret - }() - ret = f.Call(vs)[0] -} - -func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { - var addr bool - var vax, vay reflect.Value // Addressable versions of vx and vy - - var mayForce, mayForceInit bool - step := StructField{&structField{}} - for i := 0; i < t.NumField(); i++ { - step.typ = t.Field(i).Type - step.vx = vx.Field(i) - step.vy = vy.Field(i) - step.name = t.Field(i).Name - step.idx = i - step.unexported = !isExported(step.name) - if step.unexported { - if step.name == "_" { - continue - } - // Defer checking of unexported fields until later to give an - // Ignore a chance to ignore the field. - if !vax.IsValid() || !vay.IsValid() { - // For retrieveUnexportedField to work, the parent struct must - // be addressable. Create a new copy of the values if - // necessary to make them addressable. - addr = vx.CanAddr() || vy.CanAddr() - vax = makeAddressable(vx) - vay = makeAddressable(vy) - } - if !mayForceInit { - for _, xf := range s.exporters { - mayForce = mayForce || xf(t) - } - mayForceInit = true - } - step.mayForce = mayForce - step.paddr = addr - step.pvx = vax - step.pvy = vay - step.field = t.Field(i) - } - s.compareAny(step) - } -} - -func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) { - isSlice := t.Kind() == reflect.Slice - if isSlice && (vx.IsNil() || vy.IsNil()) { - s.report(vx.IsNil() && vy.IsNil(), 0) - return - } - - // NOTE: It is incorrect to call curPtrs.Push on the slice header pointer - // since slices represents a list of pointers, rather than a single pointer. - // The pointer checking logic must be handled on a per-element basis - // in compareAny. - // - // A slice header (see reflect.SliceHeader) in Go is a tuple of a starting - // pointer P, a length N, and a capacity C. Supposing each slice element has - // a memory size of M, then the slice is equivalent to the list of pointers: - // [P+i*M for i in range(N)] - // - // For example, v[:0] and v[:1] are slices with the same starting pointer, - // but they are clearly different values. Using the slice pointer alone - // violates the assumption that equal pointers implies equal values. - - step := SliceIndex{&sliceIndex{pathStep: pathStep{typ: t.Elem()}, isSlice: isSlice}} - withIndexes := func(ix, iy int) SliceIndex { - if ix >= 0 { - step.vx, step.xkey = vx.Index(ix), ix - } else { - step.vx, step.xkey = reflect.Value{}, -1 - } - if iy >= 0 { - step.vy, step.ykey = vy.Index(iy), iy - } else { - step.vy, step.ykey = reflect.Value{}, -1 - } - return step - } - - // Ignore options are able to ignore missing elements in a slice. - // However, detecting these reliably requires an optimal differencing - // algorithm, for which diff.Difference is not. - // - // Instead, we first iterate through both slices to detect which elements - // would be ignored if standing alone. The index of non-discarded elements - // are stored in a separate slice, which diffing is then performed on. - var indexesX, indexesY []int - var ignoredX, ignoredY []bool - for ix := 0; ix < vx.Len(); ix++ { - ignored := s.statelessCompare(withIndexes(ix, -1)).NumDiff == 0 - if !ignored { - indexesX = append(indexesX, ix) - } - ignoredX = append(ignoredX, ignored) - } - for iy := 0; iy < vy.Len(); iy++ { - ignored := s.statelessCompare(withIndexes(-1, iy)).NumDiff == 0 - if !ignored { - indexesY = append(indexesY, iy) - } - ignoredY = append(ignoredY, ignored) - } - - // Compute an edit-script for slices vx and vy (excluding ignored elements). - edits := diff.Difference(len(indexesX), len(indexesY), func(ix, iy int) diff.Result { - return s.statelessCompare(withIndexes(indexesX[ix], indexesY[iy])) - }) - - // Replay the ignore-scripts and the edit-script. - var ix, iy int - for ix < vx.Len() || iy < vy.Len() { - var e diff.EditType - switch { - case ix < len(ignoredX) && ignoredX[ix]: - e = diff.UniqueX - case iy < len(ignoredY) && ignoredY[iy]: - e = diff.UniqueY - default: - e, edits = edits[0], edits[1:] - } - switch e { - case diff.UniqueX: - s.compareAny(withIndexes(ix, -1)) - ix++ - case diff.UniqueY: - s.compareAny(withIndexes(-1, iy)) - iy++ - default: - s.compareAny(withIndexes(ix, iy)) - ix++ - iy++ - } - } -} - -func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), 0) - return - } - - // Cycle-detection for maps. - if eq, visited := s.curPtrs.Push(vx, vy); visited { - s.report(eq, reportByCycle) - return - } - defer s.curPtrs.Pop(vx, vy) - - // We combine and sort the two map keys so that we can perform the - // comparisons in a deterministic order. - step := MapIndex{&mapIndex{pathStep: pathStep{typ: t.Elem()}}} - for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) { - step.vx = vx.MapIndex(k) - step.vy = vy.MapIndex(k) - step.key = k - if !step.vx.IsValid() && !step.vy.IsValid() { - // It is possible for both vx and vy to be invalid if the - // key contained a NaN value in it. - // - // Even with the ability to retrieve NaN keys in Go 1.12, - // there still isn't a sensible way to compare the values since - // a NaN key may map to multiple unordered values. - // The most reasonable way to compare NaNs would be to compare the - // set of values. However, this is impossible to do efficiently - // since set equality is provably an O(n^2) operation given only - // an Equal function. If we had a Less function or Hash function, - // this could be done in O(n*log(n)) or O(n), respectively. - // - // Rather than adding complex logic to deal with NaNs, make it - // the user's responsibility to compare such obscure maps. - const help = "consider providing a Comparer to compare the map" - panic(fmt.Sprintf("%#v has map key with NaNs\n%s", s.curPath, help)) - } - s.compareAny(step) - } -} - -func (s *state) comparePtr(t reflect.Type, vx, vy reflect.Value) { - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), 0) - return - } - - // Cycle-detection for pointers. - if eq, visited := s.curPtrs.Push(vx, vy); visited { - s.report(eq, reportByCycle) - return - } - defer s.curPtrs.Pop(vx, vy) - - vx, vy = vx.Elem(), vy.Elem() - s.compareAny(Indirect{&indirect{pathStep{t.Elem(), vx, vy}}}) -} - -func (s *state) compareInterface(t reflect.Type, vx, vy reflect.Value) { - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), 0) - return - } - vx, vy = vx.Elem(), vy.Elem() - if vx.Type() != vy.Type() { - s.report(false, 0) - return - } - s.compareAny(TypeAssertion{&typeAssertion{pathStep{vx.Type(), vx, vy}}}) -} - -func (s *state) report(eq bool, rf resultFlags) { - if rf&reportByIgnore == 0 { - if eq { - s.result.NumSame++ - rf |= reportEqual - } else { - s.result.NumDiff++ - rf |= reportUnequal - } - } - for _, r := range s.reporters { - r.Report(Result{flags: rf}) - } -} - -// recChecker tracks the state needed to periodically perform checks that -// user provided transformers are not stuck in an infinitely recursive cycle. -type recChecker struct{ next int } - -// Check scans the Path for any recursive transformers and panics when any -// recursive transformers are detected. Note that the presence of a -// recursive Transformer does not necessarily imply an infinite cycle. -// As such, this check only activates after some minimal number of path steps. -func (rc *recChecker) Check(p Path) { - const minLen = 1 << 16 - if rc.next == 0 { - rc.next = minLen - } - if len(p) < rc.next { - return - } - rc.next <<= 1 - - // Check whether the same transformer has appeared at least twice. - var ss []string - m := map[Option]int{} - for _, ps := range p { - if t, ok := ps.(Transform); ok { - t := t.Option() - if m[t] == 1 { // Transformer was used exactly once before - tf := t.(*transformer).fnc.Type() - ss = append(ss, fmt.Sprintf("%v: %v => %v", t, tf.In(0), tf.Out(0))) - } - m[t]++ - } - } - if len(ss) > 0 { - const warning = "recursive set of Transformers detected" - const help = "consider using cmpopts.AcyclicTransformer" - set := strings.Join(ss, "\n\t") - panic(fmt.Sprintf("%s:\n\t%s\n%s", warning, set, help)) - } -} - -// dynChecker tracks the state needed to periodically perform checks that -// user provided functions are symmetric and deterministic. -// The zero value is safe for immediate use. -type dynChecker struct{ curr, next int } - -// Next increments the state and reports whether a check should be performed. -// -// Checks occur every Nth function call, where N is a triangular number: -// -// 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ... -// -// See https://en.wikipedia.org/wiki/Triangular_number -// -// This sequence ensures that the cost of checks drops significantly as -// the number of functions calls grows larger. -func (dc *dynChecker) Next() bool { - ok := dc.curr == dc.next - if ok { - dc.curr = 0 - dc.next++ - } - dc.curr++ - return ok -} - -// makeAddressable returns a value that is always addressable. -// It returns the input verbatim if it is already addressable, -// otherwise it creates a new value and returns an addressable copy. -func makeAddressable(v reflect.Value) reflect.Value { - if v.CanAddr() { - return v - } - vc := reflect.New(v.Type()).Elem() - vc.Set(v) - return vc -} diff --git a/vendor/github.com/google/go-cmp/cmp/export.go b/vendor/github.com/google/go-cmp/cmp/export.go deleted file mode 100644 index 29f82fe6b..000000000 --- a/vendor/github.com/google/go-cmp/cmp/export.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmp - -import ( - "reflect" - "unsafe" -) - -// retrieveUnexportedField uses unsafe to forcibly retrieve any field from -// a struct such that the value has read-write permissions. -// -// The parent struct, v, must be addressable, while f must be a StructField -// describing the field to retrieve. If addr is false, -// then the returned value will be shallowed copied to be non-addressable. -func retrieveUnexportedField(v reflect.Value, f reflect.StructField, addr bool) reflect.Value { - ve := reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem() - if !addr { - // A field is addressable if and only if the struct is addressable. - // If the original parent value was not addressable, shallow copy the - // value to make it non-addressable to avoid leaking an implementation - // detail of how forcibly exporting a field works. - if ve.Kind() == reflect.Interface && ve.IsNil() { - return reflect.Zero(f.Type) - } - return reflect.ValueOf(ve.Interface()).Convert(f.Type) - } - return ve -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go deleted file mode 100644 index 36062a604..000000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !cmp_debug -// +build !cmp_debug - -package diff - -var debug debugger - -type debugger struct{} - -func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc { - return f -} -func (debugger) Update() {} -func (debugger) Finish() {} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go deleted file mode 100644 index a3b97a1ad..000000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build cmp_debug -// +build cmp_debug - -package diff - -import ( - "fmt" - "strings" - "sync" - "time" -) - -// The algorithm can be seen running in real-time by enabling debugging: -// go test -tags=cmp_debug -v -// -// Example output: -// === RUN TestDifference/#34 -// ┌───────────────────────────────┐ -// │ \ · · · · · · · · · · · · · · │ -// │ · # · · · · · · · · · · · · · │ -// │ · \ · · · · · · · · · · · · · │ -// │ · · \ · · · · · · · · · · · · │ -// │ · · · X # · · · · · · · · · · │ -// │ · · · # \ · · · · · · · · · · │ -// │ · · · · · # # · · · · · · · · │ -// │ · · · · · # \ · · · · · · · · │ -// │ · · · · · · · \ · · · · · · · │ -// │ · · · · · · · · \ · · · · · · │ -// │ · · · · · · · · · \ · · · · · │ -// │ · · · · · · · · · · \ · · # · │ -// │ · · · · · · · · · · · \ # # · │ -// │ · · · · · · · · · · · # # # · │ -// │ · · · · · · · · · · # # # # · │ -// │ · · · · · · · · · # # # # # · │ -// │ · · · · · · · · · · · · · · \ │ -// └───────────────────────────────┘ -// [.Y..M.XY......YXYXY.|] -// -// The grid represents the edit-graph where the horizontal axis represents -// list X and the vertical axis represents list Y. The start of the two lists -// is the top-left, while the ends are the bottom-right. The '·' represents -// an unexplored node in the graph. The '\' indicates that the two symbols -// from list X and Y are equal. The 'X' indicates that two symbols are similar -// (but not exactly equal) to each other. The '#' indicates that the two symbols -// are different (and not similar). The algorithm traverses this graph trying to -// make the paths starting in the top-left and the bottom-right connect. -// -// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents -// the currently established path from the forward and reverse searches, -// separated by a '|' character. - -const ( - updateDelay = 100 * time.Millisecond - finishDelay = 500 * time.Millisecond - ansiTerminal = true // ANSI escape codes used to move terminal cursor -) - -var debug debugger - -type debugger struct { - sync.Mutex - p1, p2 EditScript - fwdPath, revPath *EditScript - grid []byte - lines int -} - -func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc { - dbg.Lock() - dbg.fwdPath, dbg.revPath = p1, p2 - top := "┌─" + strings.Repeat("──", nx) + "┐\n" - row := "│ " + strings.Repeat("· ", nx) + "│\n" - btm := "└─" + strings.Repeat("──", nx) + "┘\n" - dbg.grid = []byte(top + strings.Repeat(row, ny) + btm) - dbg.lines = strings.Count(dbg.String(), "\n") - fmt.Print(dbg) - - // Wrap the EqualFunc so that we can intercept each result. - return func(ix, iy int) (r Result) { - cell := dbg.grid[len(top)+iy*len(row):][len("│ ")+len("· ")*ix:][:len("·")] - for i := range cell { - cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot - } - switch r = f(ix, iy); { - case r.Equal(): - cell[0] = '\\' - case r.Similar(): - cell[0] = 'X' - default: - cell[0] = '#' - } - return - } -} - -func (dbg *debugger) Update() { - dbg.print(updateDelay) -} - -func (dbg *debugger) Finish() { - dbg.print(finishDelay) - dbg.Unlock() -} - -func (dbg *debugger) String() string { - dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0] - for i := len(*dbg.revPath) - 1; i >= 0; i-- { - dbg.p2 = append(dbg.p2, (*dbg.revPath)[i]) - } - return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2) -} - -func (dbg *debugger) print(d time.Duration) { - if ansiTerminal { - fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor - } - fmt.Print(dbg) - time.Sleep(d) -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go deleted file mode 100644 index a248e5436..000000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go +++ /dev/null @@ -1,402 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package diff implements an algorithm for producing edit-scripts. -// The edit-script is a sequence of operations needed to transform one list -// of symbols into another (or vice-versa). The edits allowed are insertions, -// deletions, and modifications. The summation of all edits is called the -// Levenshtein distance as this problem is well-known in computer science. -// -// This package prioritizes performance over accuracy. That is, the run time -// is more important than obtaining a minimal Levenshtein distance. -package diff - -import ( - "math/rand" - "time" - - "github.com/google/go-cmp/cmp/internal/flags" -) - -// EditType represents a single operation within an edit-script. -type EditType uint8 - -const ( - // Identity indicates that a symbol pair is identical in both list X and Y. - Identity EditType = iota - // UniqueX indicates that a symbol only exists in X and not Y. - UniqueX - // UniqueY indicates that a symbol only exists in Y and not X. - UniqueY - // Modified indicates that a symbol pair is a modification of each other. - Modified -) - -// EditScript represents the series of differences between two lists. -type EditScript []EditType - -// String returns a human-readable string representing the edit-script where -// Identity, UniqueX, UniqueY, and Modified are represented by the -// '.', 'X', 'Y', and 'M' characters, respectively. -func (es EditScript) String() string { - b := make([]byte, len(es)) - for i, e := range es { - switch e { - case Identity: - b[i] = '.' - case UniqueX: - b[i] = 'X' - case UniqueY: - b[i] = 'Y' - case Modified: - b[i] = 'M' - default: - panic("invalid edit-type") - } - } - return string(b) -} - -// stats returns a histogram of the number of each type of edit operation. -func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) { - for _, e := range es { - switch e { - case Identity: - s.NI++ - case UniqueX: - s.NX++ - case UniqueY: - s.NY++ - case Modified: - s.NM++ - default: - panic("invalid edit-type") - } - } - return -} - -// Dist is the Levenshtein distance and is guaranteed to be 0 if and only if -// lists X and Y are equal. -func (es EditScript) Dist() int { return len(es) - es.stats().NI } - -// LenX is the length of the X list. -func (es EditScript) LenX() int { return len(es) - es.stats().NY } - -// LenY is the length of the Y list. -func (es EditScript) LenY() int { return len(es) - es.stats().NX } - -// EqualFunc reports whether the symbols at indexes ix and iy are equal. -// When called by Difference, the index is guaranteed to be within nx and ny. -type EqualFunc func(ix int, iy int) Result - -// Result is the result of comparison. -// NumSame is the number of sub-elements that are equal. -// NumDiff is the number of sub-elements that are not equal. -type Result struct{ NumSame, NumDiff int } - -// BoolResult returns a Result that is either Equal or not Equal. -func BoolResult(b bool) Result { - if b { - return Result{NumSame: 1} // Equal, Similar - } else { - return Result{NumDiff: 2} // Not Equal, not Similar - } -} - -// Equal indicates whether the symbols are equal. Two symbols are equal -// if and only if NumDiff == 0. If Equal, then they are also Similar. -func (r Result) Equal() bool { return r.NumDiff == 0 } - -// Similar indicates whether two symbols are similar and may be represented -// by using the Modified type. As a special case, we consider binary comparisons -// (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar. -// -// The exact ratio of NumSame to NumDiff to determine similarity may change. -func (r Result) Similar() bool { - // Use NumSame+1 to offset NumSame so that binary comparisons are similar. - return r.NumSame+1 >= r.NumDiff -} - -var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 - -// Difference reports whether two lists of lengths nx and ny are equal -// given the definition of equality provided as f. -// -// This function returns an edit-script, which is a sequence of operations -// needed to convert one list into the other. The following invariants for -// the edit-script are maintained: -// - eq == (es.Dist()==0) -// - nx == es.LenX() -// - ny == es.LenY() -// -// This algorithm is not guaranteed to be an optimal solution (i.e., one that -// produces an edit-script with a minimal Levenshtein distance). This algorithm -// favors performance over optimality. The exact output is not guaranteed to -// be stable and may change over time. -func Difference(nx, ny int, f EqualFunc) (es EditScript) { - // This algorithm is based on traversing what is known as an "edit-graph". - // See Figure 1 from "An O(ND) Difference Algorithm and Its Variations" - // by Eugene W. Myers. Since D can be as large as N itself, this is - // effectively O(N^2). Unlike the algorithm from that paper, we are not - // interested in the optimal path, but at least some "decent" path. - // - // For example, let X and Y be lists of symbols: - // X = [A B C A B B A] - // Y = [C B A B A C] - // - // The edit-graph can be drawn as the following: - // A B C A B B A - // ┌─────────────┐ - // C │_|_|\|_|_|_|_│ 0 - // B │_|\|_|_|\|\|_│ 1 - // A │\|_|_|\|_|_|\│ 2 - // B │_|\|_|_|\|\|_│ 3 - // A │\|_|_|\|_|_|\│ 4 - // C │ | |\| | | | │ 5 - // └─────────────┘ 6 - // 0 1 2 3 4 5 6 7 - // - // List X is written along the horizontal axis, while list Y is written - // along the vertical axis. At any point on this grid, if the symbol in - // list X matches the corresponding symbol in list Y, then a '\' is drawn. - // The goal of any minimal edit-script algorithm is to find a path from the - // top-left corner to the bottom-right corner, while traveling through the - // fewest horizontal or vertical edges. - // A horizontal edge is equivalent to inserting a symbol from list X. - // A vertical edge is equivalent to inserting a symbol from list Y. - // A diagonal edge is equivalent to a matching symbol between both X and Y. - - // Invariants: - // - 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx - // - 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny - // - // In general: - // - fwdFrontier.X < revFrontier.X - // - fwdFrontier.Y < revFrontier.Y - // - // Unless, it is time for the algorithm to terminate. - fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)} - revPath := path{-1, point{nx, ny}, make(EditScript, 0)} - fwdFrontier := fwdPath.point // Forward search frontier - revFrontier := revPath.point // Reverse search frontier - - // Search budget bounds the cost of searching for better paths. - // The longest sequence of non-matching symbols that can be tolerated is - // approximately the square-root of the search budget. - searchBudget := 4 * (nx + ny) // O(n) - - // Running the tests with the "cmp_debug" build tag prints a visualization - // of the algorithm running in real-time. This is educational for - // understanding how the algorithm works. See debug_enable.go. - f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) - - // The algorithm below is a greedy, meet-in-the-middle algorithm for - // computing sub-optimal edit-scripts between two lists. - // - // The algorithm is approximately as follows: - // - Searching for differences switches back-and-forth between - // a search that starts at the beginning (the top-left corner), and - // a search that starts at the end (the bottom-right corner). - // The goal of the search is connect with the search - // from the opposite corner. - // - As we search, we build a path in a greedy manner, - // where the first match seen is added to the path (this is sub-optimal, - // but provides a decent result in practice). When matches are found, - // we try the next pair of symbols in the lists and follow all matches - // as far as possible. - // - When searching for matches, we search along a diagonal going through - // through the "frontier" point. If no matches are found, - // we advance the frontier towards the opposite corner. - // - This algorithm terminates when either the X coordinates or the - // Y coordinates of the forward and reverse frontier points ever intersect. - - // This algorithm is correct even if searching only in the forward direction - // or in the reverse direction. We do both because it is commonly observed - // that two lists commonly differ because elements were added to the front - // or end of the other list. - // - // Non-deterministically start with either the forward or reverse direction - // to introduce some deliberate instability so that we have the flexibility - // to change this algorithm in the future. - if flags.Deterministic || randBool { - goto forwardSearch - } else { - goto reverseSearch - } - -forwardSearch: - { - // Forward search from the beginning. - if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { - goto finishSearch - } - for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { - // Search in a diagonal pattern for a match. - z := zigzag(i) - p := point{fwdFrontier.X + z, fwdFrontier.Y - z} - switch { - case p.X >= revPath.X || p.Y < fwdPath.Y: - stop1 = true // Hit top-right corner - case p.Y >= revPath.Y || p.X < fwdPath.X: - stop2 = true // Hit bottom-left corner - case f(p.X, p.Y).Equal(): - // Match found, so connect the path to this point. - fwdPath.connect(p, f) - fwdPath.append(Identity) - // Follow sequence of matches as far as possible. - for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { - if !f(fwdPath.X, fwdPath.Y).Equal() { - break - } - fwdPath.append(Identity) - } - fwdFrontier = fwdPath.point - stop1, stop2 = true, true - default: - searchBudget-- // Match not found - } - debug.Update() - } - // Advance the frontier towards reverse point. - if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y { - fwdFrontier.X++ - } else { - fwdFrontier.Y++ - } - goto reverseSearch - } - -reverseSearch: - { - // Reverse search from the end. - if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { - goto finishSearch - } - for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { - // Search in a diagonal pattern for a match. - z := zigzag(i) - p := point{revFrontier.X - z, revFrontier.Y + z} - switch { - case fwdPath.X >= p.X || revPath.Y < p.Y: - stop1 = true // Hit bottom-left corner - case fwdPath.Y >= p.Y || revPath.X < p.X: - stop2 = true // Hit top-right corner - case f(p.X-1, p.Y-1).Equal(): - // Match found, so connect the path to this point. - revPath.connect(p, f) - revPath.append(Identity) - // Follow sequence of matches as far as possible. - for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { - if !f(revPath.X-1, revPath.Y-1).Equal() { - break - } - revPath.append(Identity) - } - revFrontier = revPath.point - stop1, stop2 = true, true - default: - searchBudget-- // Match not found - } - debug.Update() - } - // Advance the frontier towards forward point. - if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y { - revFrontier.X-- - } else { - revFrontier.Y-- - } - goto forwardSearch - } - -finishSearch: - // Join the forward and reverse paths and then append the reverse path. - fwdPath.connect(revPath.point, f) - for i := len(revPath.es) - 1; i >= 0; i-- { - t := revPath.es[i] - revPath.es = revPath.es[:i] - fwdPath.append(t) - } - debug.Finish() - return fwdPath.es -} - -type path struct { - dir int // +1 if forward, -1 if reverse - point // Leading point of the EditScript path - es EditScript -} - -// connect appends any necessary Identity, Modified, UniqueX, or UniqueY types -// to the edit-script to connect p.point to dst. -func (p *path) connect(dst point, f EqualFunc) { - if p.dir > 0 { - // Connect in forward direction. - for dst.X > p.X && dst.Y > p.Y { - switch r := f(p.X, p.Y); { - case r.Equal(): - p.append(Identity) - case r.Similar(): - p.append(Modified) - case dst.X-p.X >= dst.Y-p.Y: - p.append(UniqueX) - default: - p.append(UniqueY) - } - } - for dst.X > p.X { - p.append(UniqueX) - } - for dst.Y > p.Y { - p.append(UniqueY) - } - } else { - // Connect in reverse direction. - for p.X > dst.X && p.Y > dst.Y { - switch r := f(p.X-1, p.Y-1); { - case r.Equal(): - p.append(Identity) - case r.Similar(): - p.append(Modified) - case p.Y-dst.Y >= p.X-dst.X: - p.append(UniqueY) - default: - p.append(UniqueX) - } - } - for p.X > dst.X { - p.append(UniqueX) - } - for p.Y > dst.Y { - p.append(UniqueY) - } - } -} - -func (p *path) append(t EditType) { - p.es = append(p.es, t) - switch t { - case Identity, Modified: - p.add(p.dir, p.dir) - case UniqueX: - p.add(p.dir, 0) - case UniqueY: - p.add(0, p.dir) - } - debug.Update() -} - -type point struct{ X, Y int } - -func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy } - -// zigzag maps a consecutive sequence of integers to a zig-zag sequence. -// -// [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...] -func zigzag(x int) int { - if x&1 != 0 { - x = ^x - } - return x >> 1 -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/flags/flags.go b/vendor/github.com/google/go-cmp/cmp/internal/flags/flags.go deleted file mode 100644 index d8e459c9b..000000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/flags/flags.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2019, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package flags - -// Deterministic controls whether the output of Diff should be deterministic. -// This is only used for testing. -var Deterministic bool diff --git a/vendor/github.com/google/go-cmp/cmp/internal/function/func.go b/vendor/github.com/google/go-cmp/cmp/internal/function/func.go deleted file mode 100644 index def01a6be..000000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/function/func.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package function provides functionality for identifying function types. -package function - -import ( - "reflect" - "regexp" - "runtime" - "strings" -) - -type funcType int - -const ( - _ funcType = iota - - tbFunc // func(T) bool - ttbFunc // func(T, T) bool - ttiFunc // func(T, T) int - trbFunc // func(T, R) bool - tibFunc // func(T, I) bool - trFunc // func(T) R - - Equal = ttbFunc // func(T, T) bool - EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool - Transformer = trFunc // func(T) R - ValueFilter = ttbFunc // func(T, T) bool - Less = ttbFunc // func(T, T) bool - Compare = ttiFunc // func(T, T) int - ValuePredicate = tbFunc // func(T) bool - KeyValuePredicate = trbFunc // func(T, R) bool -) - -var boolType = reflect.TypeOf(true) -var intType = reflect.TypeOf(0) - -// IsType reports whether the reflect.Type is of the specified function type. -func IsType(t reflect.Type, ft funcType) bool { - if t == nil || t.Kind() != reflect.Func || t.IsVariadic() { - return false - } - ni, no := t.NumIn(), t.NumOut() - switch ft { - case tbFunc: // func(T) bool - if ni == 1 && no == 1 && t.Out(0) == boolType { - return true - } - case ttbFunc: // func(T, T) bool - if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType { - return true - } - case ttiFunc: // func(T, T) int - if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == intType { - return true - } - case trbFunc: // func(T, R) bool - if ni == 2 && no == 1 && t.Out(0) == boolType { - return true - } - case tibFunc: // func(T, I) bool - if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType { - return true - } - case trFunc: // func(T) R - if ni == 1 && no == 1 { - return true - } - } - return false -} - -var lastIdentRx = regexp.MustCompile(`[_\p{L}][_\p{L}\p{N}]*$`) - -// NameOf returns the name of the function value. -func NameOf(v reflect.Value) string { - fnc := runtime.FuncForPC(v.Pointer()) - if fnc == nil { - return "" - } - fullName := fnc.Name() // e.g., "long/path/name/mypkg.(*MyType).(long/path/name/mypkg.myMethod)-fm" - - // Method closures have a "-fm" suffix. - fullName = strings.TrimSuffix(fullName, "-fm") - - var name string - for len(fullName) > 0 { - inParen := strings.HasSuffix(fullName, ")") - fullName = strings.TrimSuffix(fullName, ")") - - s := lastIdentRx.FindString(fullName) - if s == "" { - break - } - name = s + "." + name - fullName = strings.TrimSuffix(fullName, s) - - if i := strings.LastIndexByte(fullName, '('); inParen && i >= 0 { - fullName = fullName[:i] - } - fullName = strings.TrimSuffix(fullName, ".") - } - return strings.TrimSuffix(name, ".") -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/name.go b/vendor/github.com/google/go-cmp/cmp/internal/value/name.go deleted file mode 100644 index 7b498bb2c..000000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/value/name.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2020, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package value - -import ( - "reflect" - "strconv" -) - -var anyType = reflect.TypeOf((*interface{})(nil)).Elem() - -// TypeString is nearly identical to reflect.Type.String, -// but has an additional option to specify that full type names be used. -func TypeString(t reflect.Type, qualified bool) string { - return string(appendTypeName(nil, t, qualified, false)) -} - -func appendTypeName(b []byte, t reflect.Type, qualified, elideFunc bool) []byte { - // BUG: Go reflection provides no way to disambiguate two named types - // of the same name and within the same package, - // but declared within the namespace of different functions. - - // Use the "any" alias instead of "interface{}" for better readability. - if t == anyType { - return append(b, "any"...) - } - - // Named type. - if t.Name() != "" { - if qualified && t.PkgPath() != "" { - b = append(b, '"') - b = append(b, t.PkgPath()...) - b = append(b, '"') - b = append(b, '.') - b = append(b, t.Name()...) - } else { - b = append(b, t.String()...) - } - return b - } - - // Unnamed type. - switch k := t.Kind(); k { - case reflect.Bool, reflect.String, reflect.UnsafePointer, - reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, - reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: - b = append(b, k.String()...) - case reflect.Chan: - if t.ChanDir() == reflect.RecvDir { - b = append(b, "<-"...) - } - b = append(b, "chan"...) - if t.ChanDir() == reflect.SendDir { - b = append(b, "<-"...) - } - b = append(b, ' ') - b = appendTypeName(b, t.Elem(), qualified, false) - case reflect.Func: - if !elideFunc { - b = append(b, "func"...) - } - b = append(b, '(') - for i := 0; i < t.NumIn(); i++ { - if i > 0 { - b = append(b, ", "...) - } - if i == t.NumIn()-1 && t.IsVariadic() { - b = append(b, "..."...) - b = appendTypeName(b, t.In(i).Elem(), qualified, false) - } else { - b = appendTypeName(b, t.In(i), qualified, false) - } - } - b = append(b, ')') - switch t.NumOut() { - case 0: - // Do nothing - case 1: - b = append(b, ' ') - b = appendTypeName(b, t.Out(0), qualified, false) - default: - b = append(b, " ("...) - for i := 0; i < t.NumOut(); i++ { - if i > 0 { - b = append(b, ", "...) - } - b = appendTypeName(b, t.Out(i), qualified, false) - } - b = append(b, ')') - } - case reflect.Struct: - b = append(b, "struct{ "...) - for i := 0; i < t.NumField(); i++ { - if i > 0 { - b = append(b, "; "...) - } - sf := t.Field(i) - if !sf.Anonymous { - if qualified && sf.PkgPath != "" { - b = append(b, '"') - b = append(b, sf.PkgPath...) - b = append(b, '"') - b = append(b, '.') - } - b = append(b, sf.Name...) - b = append(b, ' ') - } - b = appendTypeName(b, sf.Type, qualified, false) - if sf.Tag != "" { - b = append(b, ' ') - b = strconv.AppendQuote(b, string(sf.Tag)) - } - } - if b[len(b)-1] == ' ' { - b = b[:len(b)-1] - } else { - b = append(b, ' ') - } - b = append(b, '}') - case reflect.Slice, reflect.Array: - b = append(b, '[') - if k == reflect.Array { - b = strconv.AppendUint(b, uint64(t.Len()), 10) - } - b = append(b, ']') - b = appendTypeName(b, t.Elem(), qualified, false) - case reflect.Map: - b = append(b, "map["...) - b = appendTypeName(b, t.Key(), qualified, false) - b = append(b, ']') - b = appendTypeName(b, t.Elem(), qualified, false) - case reflect.Ptr: - b = append(b, '*') - b = appendTypeName(b, t.Elem(), qualified, false) - case reflect.Interface: - b = append(b, "interface{ "...) - for i := 0; i < t.NumMethod(); i++ { - if i > 0 { - b = append(b, "; "...) - } - m := t.Method(i) - if qualified && m.PkgPath != "" { - b = append(b, '"') - b = append(b, m.PkgPath...) - b = append(b, '"') - b = append(b, '.') - } - b = append(b, m.Name...) - b = appendTypeName(b, m.Type, qualified, true) - } - if b[len(b)-1] == ' ' { - b = b[:len(b)-1] - } else { - b = append(b, ' ') - } - b = append(b, '}') - default: - panic("invalid kind: " + k.String()) - } - return b -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/pointer.go b/vendor/github.com/google/go-cmp/cmp/internal/value/pointer.go deleted file mode 100644 index e5dfff69a..000000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/value/pointer.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2018, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package value - -import ( - "reflect" - "unsafe" -) - -// Pointer is an opaque typed pointer and is guaranteed to be comparable. -type Pointer struct { - p unsafe.Pointer - t reflect.Type -} - -// PointerOf returns a Pointer from v, which must be a -// reflect.Ptr, reflect.Slice, or reflect.Map. -func PointerOf(v reflect.Value) Pointer { - // The proper representation of a pointer is unsafe.Pointer, - // which is necessary if the GC ever uses a moving collector. - return Pointer{unsafe.Pointer(v.Pointer()), v.Type()} -} - -// IsNil reports whether the pointer is nil. -func (p Pointer) IsNil() bool { - return p.p == nil -} - -// Uintptr returns the pointer as a uintptr. -func (p Pointer) Uintptr() uintptr { - return uintptr(p.p) -} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go b/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go deleted file mode 100644 index 98533b036..000000000 --- a/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package value - -import ( - "fmt" - "math" - "reflect" - "sort" -) - -// SortKeys sorts a list of map keys, deduplicating keys if necessary. -// The type of each value must be comparable. -func SortKeys(vs []reflect.Value) []reflect.Value { - if len(vs) == 0 { - return vs - } - - // Sort the map keys. - sort.SliceStable(vs, func(i, j int) bool { return isLess(vs[i], vs[j]) }) - - // Deduplicate keys (fails for NaNs). - vs2 := vs[:1] - for _, v := range vs[1:] { - if isLess(vs2[len(vs2)-1], v) { - vs2 = append(vs2, v) - } - } - return vs2 -} - -// isLess is a generic function for sorting arbitrary map keys. -// The inputs must be of the same type and must be comparable. -func isLess(x, y reflect.Value) bool { - switch x.Type().Kind() { - case reflect.Bool: - return !x.Bool() && y.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return x.Int() < y.Int() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return x.Uint() < y.Uint() - case reflect.Float32, reflect.Float64: - // NOTE: This does not sort -0 as less than +0 - // since Go maps treat -0 and +0 as equal keys. - fx, fy := x.Float(), y.Float() - return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) - case reflect.Complex64, reflect.Complex128: - cx, cy := x.Complex(), y.Complex() - rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy) - if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) { - return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy) - } - return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry) - case reflect.Ptr, reflect.UnsafePointer, reflect.Chan: - return x.Pointer() < y.Pointer() - case reflect.String: - return x.String() < y.String() - case reflect.Array: - for i := 0; i < x.Len(); i++ { - if isLess(x.Index(i), y.Index(i)) { - return true - } - if isLess(y.Index(i), x.Index(i)) { - return false - } - } - return false - case reflect.Struct: - for i := 0; i < x.NumField(); i++ { - if isLess(x.Field(i), y.Field(i)) { - return true - } - if isLess(y.Field(i), x.Field(i)) { - return false - } - } - return false - case reflect.Interface: - vx, vy := x.Elem(), y.Elem() - if !vx.IsValid() || !vy.IsValid() { - return !vx.IsValid() && vy.IsValid() - } - tx, ty := vx.Type(), vy.Type() - if tx == ty { - return isLess(x.Elem(), y.Elem()) - } - if tx.Kind() != ty.Kind() { - return vx.Kind() < vy.Kind() - } - if tx.String() != ty.String() { - return tx.String() < ty.String() - } - if tx.PkgPath() != ty.PkgPath() { - return tx.PkgPath() < ty.PkgPath() - } - // This can happen in rare situations, so we fallback to just comparing - // the unique pointer for a reflect.Type. This guarantees deterministic - // ordering within a program, but it is obviously not stable. - return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer() - default: - // Must be Func, Map, or Slice; which are not comparable. - panic(fmt.Sprintf("%T is not comparable", x.Type())) - } -} diff --git a/vendor/github.com/google/go-cmp/cmp/options.go b/vendor/github.com/google/go-cmp/cmp/options.go deleted file mode 100644 index ba3fce81f..000000000 --- a/vendor/github.com/google/go-cmp/cmp/options.go +++ /dev/null @@ -1,562 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmp - -import ( - "fmt" - "reflect" - "regexp" - "strings" - - "github.com/google/go-cmp/cmp/internal/function" -) - -// Option configures for specific behavior of [Equal] and [Diff]. In particular, -// the fundamental Option functions ([Ignore], [Transformer], and [Comparer]), -// configure how equality is determined. -// -// The fundamental options may be composed with filters ([FilterPath] and -// [FilterValues]) to control the scope over which they are applied. -// -// The [github.com/google/go-cmp/cmp/cmpopts] package provides helper functions -// for creating options that may be used with [Equal] and [Diff]. -type Option interface { - // filter applies all filters and returns the option that remains. - // Each option may only read s.curPath and call s.callTTBFunc. - // - // An Options is returned only if multiple comparers or transformers - // can apply simultaneously and will only contain values of those types - // or sub-Options containing values of those types. - filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption -} - -// applicableOption represents the following types: -// -// Fundamental: ignore | validator | *comparer | *transformer -// Grouping: Options -type applicableOption interface { - Option - - // apply executes the option, which may mutate s or panic. - apply(s *state, vx, vy reflect.Value) -} - -// coreOption represents the following types: -// -// Fundamental: ignore | validator | *comparer | *transformer -// Filters: *pathFilter | *valuesFilter -type coreOption interface { - Option - isCore() -} - -type core struct{} - -func (core) isCore() {} - -// Options is a list of [Option] values that also satisfies the [Option] interface. -// Helper comparison packages may return an Options value when packing multiple -// [Option] values into a single [Option]. When this package processes an Options, -// it will be implicitly expanded into a flat list. -// -// Applying a filter on an Options is equivalent to applying that same filter -// on all individual options held within. -type Options []Option - -func (opts Options) filter(s *state, t reflect.Type, vx, vy reflect.Value) (out applicableOption) { - for _, opt := range opts { - switch opt := opt.filter(s, t, vx, vy); opt.(type) { - case ignore: - return ignore{} // Only ignore can short-circuit evaluation - case validator: - out = validator{} // Takes precedence over comparer or transformer - case *comparer, *transformer, Options: - switch out.(type) { - case nil: - out = opt - case validator: - // Keep validator - case *comparer, *transformer, Options: - out = Options{out, opt} // Conflicting comparers or transformers - } - } - } - return out -} - -func (opts Options) apply(s *state, _, _ reflect.Value) { - const warning = "ambiguous set of applicable options" - const help = "consider using filters to ensure at most one Comparer or Transformer may apply" - var ss []string - for _, opt := range flattenOptions(nil, opts) { - ss = append(ss, fmt.Sprint(opt)) - } - set := strings.Join(ss, "\n\t") - panic(fmt.Sprintf("%s at %#v:\n\t%s\n%s", warning, s.curPath, set, help)) -} - -func (opts Options) String() string { - var ss []string - for _, opt := range opts { - ss = append(ss, fmt.Sprint(opt)) - } - return fmt.Sprintf("Options{%s}", strings.Join(ss, ", ")) -} - -// FilterPath returns a new [Option] where opt is only evaluated if filter f -// returns true for the current [Path] in the value tree. -// -// This filter is called even if a slice element or map entry is missing and -// provides an opportunity to ignore such cases. The filter function must be -// symmetric such that the filter result is identical regardless of whether the -// missing value is from x or y. -// -// The option passed in may be an [Ignore], [Transformer], [Comparer], [Options], or -// a previously filtered [Option]. -func FilterPath(f func(Path) bool, opt Option) Option { - if f == nil { - panic("invalid path filter function") - } - if opt := normalizeOption(opt); opt != nil { - return &pathFilter{fnc: f, opt: opt} - } - return nil -} - -type pathFilter struct { - core - fnc func(Path) bool - opt Option -} - -func (f pathFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { - if f.fnc(s.curPath) { - return f.opt.filter(s, t, vx, vy) - } - return nil -} - -func (f pathFilter) String() string { - return fmt.Sprintf("FilterPath(%s, %v)", function.NameOf(reflect.ValueOf(f.fnc)), f.opt) -} - -// FilterValues returns a new [Option] where opt is only evaluated if filter f, -// which is a function of the form "func(T, T) bool", returns true for the -// current pair of values being compared. If either value is invalid or -// the type of the values is not assignable to T, then this filter implicitly -// returns false. -// -// The filter function must be -// symmetric (i.e., agnostic to the order of the inputs) and -// deterministic (i.e., produces the same result when given the same inputs). -// If T is an interface, it is possible that f is called with two values with -// different concrete types that both implement T. -// -// The option passed in may be an [Ignore], [Transformer], [Comparer], [Options], or -// a previously filtered [Option]. -func FilterValues(f interface{}, opt Option) Option { - v := reflect.ValueOf(f) - if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() { - panic(fmt.Sprintf("invalid values filter function: %T", f)) - } - if opt := normalizeOption(opt); opt != nil { - vf := &valuesFilter{fnc: v, opt: opt} - if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { - vf.typ = ti - } - return vf - } - return nil -} - -type valuesFilter struct { - core - typ reflect.Type // T - fnc reflect.Value // func(T, T) bool - opt Option -} - -func (f valuesFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { - if !vx.IsValid() || !vx.CanInterface() || !vy.IsValid() || !vy.CanInterface() { - return nil - } - if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) { - return f.opt.filter(s, t, vx, vy) - } - return nil -} - -func (f valuesFilter) String() string { - return fmt.Sprintf("FilterValues(%s, %v)", function.NameOf(f.fnc), f.opt) -} - -// Ignore is an [Option] that causes all comparisons to be ignored. -// This value is intended to be combined with [FilterPath] or [FilterValues]. -// It is an error to pass an unfiltered Ignore option to [Equal]. -func Ignore() Option { return ignore{} } - -type ignore struct{ core } - -func (ignore) isFiltered() bool { return false } -func (ignore) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { return ignore{} } -func (ignore) apply(s *state, _, _ reflect.Value) { s.report(true, reportByIgnore) } -func (ignore) String() string { return "Ignore()" } - -// validator is a sentinel Option type to indicate that some options could not -// be evaluated due to unexported fields, missing slice elements, or -// missing map entries. Both values are validator only for unexported fields. -type validator struct{ core } - -func (validator) filter(_ *state, _ reflect.Type, vx, vy reflect.Value) applicableOption { - if !vx.IsValid() || !vy.IsValid() { - return validator{} - } - if !vx.CanInterface() || !vy.CanInterface() { - return validator{} - } - return nil -} -func (validator) apply(s *state, vx, vy reflect.Value) { - // Implies missing slice element or map entry. - if !vx.IsValid() || !vy.IsValid() { - s.report(vx.IsValid() == vy.IsValid(), 0) - return - } - - // Unable to Interface implies unexported field without visibility access. - if !vx.CanInterface() || !vy.CanInterface() { - help := "consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported" - var name string - if t := s.curPath.Index(-2).Type(); t.Name() != "" { - // Named type with unexported fields. - name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType - isProtoMessage := func(t reflect.Type) bool { - m, ok := reflect.PointerTo(t).MethodByName("ProtoReflect") - return ok && m.Type.NumIn() == 1 && m.Type.NumOut() == 1 && - m.Type.Out(0).PkgPath() == "google.golang.org/protobuf/reflect/protoreflect" && - m.Type.Out(0).Name() == "Message" - } - if isProtoMessage(t) { - help = `consider using "google.golang.org/protobuf/testing/protocmp".Transform to compare proto.Message types` - } else if _, ok := reflect.New(t).Interface().(error); ok { - help = "consider using cmpopts.EquateErrors to compare error values" - } else if t.Comparable() { - help = "consider using cmpopts.EquateComparable to compare comparable Go types" - } - } else { - // Unnamed type with unexported fields. Derive PkgPath from field. - var pkgPath string - for i := 0; i < t.NumField() && pkgPath == ""; i++ { - pkgPath = t.Field(i).PkgPath - } - name = fmt.Sprintf("%q.(%v)", pkgPath, t.String()) // e.g., "path/to/package".(struct { a int }) - } - panic(fmt.Sprintf("cannot handle unexported field at %#v:\n\t%v\n%s", s.curPath, name, help)) - } - - panic("not reachable") -} - -// identRx represents a valid identifier according to the Go specification. -const identRx = `[_\p{L}][_\p{L}\p{N}]*` - -var identsRx = regexp.MustCompile(`^` + identRx + `(\.` + identRx + `)*$`) - -// Transformer returns an [Option] that applies a transformation function that -// converts values of a certain type into that of another. -// -// The transformer f must be a function "func(T) R" that converts values of -// type T to those of type R and is implicitly filtered to input values -// assignable to T. The transformer must not mutate T in any way. -// -// To help prevent some cases of infinite recursive cycles applying the -// same transform to the output of itself (e.g., in the case where the -// input and output types are the same), an implicit filter is added such that -// a transformer is applicable only if that exact transformer is not already -// in the tail of the [Path] since the last non-[Transform] step. -// For situations where the implicit filter is still insufficient, -// consider using [github.com/google/go-cmp/cmp/cmpopts.AcyclicTransformer], -// which adds a filter to prevent the transformer from -// being recursively applied upon itself. -// -// The name is a user provided label that is used as the [Transform.Name] in the -// transformation [PathStep] (and eventually shown in the [Diff] output). -// The name must be a valid identifier or qualified identifier in Go syntax. -// If empty, an arbitrary name is used. -func Transformer(name string, f interface{}) Option { - v := reflect.ValueOf(f) - if !function.IsType(v.Type(), function.Transformer) || v.IsNil() { - panic(fmt.Sprintf("invalid transformer function: %T", f)) - } - if name == "" { - name = function.NameOf(v) - if !identsRx.MatchString(name) { - name = "λ" // Lambda-symbol as placeholder name - } - } else if !identsRx.MatchString(name) { - panic(fmt.Sprintf("invalid name: %q", name)) - } - tr := &transformer{name: name, fnc: reflect.ValueOf(f)} - if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { - tr.typ = ti - } - return tr -} - -type transformer struct { - core - name string - typ reflect.Type // T - fnc reflect.Value // func(T) R -} - -func (tr *transformer) isFiltered() bool { return tr.typ != nil } - -func (tr *transformer) filter(s *state, t reflect.Type, _, _ reflect.Value) applicableOption { - for i := len(s.curPath) - 1; i >= 0; i-- { - if t, ok := s.curPath[i].(Transform); !ok { - break // Hit most recent non-Transform step - } else if tr == t.trans { - return nil // Cannot directly use same Transform - } - } - if tr.typ == nil || t.AssignableTo(tr.typ) { - return tr - } - return nil -} - -func (tr *transformer) apply(s *state, vx, vy reflect.Value) { - step := Transform{&transform{pathStep{typ: tr.fnc.Type().Out(0)}, tr}} - vvx := s.callTRFunc(tr.fnc, vx, step) - vvy := s.callTRFunc(tr.fnc, vy, step) - step.vx, step.vy = vvx, vvy - s.compareAny(step) -} - -func (tr transformer) String() string { - return fmt.Sprintf("Transformer(%s, %s)", tr.name, function.NameOf(tr.fnc)) -} - -// Comparer returns an [Option] that determines whether two values are equal -// to each other. -// -// The comparer f must be a function "func(T, T) bool" and is implicitly -// filtered to input values assignable to T. If T is an interface, it is -// possible that f is called with two values of different concrete types that -// both implement T. -// -// The equality function must be: -// - Symmetric: equal(x, y) == equal(y, x) -// - Deterministic: equal(x, y) == equal(x, y) -// - Pure: equal(x, y) does not modify x or y -func Comparer(f interface{}) Option { - v := reflect.ValueOf(f) - if !function.IsType(v.Type(), function.Equal) || v.IsNil() { - panic(fmt.Sprintf("invalid comparer function: %T", f)) - } - cm := &comparer{fnc: v} - if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { - cm.typ = ti - } - return cm -} - -type comparer struct { - core - typ reflect.Type // T - fnc reflect.Value // func(T, T) bool -} - -func (cm *comparer) isFiltered() bool { return cm.typ != nil } - -func (cm *comparer) filter(_ *state, t reflect.Type, _, _ reflect.Value) applicableOption { - if cm.typ == nil || t.AssignableTo(cm.typ) { - return cm - } - return nil -} - -func (cm *comparer) apply(s *state, vx, vy reflect.Value) { - eq := s.callTTBFunc(cm.fnc, vx, vy) - s.report(eq, reportByFunc) -} - -func (cm comparer) String() string { - return fmt.Sprintf("Comparer(%s)", function.NameOf(cm.fnc)) -} - -// Exporter returns an [Option] that specifies whether [Equal] is allowed to -// introspect into the unexported fields of certain struct types. -// -// Users of this option must understand that comparing on unexported fields -// from external packages is not safe since changes in the internal -// implementation of some external package may cause the result of [Equal] -// to unexpectedly change. However, it may be valid to use this option on types -// defined in an internal package where the semantic meaning of an unexported -// field is in the control of the user. -// -// In many cases, a custom [Comparer] should be used instead that defines -// equality as a function of the public API of a type rather than the underlying -// unexported implementation. -// -// For example, the [reflect.Type] documentation defines equality to be determined -// by the == operator on the interface (essentially performing a shallow pointer -// comparison) and most attempts to compare *[regexp.Regexp] types are interested -// in only checking that the regular expression strings are equal. -// Both of these are accomplished using [Comparer] options: -// -// Comparer(func(x, y reflect.Type) bool { return x == y }) -// Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() }) -// -// In other cases, the [github.com/google/go-cmp/cmp/cmpopts.IgnoreUnexported] -// option can be used to ignore all unexported fields on specified struct types. -func Exporter(f func(reflect.Type) bool) Option { - return exporter(f) -} - -type exporter func(reflect.Type) bool - -func (exporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { - panic("not implemented") -} - -// AllowUnexported returns an [Option] that allows [Equal] to forcibly introspect -// unexported fields of the specified struct types. -// -// See [Exporter] for the proper use of this option. -func AllowUnexported(types ...interface{}) Option { - m := make(map[reflect.Type]bool) - for _, typ := range types { - t := reflect.TypeOf(typ) - if t.Kind() != reflect.Struct { - panic(fmt.Sprintf("invalid struct type: %T", typ)) - } - m[t] = true - } - return exporter(func(t reflect.Type) bool { return m[t] }) -} - -// Result represents the comparison result for a single node and -// is provided by cmp when calling Report (see [Reporter]). -type Result struct { - _ [0]func() // Make Result incomparable - flags resultFlags -} - -// Equal reports whether the node was determined to be equal or not. -// As a special case, ignored nodes are considered equal. -func (r Result) Equal() bool { - return r.flags&(reportEqual|reportByIgnore) != 0 -} - -// ByIgnore reports whether the node is equal because it was ignored. -// This never reports true if [Result.Equal] reports false. -func (r Result) ByIgnore() bool { - return r.flags&reportByIgnore != 0 -} - -// ByMethod reports whether the Equal method determined equality. -func (r Result) ByMethod() bool { - return r.flags&reportByMethod != 0 -} - -// ByFunc reports whether a [Comparer] function determined equality. -func (r Result) ByFunc() bool { - return r.flags&reportByFunc != 0 -} - -// ByCycle reports whether a reference cycle was detected. -func (r Result) ByCycle() bool { - return r.flags&reportByCycle != 0 -} - -type resultFlags uint - -const ( - _ resultFlags = (1 << iota) / 2 - - reportEqual - reportUnequal - reportByIgnore - reportByMethod - reportByFunc - reportByCycle -) - -// Reporter is an [Option] that can be passed to [Equal]. When [Equal] traverses -// the value trees, it calls PushStep as it descends into each node in the -// tree and PopStep as it ascend out of the node. The leaves of the tree are -// either compared (determined to be equal or not equal) or ignored and reported -// as such by calling the Report method. -func Reporter(r interface { - // PushStep is called when a tree-traversal operation is performed. - // The PathStep itself is only valid until the step is popped. - // The PathStep.Values are valid for the duration of the entire traversal - // and must not be mutated. - // - // Equal always calls PushStep at the start to provide an operation-less - // PathStep used to report the root values. - // - // Within a slice, the exact set of inserted, removed, or modified elements - // is unspecified and may change in future implementations. - // The entries of a map are iterated through in an unspecified order. - PushStep(PathStep) - - // Report is called exactly once on leaf nodes to report whether the - // comparison identified the node as equal, unequal, or ignored. - // A leaf node is one that is immediately preceded by and followed by - // a pair of PushStep and PopStep calls. - Report(Result) - - // PopStep ascends back up the value tree. - // There is always a matching pop call for every push call. - PopStep() -}) Option { - return reporter{r} -} - -type reporter struct{ reporterIface } -type reporterIface interface { - PushStep(PathStep) - Report(Result) - PopStep() -} - -func (reporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { - panic("not implemented") -} - -// normalizeOption normalizes the input options such that all Options groups -// are flattened and groups with a single element are reduced to that element. -// Only coreOptions and Options containing coreOptions are allowed. -func normalizeOption(src Option) Option { - switch opts := flattenOptions(nil, Options{src}); len(opts) { - case 0: - return nil - case 1: - return opts[0] - default: - return opts - } -} - -// flattenOptions copies all options in src to dst as a flat list. -// Only coreOptions and Options containing coreOptions are allowed. -func flattenOptions(dst, src Options) Options { - for _, opt := range src { - switch opt := opt.(type) { - case nil: - continue - case Options: - dst = flattenOptions(dst, opt) - case coreOption: - dst = append(dst, opt) - default: - panic(fmt.Sprintf("invalid option type: %T", opt)) - } - } - return dst -} diff --git a/vendor/github.com/google/go-cmp/cmp/path.go b/vendor/github.com/google/go-cmp/cmp/path.go deleted file mode 100644 index c3c145642..000000000 --- a/vendor/github.com/google/go-cmp/cmp/path.go +++ /dev/null @@ -1,390 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmp - -import ( - "fmt" - "reflect" - "strings" - "unicode" - "unicode/utf8" - - "github.com/google/go-cmp/cmp/internal/value" -) - -// Path is a list of [PathStep] describing the sequence of operations to get -// from some root type to the current position in the value tree. -// The first Path element is always an operation-less [PathStep] that exists -// simply to identify the initial type. -// -// When traversing structs with embedded structs, the embedded struct will -// always be accessed as a field before traversing the fields of the -// embedded struct themselves. That is, an exported field from the -// embedded struct will never be accessed directly from the parent struct. -type Path []PathStep - -// PathStep is a union-type for specific operations to traverse -// a value's tree structure. Users of this package never need to implement -// these types as values of this type will be returned by this package. -// -// Implementations of this interface: -// - [StructField] -// - [SliceIndex] -// - [MapIndex] -// - [Indirect] -// - [TypeAssertion] -// - [Transform] -type PathStep interface { - String() string - - // Type is the resulting type after performing the path step. - Type() reflect.Type - - // Values is the resulting values after performing the path step. - // The type of each valid value is guaranteed to be identical to Type. - // - // In some cases, one or both may be invalid or have restrictions: - // - For StructField, both are not interface-able if the current field - // is unexported and the struct type is not explicitly permitted by - // an Exporter to traverse unexported fields. - // - For SliceIndex, one may be invalid if an element is missing from - // either the x or y slice. - // - For MapIndex, one may be invalid if an entry is missing from - // either the x or y map. - // - // The provided values must not be mutated. - Values() (vx, vy reflect.Value) -} - -var ( - _ PathStep = StructField{} - _ PathStep = SliceIndex{} - _ PathStep = MapIndex{} - _ PathStep = Indirect{} - _ PathStep = TypeAssertion{} - _ PathStep = Transform{} -) - -func (pa *Path) push(s PathStep) { - *pa = append(*pa, s) -} - -func (pa *Path) pop() { - *pa = (*pa)[:len(*pa)-1] -} - -// Last returns the last [PathStep] in the Path. -// If the path is empty, this returns a non-nil [PathStep] -// that reports a nil [PathStep.Type]. -func (pa Path) Last() PathStep { - return pa.Index(-1) -} - -// Index returns the ith step in the Path and supports negative indexing. -// A negative index starts counting from the tail of the Path such that -1 -// refers to the last step, -2 refers to the second-to-last step, and so on. -// If index is invalid, this returns a non-nil [PathStep] -// that reports a nil [PathStep.Type]. -func (pa Path) Index(i int) PathStep { - if i < 0 { - i = len(pa) + i - } - if i < 0 || i >= len(pa) { - return pathStep{} - } - return pa[i] -} - -// String returns the simplified path to a node. -// The simplified path only contains struct field accesses. -// -// For example: -// -// MyMap.MySlices.MyField -func (pa Path) String() string { - var ss []string - for _, s := range pa { - if _, ok := s.(StructField); ok { - ss = append(ss, s.String()) - } - } - return strings.TrimPrefix(strings.Join(ss, ""), ".") -} - -// GoString returns the path to a specific node using Go syntax. -// -// For example: -// -// (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField -func (pa Path) GoString() string { - var ssPre, ssPost []string - var numIndirect int - for i, s := range pa { - var nextStep PathStep - if i+1 < len(pa) { - nextStep = pa[i+1] - } - switch s := s.(type) { - case Indirect: - numIndirect++ - pPre, pPost := "(", ")" - switch nextStep.(type) { - case Indirect: - continue // Next step is indirection, so let them batch up - case StructField: - numIndirect-- // Automatic indirection on struct fields - case nil: - pPre, pPost = "", "" // Last step; no need for parenthesis - } - if numIndirect > 0 { - ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect)) - ssPost = append(ssPost, pPost) - } - numIndirect = 0 - continue - case Transform: - ssPre = append(ssPre, s.trans.name+"(") - ssPost = append(ssPost, ")") - continue - } - ssPost = append(ssPost, s.String()) - } - for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 { - ssPre[i], ssPre[j] = ssPre[j], ssPre[i] - } - return strings.Join(ssPre, "") + strings.Join(ssPost, "") -} - -type pathStep struct { - typ reflect.Type - vx, vy reflect.Value -} - -func (ps pathStep) Type() reflect.Type { return ps.typ } -func (ps pathStep) Values() (vx, vy reflect.Value) { return ps.vx, ps.vy } -func (ps pathStep) String() string { - if ps.typ == nil { - return "" - } - s := value.TypeString(ps.typ, false) - if s == "" || strings.ContainsAny(s, "{}\n") { - return "root" // Type too simple or complex to print - } - return fmt.Sprintf("{%s}", s) -} - -// StructField is a [PathStep] that represents a struct field access -// on a field called [StructField.Name]. -type StructField struct{ *structField } -type structField struct { - pathStep - name string - idx int - - // These fields are used for forcibly accessing an unexported field. - // pvx, pvy, and field are only valid if unexported is true. - unexported bool - mayForce bool // Forcibly allow visibility - paddr bool // Was parent addressable? - pvx, pvy reflect.Value // Parent values (always addressable) - field reflect.StructField // Field information -} - -func (sf StructField) Type() reflect.Type { return sf.typ } -func (sf StructField) Values() (vx, vy reflect.Value) { - if !sf.unexported { - return sf.vx, sf.vy // CanInterface reports true - } - - // Forcibly obtain read-write access to an unexported struct field. - if sf.mayForce { - vx = retrieveUnexportedField(sf.pvx, sf.field, sf.paddr) - vy = retrieveUnexportedField(sf.pvy, sf.field, sf.paddr) - return vx, vy // CanInterface reports true - } - return sf.vx, sf.vy // CanInterface reports false -} -func (sf StructField) String() string { return fmt.Sprintf(".%s", sf.name) } - -// Name is the field name. -func (sf StructField) Name() string { return sf.name } - -// Index is the index of the field in the parent struct type. -// See [reflect.Type.Field]. -func (sf StructField) Index() int { return sf.idx } - -// SliceIndex is a [PathStep] that represents an index operation on -// a slice or array at some index [SliceIndex.Key]. -type SliceIndex struct{ *sliceIndex } -type sliceIndex struct { - pathStep - xkey, ykey int - isSlice bool // False for reflect.Array -} - -func (si SliceIndex) Type() reflect.Type { return si.typ } -func (si SliceIndex) Values() (vx, vy reflect.Value) { return si.vx, si.vy } -func (si SliceIndex) String() string { - switch { - case si.xkey == si.ykey: - return fmt.Sprintf("[%d]", si.xkey) - case si.ykey == -1: - // [5->?] means "I don't know where X[5] went" - return fmt.Sprintf("[%d->?]", si.xkey) - case si.xkey == -1: - // [?->3] means "I don't know where Y[3] came from" - return fmt.Sprintf("[?->%d]", si.ykey) - default: - // [5->3] means "X[5] moved to Y[3]" - return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey) - } -} - -// Key is the index key; it may return -1 if in a split state -func (si SliceIndex) Key() int { - if si.xkey != si.ykey { - return -1 - } - return si.xkey -} - -// SplitKeys are the indexes for indexing into slices in the -// x and y values, respectively. These indexes may differ due to the -// insertion or removal of an element in one of the slices, causing -// all of the indexes to be shifted. If an index is -1, then that -// indicates that the element does not exist in the associated slice. -// -// [SliceIndex.Key] is guaranteed to return -1 if and only if the indexes -// returned by SplitKeys are not the same. SplitKeys will never return -1 for -// both indexes. -func (si SliceIndex) SplitKeys() (ix, iy int) { return si.xkey, si.ykey } - -// MapIndex is a [PathStep] that represents an index operation on a map at some index Key. -type MapIndex struct{ *mapIndex } -type mapIndex struct { - pathStep - key reflect.Value -} - -func (mi MapIndex) Type() reflect.Type { return mi.typ } -func (mi MapIndex) Values() (vx, vy reflect.Value) { return mi.vx, mi.vy } -func (mi MapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } - -// Key is the value of the map key. -func (mi MapIndex) Key() reflect.Value { return mi.key } - -// Indirect is a [PathStep] that represents pointer indirection on the parent type. -type Indirect struct{ *indirect } -type indirect struct { - pathStep -} - -func (in Indirect) Type() reflect.Type { return in.typ } -func (in Indirect) Values() (vx, vy reflect.Value) { return in.vx, in.vy } -func (in Indirect) String() string { return "*" } - -// TypeAssertion is a [PathStep] that represents a type assertion on an interface. -type TypeAssertion struct{ *typeAssertion } -type typeAssertion struct { - pathStep -} - -func (ta TypeAssertion) Type() reflect.Type { return ta.typ } -func (ta TypeAssertion) Values() (vx, vy reflect.Value) { return ta.vx, ta.vy } -func (ta TypeAssertion) String() string { return fmt.Sprintf(".(%v)", value.TypeString(ta.typ, false)) } - -// Transform is a [PathStep] that represents a transformation -// from the parent type to the current type. -type Transform struct{ *transform } -type transform struct { - pathStep - trans *transformer -} - -func (tf Transform) Type() reflect.Type { return tf.typ } -func (tf Transform) Values() (vx, vy reflect.Value) { return tf.vx, tf.vy } -func (tf Transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) } - -// Name is the name of the [Transformer]. -func (tf Transform) Name() string { return tf.trans.name } - -// Func is the function pointer to the transformer function. -func (tf Transform) Func() reflect.Value { return tf.trans.fnc } - -// Option returns the originally constructed [Transformer] option. -// The == operator can be used to detect the exact option used. -func (tf Transform) Option() Option { return tf.trans } - -// pointerPath represents a dual-stack of pointers encountered when -// recursively traversing the x and y values. This data structure supports -// detection of cycles and determining whether the cycles are equal. -// In Go, cycles can occur via pointers, slices, and maps. -// -// The pointerPath uses a map to represent a stack; where descension into a -// pointer pushes the address onto the stack, and ascension from a pointer -// pops the address from the stack. Thus, when traversing into a pointer from -// reflect.Ptr, reflect.Slice element, or reflect.Map, we can detect cycles -// by checking whether the pointer has already been visited. The cycle detection -// uses a separate stack for the x and y values. -// -// If a cycle is detected we need to determine whether the two pointers -// should be considered equal. The definition of equality chosen by Equal -// requires two graphs to have the same structure. To determine this, both the -// x and y values must have a cycle where the previous pointers were also -// encountered together as a pair. -// -// Semantically, this is equivalent to augmenting Indirect, SliceIndex, and -// MapIndex with pointer information for the x and y values. -// Suppose px and py are two pointers to compare, we then search the -// Path for whether px was ever encountered in the Path history of x, and -// similarly so with py. If either side has a cycle, the comparison is only -// equal if both px and py have a cycle resulting from the same PathStep. -// -// Using a map as a stack is more performant as we can perform cycle detection -// in O(1) instead of O(N) where N is len(Path). -type pointerPath struct { - // mx is keyed by x pointers, where the value is the associated y pointer. - mx map[value.Pointer]value.Pointer - // my is keyed by y pointers, where the value is the associated x pointer. - my map[value.Pointer]value.Pointer -} - -func (p *pointerPath) Init() { - p.mx = make(map[value.Pointer]value.Pointer) - p.my = make(map[value.Pointer]value.Pointer) -} - -// Push indicates intent to descend into pointers vx and vy where -// visited reports whether either has been seen before. If visited before, -// equal reports whether both pointers were encountered together. -// Pop must be called if and only if the pointers were never visited. -// -// The pointers vx and vy must be a reflect.Ptr, reflect.Slice, or reflect.Map -// and be non-nil. -func (p pointerPath) Push(vx, vy reflect.Value) (equal, visited bool) { - px := value.PointerOf(vx) - py := value.PointerOf(vy) - _, ok1 := p.mx[px] - _, ok2 := p.my[py] - if ok1 || ok2 { - equal = p.mx[px] == py && p.my[py] == px // Pointers paired together - return equal, true - } - p.mx[px] = py - p.my[py] = px - return false, false -} - -// Pop ascends from pointers vx and vy. -func (p pointerPath) Pop(vx, vy reflect.Value) { - delete(p.mx, value.PointerOf(vx)) - delete(p.my, value.PointerOf(vy)) -} - -// isExported reports whether the identifier is exported. -func isExported(id string) bool { - r, _ := utf8.DecodeRuneInString(id) - return unicode.IsUpper(r) -} diff --git a/vendor/github.com/google/go-cmp/cmp/report.go b/vendor/github.com/google/go-cmp/cmp/report.go deleted file mode 100644 index f43cd12eb..000000000 --- a/vendor/github.com/google/go-cmp/cmp/report.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmp - -// defaultReporter implements the reporter interface. -// -// As Equal serially calls the PushStep, Report, and PopStep methods, the -// defaultReporter constructs a tree-based representation of the compared value -// and the result of each comparison (see valueNode). -// -// When the String method is called, the FormatDiff method transforms the -// valueNode tree into a textNode tree, which is a tree-based representation -// of the textual output (see textNode). -// -// Lastly, the textNode.String method produces the final report as a string. -type defaultReporter struct { - root *valueNode - curr *valueNode -} - -func (r *defaultReporter) PushStep(ps PathStep) { - r.curr = r.curr.PushStep(ps) - if r.root == nil { - r.root = r.curr - } -} -func (r *defaultReporter) Report(rs Result) { - r.curr.Report(rs) -} -func (r *defaultReporter) PopStep() { - r.curr = r.curr.PopStep() -} - -// String provides a full report of the differences detected as a structured -// literal in pseudo-Go syntax. String may only be called after the entire tree -// has been traversed. -func (r *defaultReporter) String() string { - assert(r.root != nil && r.curr == nil) - if r.root.NumDiff == 0 { - return "" - } - ptrs := new(pointerReferences) - text := formatOptions{}.FormatDiff(r.root, ptrs) - resolveReferences(text) - return text.String() -} - -func assert(ok bool) { - if !ok { - panic("assertion failure") - } -} diff --git a/vendor/github.com/google/go-cmp/cmp/report_compare.go b/vendor/github.com/google/go-cmp/cmp/report_compare.go deleted file mode 100644 index 2050bf6b4..000000000 --- a/vendor/github.com/google/go-cmp/cmp/report_compare.go +++ /dev/null @@ -1,433 +0,0 @@ -// Copyright 2019, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmp - -import ( - "fmt" - "reflect" -) - -// numContextRecords is the number of surrounding equal records to print. -const numContextRecords = 2 - -type diffMode byte - -const ( - diffUnknown diffMode = 0 - diffIdentical diffMode = ' ' - diffRemoved diffMode = '-' - diffInserted diffMode = '+' -) - -type typeMode int - -const ( - // emitType always prints the type. - emitType typeMode = iota - // elideType never prints the type. - elideType - // autoType prints the type only for composite kinds - // (i.e., structs, slices, arrays, and maps). - autoType -) - -type formatOptions struct { - // DiffMode controls the output mode of FormatDiff. - // - // If diffUnknown, then produce a diff of the x and y values. - // If diffIdentical, then emit values as if they were equal. - // If diffRemoved, then only emit x values (ignoring y values). - // If diffInserted, then only emit y values (ignoring x values). - DiffMode diffMode - - // TypeMode controls whether to print the type for the current node. - // - // As a general rule of thumb, we always print the type of the next node - // after an interface, and always elide the type of the next node after - // a slice or map node. - TypeMode typeMode - - // formatValueOptions are options specific to printing reflect.Values. - formatValueOptions -} - -func (opts formatOptions) WithDiffMode(d diffMode) formatOptions { - opts.DiffMode = d - return opts -} -func (opts formatOptions) WithTypeMode(t typeMode) formatOptions { - opts.TypeMode = t - return opts -} -func (opts formatOptions) WithVerbosity(level int) formatOptions { - opts.VerbosityLevel = level - opts.LimitVerbosity = true - return opts -} -func (opts formatOptions) verbosity() uint { - switch { - case opts.VerbosityLevel < 0: - return 0 - case opts.VerbosityLevel > 16: - return 16 // some reasonable maximum to avoid shift overflow - default: - return uint(opts.VerbosityLevel) - } -} - -const maxVerbosityPreset = 6 - -// verbosityPreset modifies the verbosity settings given an index -// between 0 and maxVerbosityPreset, inclusive. -func verbosityPreset(opts formatOptions, i int) formatOptions { - opts.VerbosityLevel = int(opts.verbosity()) + 2*i - if i > 0 { - opts.AvoidStringer = true - } - if i >= maxVerbosityPreset { - opts.PrintAddresses = true - opts.QualifiedNames = true - } - return opts -} - -// FormatDiff converts a valueNode tree into a textNode tree, where the later -// is a textual representation of the differences detected in the former. -func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) { - if opts.DiffMode == diffIdentical { - opts = opts.WithVerbosity(1) - } else if opts.verbosity() < 3 { - opts = opts.WithVerbosity(3) - } - - // Check whether we have specialized formatting for this node. - // This is not necessary, but helpful for producing more readable outputs. - if opts.CanFormatDiffSlice(v) { - return opts.FormatDiffSlice(v) - } - - var parentKind reflect.Kind - if v.parent != nil && v.parent.TransformerName == "" { - parentKind = v.parent.Type.Kind() - } - - // For leaf nodes, format the value based on the reflect.Values alone. - // As a special case, treat equal []byte as a leaf nodes. - isBytes := v.Type.Kind() == reflect.Slice && v.Type.Elem() == byteType - isEqualBytes := isBytes && v.NumDiff+v.NumIgnored+v.NumTransformed == 0 - if v.MaxDepth == 0 || isEqualBytes { - switch opts.DiffMode { - case diffUnknown, diffIdentical: - // Format Equal. - if v.NumDiff == 0 { - outx := opts.FormatValue(v.ValueX, parentKind, ptrs) - outy := opts.FormatValue(v.ValueY, parentKind, ptrs) - if v.NumIgnored > 0 && v.NumSame == 0 { - return textEllipsis - } else if outx.Len() < outy.Len() { - return outx - } else { - return outy - } - } - - // Format unequal. - assert(opts.DiffMode == diffUnknown) - var list textList - outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs) - outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs) - for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { - opts2 := verbosityPreset(opts, i).WithTypeMode(elideType) - outx = opts2.FormatValue(v.ValueX, parentKind, ptrs) - outy = opts2.FormatValue(v.ValueY, parentKind, ptrs) - } - if outx != nil { - list = append(list, textRecord{Diff: '-', Value: outx}) - } - if outy != nil { - list = append(list, textRecord{Diff: '+', Value: outy}) - } - return opts.WithTypeMode(emitType).FormatType(v.Type, list) - case diffRemoved: - return opts.FormatValue(v.ValueX, parentKind, ptrs) - case diffInserted: - return opts.FormatValue(v.ValueY, parentKind, ptrs) - default: - panic("invalid diff mode") - } - } - - // Register slice element to support cycle detection. - if parentKind == reflect.Slice { - ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true) - defer ptrs.Pop() - defer func() { out = wrapTrunkReferences(ptrRefs, out) }() - } - - // Descend into the child value node. - if v.TransformerName != "" { - out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) - out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"} - return opts.FormatType(v.Type, out) - } else { - switch k := v.Type.Kind(); k { - case reflect.Struct, reflect.Array, reflect.Slice: - out = opts.formatDiffList(v.Records, k, ptrs) - out = opts.FormatType(v.Type, out) - case reflect.Map: - // Register map to support cycle detection. - ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) - defer ptrs.Pop() - - out = opts.formatDiffList(v.Records, k, ptrs) - out = wrapTrunkReferences(ptrRefs, out) - out = opts.FormatType(v.Type, out) - case reflect.Ptr: - // Register pointer to support cycle detection. - ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) - defer ptrs.Pop() - - out = opts.FormatDiff(v.Value, ptrs) - out = wrapTrunkReferences(ptrRefs, out) - out = &textWrap{Prefix: "&", Value: out} - case reflect.Interface: - out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) - default: - panic(fmt.Sprintf("%v cannot have children", k)) - } - return out - } -} - -func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode { - // Derive record name based on the data structure kind. - var name string - var formatKey func(reflect.Value) string - switch k { - case reflect.Struct: - name = "field" - opts = opts.WithTypeMode(autoType) - formatKey = func(v reflect.Value) string { return v.String() } - case reflect.Slice, reflect.Array: - name = "element" - opts = opts.WithTypeMode(elideType) - formatKey = func(reflect.Value) string { return "" } - case reflect.Map: - name = "entry" - opts = opts.WithTypeMode(elideType) - formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) } - } - - maxLen := -1 - if opts.LimitVerbosity { - if opts.DiffMode == diffIdentical { - maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... - } else { - maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc... - } - opts.VerbosityLevel-- - } - - // Handle unification. - switch opts.DiffMode { - case diffIdentical, diffRemoved, diffInserted: - var list textList - var deferredEllipsis bool // Add final "..." to indicate records were dropped - for _, r := range recs { - if len(list) == maxLen { - deferredEllipsis = true - break - } - - // Elide struct fields that are zero value. - if k == reflect.Struct { - var isZero bool - switch opts.DiffMode { - case diffIdentical: - isZero = r.Value.ValueX.IsZero() || r.Value.ValueY.IsZero() - case diffRemoved: - isZero = r.Value.ValueX.IsZero() - case diffInserted: - isZero = r.Value.ValueY.IsZero() - } - if isZero { - continue - } - } - // Elide ignored nodes. - if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 { - deferredEllipsis = !(k == reflect.Slice || k == reflect.Array) - if !deferredEllipsis { - list.AppendEllipsis(diffStats{}) - } - continue - } - if out := opts.FormatDiff(r.Value, ptrs); out != nil { - list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) - } - } - if deferredEllipsis { - list.AppendEllipsis(diffStats{}) - } - return &textWrap{Prefix: "{", Value: list, Suffix: "}"} - case diffUnknown: - default: - panic("invalid diff mode") - } - - // Handle differencing. - var numDiffs int - var list textList - var keys []reflect.Value // invariant: len(list) == len(keys) - groups := coalesceAdjacentRecords(name, recs) - maxGroup := diffStats{Name: name} - for i, ds := range groups { - if maxLen >= 0 && numDiffs >= maxLen { - maxGroup = maxGroup.Append(ds) - continue - } - - // Handle equal records. - if ds.NumDiff() == 0 { - // Compute the number of leading and trailing records to print. - var numLo, numHi int - numEqual := ds.NumIgnored + ds.NumIdentical - for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 { - if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 { - break - } - numLo++ - } - for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 { - if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 { - break - } - numHi++ - } - if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 { - numHi++ // Avoid pointless coalescing of a single equal record - } - - // Format the equal values. - for _, r := range recs[:numLo] { - out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) - list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) - keys = append(keys, r.Key) - } - if numEqual > numLo+numHi { - ds.NumIdentical -= numLo + numHi - list.AppendEllipsis(ds) - for len(keys) < len(list) { - keys = append(keys, reflect.Value{}) - } - } - for _, r := range recs[numEqual-numHi : numEqual] { - out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) - list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) - keys = append(keys, r.Key) - } - recs = recs[numEqual:] - continue - } - - // Handle unequal records. - for _, r := range recs[:ds.NumDiff()] { - switch { - case opts.CanFormatDiffSlice(r.Value): - out := opts.FormatDiffSlice(r.Value) - list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) - keys = append(keys, r.Key) - case r.Value.NumChildren == r.Value.MaxDepth: - outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) - outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) - for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { - opts2 := verbosityPreset(opts, i) - outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) - outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) - } - if outx != nil { - list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx}) - keys = append(keys, r.Key) - } - if outy != nil { - list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy}) - keys = append(keys, r.Key) - } - default: - out := opts.FormatDiff(r.Value, ptrs) - list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) - keys = append(keys, r.Key) - } - } - recs = recs[ds.NumDiff():] - numDiffs += ds.NumDiff() - } - if maxGroup.IsZero() { - assert(len(recs) == 0) - } else { - list.AppendEllipsis(maxGroup) - for len(keys) < len(list) { - keys = append(keys, reflect.Value{}) - } - } - assert(len(list) == len(keys)) - - // For maps, the default formatting logic uses fmt.Stringer which may - // produce ambiguous output. Avoid calling String to disambiguate. - if k == reflect.Map { - var ambiguous bool - seenKeys := map[string]reflect.Value{} - for i, currKey := range keys { - if currKey.IsValid() { - strKey := list[i].Key - prevKey, seen := seenKeys[strKey] - if seen && prevKey.CanInterface() && currKey.CanInterface() { - ambiguous = prevKey.Interface() != currKey.Interface() - if ambiguous { - break - } - } - seenKeys[strKey] = currKey - } - } - if ambiguous { - for i, k := range keys { - if k.IsValid() { - list[i].Key = formatMapKey(k, true, ptrs) - } - } - } - } - - return &textWrap{Prefix: "{", Value: list, Suffix: "}"} -} - -// coalesceAdjacentRecords coalesces the list of records into groups of -// adjacent equal, or unequal counts. -func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) { - var prevCase int // Arbitrary index into which case last occurred - lastStats := func(i int) *diffStats { - if prevCase != i { - groups = append(groups, diffStats{Name: name}) - prevCase = i - } - return &groups[len(groups)-1] - } - for _, r := range recs { - switch rv := r.Value; { - case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0: - lastStats(1).NumIgnored++ - case rv.NumDiff == 0: - lastStats(1).NumIdentical++ - case rv.NumDiff > 0 && !rv.ValueY.IsValid(): - lastStats(2).NumRemoved++ - case rv.NumDiff > 0 && !rv.ValueX.IsValid(): - lastStats(2).NumInserted++ - default: - lastStats(2).NumModified++ - } - } - return groups -} diff --git a/vendor/github.com/google/go-cmp/cmp/report_references.go b/vendor/github.com/google/go-cmp/cmp/report_references.go deleted file mode 100644 index be31b33a9..000000000 --- a/vendor/github.com/google/go-cmp/cmp/report_references.go +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2020, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmp - -import ( - "fmt" - "reflect" - "strings" - - "github.com/google/go-cmp/cmp/internal/flags" - "github.com/google/go-cmp/cmp/internal/value" -) - -const ( - pointerDelimPrefix = "⟪" - pointerDelimSuffix = "⟫" -) - -// formatPointer prints the address of the pointer. -func formatPointer(p value.Pointer, withDelims bool) string { - v := p.Uintptr() - if flags.Deterministic { - v = 0xdeadf00f // Only used for stable testing purposes - } - if withDelims { - return pointerDelimPrefix + formatHex(uint64(v)) + pointerDelimSuffix - } - return formatHex(uint64(v)) -} - -// pointerReferences is a stack of pointers visited so far. -type pointerReferences [][2]value.Pointer - -func (ps *pointerReferences) PushPair(vx, vy reflect.Value, d diffMode, deref bool) (pp [2]value.Pointer) { - if deref && vx.IsValid() { - vx = vx.Addr() - } - if deref && vy.IsValid() { - vy = vy.Addr() - } - switch d { - case diffUnknown, diffIdentical: - pp = [2]value.Pointer{value.PointerOf(vx), value.PointerOf(vy)} - case diffRemoved: - pp = [2]value.Pointer{value.PointerOf(vx), value.Pointer{}} - case diffInserted: - pp = [2]value.Pointer{value.Pointer{}, value.PointerOf(vy)} - } - *ps = append(*ps, pp) - return pp -} - -func (ps *pointerReferences) Push(v reflect.Value) (p value.Pointer, seen bool) { - p = value.PointerOf(v) - for _, pp := range *ps { - if p == pp[0] || p == pp[1] { - return p, true - } - } - *ps = append(*ps, [2]value.Pointer{p, p}) - return p, false -} - -func (ps *pointerReferences) Pop() { - *ps = (*ps)[:len(*ps)-1] -} - -// trunkReferences is metadata for a textNode indicating that the sub-tree -// represents the value for either pointer in a pair of references. -type trunkReferences struct{ pp [2]value.Pointer } - -// trunkReference is metadata for a textNode indicating that the sub-tree -// represents the value for the given pointer reference. -type trunkReference struct{ p value.Pointer } - -// leafReference is metadata for a textNode indicating that the value is -// truncated as it refers to another part of the tree (i.e., a trunk). -type leafReference struct{ p value.Pointer } - -func wrapTrunkReferences(pp [2]value.Pointer, s textNode) textNode { - switch { - case pp[0].IsNil(): - return &textWrap{Value: s, Metadata: trunkReference{pp[1]}} - case pp[1].IsNil(): - return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} - case pp[0] == pp[1]: - return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} - default: - return &textWrap{Value: s, Metadata: trunkReferences{pp}} - } -} -func wrapTrunkReference(p value.Pointer, printAddress bool, s textNode) textNode { - var prefix string - if printAddress { - prefix = formatPointer(p, true) - } - return &textWrap{Prefix: prefix, Value: s, Metadata: trunkReference{p}} -} -func makeLeafReference(p value.Pointer, printAddress bool) textNode { - out := &textWrap{Prefix: "(", Value: textEllipsis, Suffix: ")"} - var prefix string - if printAddress { - prefix = formatPointer(p, true) - } - return &textWrap{Prefix: prefix, Value: out, Metadata: leafReference{p}} -} - -// resolveReferences walks the textNode tree searching for any leaf reference -// metadata and resolves each against the corresponding trunk references. -// Since pointer addresses in memory are not particularly readable to the user, -// it replaces each pointer value with an arbitrary and unique reference ID. -func resolveReferences(s textNode) { - var walkNodes func(textNode, func(textNode)) - walkNodes = func(s textNode, f func(textNode)) { - f(s) - switch s := s.(type) { - case *textWrap: - walkNodes(s.Value, f) - case textList: - for _, r := range s { - walkNodes(r.Value, f) - } - } - } - - // Collect all trunks and leaves with reference metadata. - var trunks, leaves []*textWrap - walkNodes(s, func(s textNode) { - if s, ok := s.(*textWrap); ok { - switch s.Metadata.(type) { - case leafReference: - leaves = append(leaves, s) - case trunkReference, trunkReferences: - trunks = append(trunks, s) - } - } - }) - - // No leaf references to resolve. - if len(leaves) == 0 { - return - } - - // Collect the set of all leaf references to resolve. - leafPtrs := make(map[value.Pointer]bool) - for _, leaf := range leaves { - leafPtrs[leaf.Metadata.(leafReference).p] = true - } - - // Collect the set of trunk pointers that are always paired together. - // This allows us to assign a single ID to both pointers for brevity. - // If a pointer in a pair ever occurs by itself or as a different pair, - // then the pair is broken. - pairedTrunkPtrs := make(map[value.Pointer]value.Pointer) - unpair := func(p value.Pointer) { - if !pairedTrunkPtrs[p].IsNil() { - pairedTrunkPtrs[pairedTrunkPtrs[p]] = value.Pointer{} // invalidate other half - } - pairedTrunkPtrs[p] = value.Pointer{} // invalidate this half - } - for _, trunk := range trunks { - switch p := trunk.Metadata.(type) { - case trunkReference: - unpair(p.p) // standalone pointer cannot be part of a pair - case trunkReferences: - p0, ok0 := pairedTrunkPtrs[p.pp[0]] - p1, ok1 := pairedTrunkPtrs[p.pp[1]] - switch { - case !ok0 && !ok1: - // Register the newly seen pair. - pairedTrunkPtrs[p.pp[0]] = p.pp[1] - pairedTrunkPtrs[p.pp[1]] = p.pp[0] - case ok0 && ok1 && p0 == p.pp[1] && p1 == p.pp[0]: - // Exact pair already seen; do nothing. - default: - // Pair conflicts with some other pair; break all pairs. - unpair(p.pp[0]) - unpair(p.pp[1]) - } - } - } - - // Correlate each pointer referenced by leaves to a unique identifier, - // and print the IDs for each trunk that matches those pointers. - var nextID uint - ptrIDs := make(map[value.Pointer]uint) - newID := func() uint { - id := nextID - nextID++ - return id - } - for _, trunk := range trunks { - switch p := trunk.Metadata.(type) { - case trunkReference: - if print := leafPtrs[p.p]; print { - id, ok := ptrIDs[p.p] - if !ok { - id = newID() - ptrIDs[p.p] = id - } - trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) - } - case trunkReferences: - print0 := leafPtrs[p.pp[0]] - print1 := leafPtrs[p.pp[1]] - if print0 || print1 { - id0, ok0 := ptrIDs[p.pp[0]] - id1, ok1 := ptrIDs[p.pp[1]] - isPair := pairedTrunkPtrs[p.pp[0]] == p.pp[1] && pairedTrunkPtrs[p.pp[1]] == p.pp[0] - if isPair { - var id uint - assert(ok0 == ok1) // must be seen together or not at all - if ok0 { - assert(id0 == id1) // must have the same ID - id = id0 - } else { - id = newID() - ptrIDs[p.pp[0]] = id - ptrIDs[p.pp[1]] = id - } - trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) - } else { - if print0 && !ok0 { - id0 = newID() - ptrIDs[p.pp[0]] = id0 - } - if print1 && !ok1 { - id1 = newID() - ptrIDs[p.pp[1]] = id1 - } - switch { - case print0 && print1: - trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)+","+formatReference(id1)) - case print0: - trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)) - case print1: - trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id1)) - } - } - } - } - } - - // Update all leaf references with the unique identifier. - for _, leaf := range leaves { - if id, ok := ptrIDs[leaf.Metadata.(leafReference).p]; ok { - leaf.Prefix = updateReferencePrefix(leaf.Prefix, formatReference(id)) - } - } -} - -func formatReference(id uint) string { - return fmt.Sprintf("ref#%d", id) -} - -func updateReferencePrefix(prefix, ref string) string { - if prefix == "" { - return pointerDelimPrefix + ref + pointerDelimSuffix - } - suffix := strings.TrimPrefix(prefix, pointerDelimPrefix) - return pointerDelimPrefix + ref + ": " + suffix -} diff --git a/vendor/github.com/google/go-cmp/cmp/report_reflect.go b/vendor/github.com/google/go-cmp/cmp/report_reflect.go deleted file mode 100644 index e39f42284..000000000 --- a/vendor/github.com/google/go-cmp/cmp/report_reflect.go +++ /dev/null @@ -1,414 +0,0 @@ -// Copyright 2019, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmp - -import ( - "bytes" - "fmt" - "reflect" - "strconv" - "strings" - "unicode" - "unicode/utf8" - - "github.com/google/go-cmp/cmp/internal/value" -) - -var ( - anyType = reflect.TypeOf((*interface{})(nil)).Elem() - stringType = reflect.TypeOf((*string)(nil)).Elem() - bytesType = reflect.TypeOf((*[]byte)(nil)).Elem() - byteType = reflect.TypeOf((*byte)(nil)).Elem() -) - -type formatValueOptions struct { - // AvoidStringer controls whether to avoid calling custom stringer - // methods like error.Error or fmt.Stringer.String. - AvoidStringer bool - - // PrintAddresses controls whether to print the address of all pointers, - // slice elements, and maps. - PrintAddresses bool - - // QualifiedNames controls whether FormatType uses the fully qualified name - // (including the full package path as opposed to just the package name). - QualifiedNames bool - - // VerbosityLevel controls the amount of output to produce. - // A higher value produces more output. A value of zero or lower produces - // no output (represented using an ellipsis). - // If LimitVerbosity is false, then the level is treated as infinite. - VerbosityLevel int - - // LimitVerbosity specifies that formatting should respect VerbosityLevel. - LimitVerbosity bool -} - -// FormatType prints the type as if it were wrapping s. -// This may return s as-is depending on the current type and TypeMode mode. -func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode { - // Check whether to emit the type or not. - switch opts.TypeMode { - case autoType: - switch t.Kind() { - case reflect.Struct, reflect.Slice, reflect.Array, reflect.Map: - if s.Equal(textNil) { - return s - } - default: - return s - } - if opts.DiffMode == diffIdentical { - return s // elide type for identical nodes - } - case elideType: - return s - } - - // Determine the type label, applying special handling for unnamed types. - typeName := value.TypeString(t, opts.QualifiedNames) - if t.Name() == "" { - // According to Go grammar, certain type literals contain symbols that - // do not strongly bind to the next lexicographical token (e.g., *T). - switch t.Kind() { - case reflect.Chan, reflect.Func, reflect.Ptr: - typeName = "(" + typeName + ")" - } - } - return &textWrap{Prefix: typeName, Value: wrapParens(s)} -} - -// wrapParens wraps s with a set of parenthesis, but avoids it if the -// wrapped node itself is already surrounded by a pair of parenthesis or braces. -// It handles unwrapping one level of pointer-reference nodes. -func wrapParens(s textNode) textNode { - var refNode *textWrap - if s2, ok := s.(*textWrap); ok { - // Unwrap a single pointer reference node. - switch s2.Metadata.(type) { - case leafReference, trunkReference, trunkReferences: - refNode = s2 - if s3, ok := refNode.Value.(*textWrap); ok { - s2 = s3 - } - } - - // Already has delimiters that make parenthesis unnecessary. - hasParens := strings.HasPrefix(s2.Prefix, "(") && strings.HasSuffix(s2.Suffix, ")") - hasBraces := strings.HasPrefix(s2.Prefix, "{") && strings.HasSuffix(s2.Suffix, "}") - if hasParens || hasBraces { - return s - } - } - if refNode != nil { - refNode.Value = &textWrap{Prefix: "(", Value: refNode.Value, Suffix: ")"} - return s - } - return &textWrap{Prefix: "(", Value: s, Suffix: ")"} -} - -// FormatValue prints the reflect.Value, taking extra care to avoid descending -// into pointers already in ptrs. As pointers are visited, ptrs is also updated. -func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, ptrs *pointerReferences) (out textNode) { - if !v.IsValid() { - return nil - } - t := v.Type() - - // Check slice element for cycles. - if parentKind == reflect.Slice { - ptrRef, visited := ptrs.Push(v.Addr()) - if visited { - return makeLeafReference(ptrRef, false) - } - defer ptrs.Pop() - defer func() { out = wrapTrunkReference(ptrRef, false, out) }() - } - - // Check whether there is an Error or String method to call. - if !opts.AvoidStringer && v.CanInterface() { - // Avoid calling Error or String methods on nil receivers since many - // implementations crash when doing so. - if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() { - var prefix, strVal string - func() { - // Swallow and ignore any panics from String or Error. - defer func() { recover() }() - switch v := v.Interface().(type) { - case error: - strVal = v.Error() - prefix = "e" - case fmt.Stringer: - strVal = v.String() - prefix = "s" - } - }() - if prefix != "" { - return opts.formatString(prefix, strVal) - } - } - } - - // Check whether to explicitly wrap the result with the type. - var skipType bool - defer func() { - if !skipType { - out = opts.FormatType(t, out) - } - }() - - switch t.Kind() { - case reflect.Bool: - return textLine(fmt.Sprint(v.Bool())) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return textLine(fmt.Sprint(v.Int())) - case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return textLine(fmt.Sprint(v.Uint())) - case reflect.Uint8: - if parentKind == reflect.Slice || parentKind == reflect.Array { - return textLine(formatHex(v.Uint())) - } - return textLine(fmt.Sprint(v.Uint())) - case reflect.Uintptr: - return textLine(formatHex(v.Uint())) - case reflect.Float32, reflect.Float64: - return textLine(fmt.Sprint(v.Float())) - case reflect.Complex64, reflect.Complex128: - return textLine(fmt.Sprint(v.Complex())) - case reflect.String: - return opts.formatString("", v.String()) - case reflect.UnsafePointer, reflect.Chan, reflect.Func: - return textLine(formatPointer(value.PointerOf(v), true)) - case reflect.Struct: - var list textList - v := makeAddressable(v) // needed for retrieveUnexportedField - maxLen := v.NumField() - if opts.LimitVerbosity { - maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... - opts.VerbosityLevel-- - } - for i := 0; i < v.NumField(); i++ { - vv := v.Field(i) - if vv.IsZero() { - continue // Elide fields with zero values - } - if len(list) == maxLen { - list.AppendEllipsis(diffStats{}) - break - } - sf := t.Field(i) - if !isExported(sf.Name) { - vv = retrieveUnexportedField(v, sf, true) - } - s := opts.WithTypeMode(autoType).FormatValue(vv, t.Kind(), ptrs) - list = append(list, textRecord{Key: sf.Name, Value: s}) - } - return &textWrap{Prefix: "{", Value: list, Suffix: "}"} - case reflect.Slice: - if v.IsNil() { - return textNil - } - - // Check whether this is a []byte of text data. - if t.Elem() == byteType { - b := v.Bytes() - isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) || unicode.IsSpace(r) } - if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 { - out = opts.formatString("", string(b)) - skipType = true - return opts.FormatType(t, out) - } - } - - fallthrough - case reflect.Array: - maxLen := v.Len() - if opts.LimitVerbosity { - maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... - opts.VerbosityLevel-- - } - var list textList - for i := 0; i < v.Len(); i++ { - if len(list) == maxLen { - list.AppendEllipsis(diffStats{}) - break - } - s := opts.WithTypeMode(elideType).FormatValue(v.Index(i), t.Kind(), ptrs) - list = append(list, textRecord{Value: s}) - } - - out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} - if t.Kind() == reflect.Slice && opts.PrintAddresses { - header := fmt.Sprintf("ptr:%v, len:%d, cap:%d", formatPointer(value.PointerOf(v), false), v.Len(), v.Cap()) - out = &textWrap{Prefix: pointerDelimPrefix + header + pointerDelimSuffix, Value: out} - } - return out - case reflect.Map: - if v.IsNil() { - return textNil - } - - // Check pointer for cycles. - ptrRef, visited := ptrs.Push(v) - if visited { - return makeLeafReference(ptrRef, opts.PrintAddresses) - } - defer ptrs.Pop() - - maxLen := v.Len() - if opts.LimitVerbosity { - maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... - opts.VerbosityLevel-- - } - var list textList - for _, k := range value.SortKeys(v.MapKeys()) { - if len(list) == maxLen { - list.AppendEllipsis(diffStats{}) - break - } - sk := formatMapKey(k, false, ptrs) - sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), t.Kind(), ptrs) - list = append(list, textRecord{Key: sk, Value: sv}) - } - - out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} - out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) - return out - case reflect.Ptr: - if v.IsNil() { - return textNil - } - - // Check pointer for cycles. - ptrRef, visited := ptrs.Push(v) - if visited { - out = makeLeafReference(ptrRef, opts.PrintAddresses) - return &textWrap{Prefix: "&", Value: out} - } - defer ptrs.Pop() - - // Skip the name only if this is an unnamed pointer type. - // Otherwise taking the address of a value does not reproduce - // the named pointer type. - if v.Type().Name() == "" { - skipType = true // Let the underlying value print the type instead - } - out = opts.FormatValue(v.Elem(), t.Kind(), ptrs) - out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) - out = &textWrap{Prefix: "&", Value: out} - return out - case reflect.Interface: - if v.IsNil() { - return textNil - } - // Interfaces accept different concrete types, - // so configure the underlying value to explicitly print the type. - return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs) - default: - panic(fmt.Sprintf("%v kind not handled", v.Kind())) - } -} - -func (opts formatOptions) formatString(prefix, s string) textNode { - maxLen := len(s) - maxLines := strings.Count(s, "\n") + 1 - if opts.LimitVerbosity { - maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc... - maxLines = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc... - } - - // For multiline strings, use the triple-quote syntax, - // but only use it when printing removed or inserted nodes since - // we only want the extra verbosity for those cases. - lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n") - isTripleQuoted := len(lines) >= 4 && (opts.DiffMode == '-' || opts.DiffMode == '+') - for i := 0; i < len(lines) && isTripleQuoted; i++ { - lines[i] = strings.TrimPrefix(strings.TrimSuffix(lines[i], "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support - isPrintable := func(r rune) bool { - return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable - } - line := lines[i] - isTripleQuoted = !strings.HasPrefix(strings.TrimPrefix(line, prefix), `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" && len(line) <= maxLen - } - if isTripleQuoted { - var list textList - list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) - for i, line := range lines { - if numElided := len(lines) - i; i == maxLines-1 && numElided > 1 { - comment := commentString(fmt.Sprintf("%d elided lines", numElided)) - list = append(list, textRecord{Diff: opts.DiffMode, Value: textEllipsis, ElideComma: true, Comment: comment}) - break - } - list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(line), ElideComma: true}) - } - list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) - return &textWrap{Prefix: "(", Value: list, Suffix: ")"} - } - - // Format the string as a single-line quoted string. - if len(s) > maxLen+len(textEllipsis) { - return textLine(prefix + formatString(s[:maxLen]) + string(textEllipsis)) - } - return textLine(prefix + formatString(s)) -} - -// formatMapKey formats v as if it were a map key. -// The result is guaranteed to be a single line. -func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string { - var opts formatOptions - opts.DiffMode = diffIdentical - opts.TypeMode = elideType - opts.PrintAddresses = disambiguate - opts.AvoidStringer = disambiguate - opts.QualifiedNames = disambiguate - opts.VerbosityLevel = maxVerbosityPreset - opts.LimitVerbosity = true - s := opts.FormatValue(v, reflect.Map, ptrs).String() - return strings.TrimSpace(s) -} - -// formatString prints s as a double-quoted or backtick-quoted string. -func formatString(s string) string { - // Use quoted string if it the same length as a raw string literal. - // Otherwise, attempt to use the raw string form. - qs := strconv.Quote(s) - if len(qs) == 1+len(s)+1 { - return qs - } - - // Disallow newlines to ensure output is a single line. - // Only allow printable runes for readability purposes. - rawInvalid := func(r rune) bool { - return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t') - } - if utf8.ValidString(s) && strings.IndexFunc(s, rawInvalid) < 0 { - return "`" + s + "`" - } - return qs -} - -// formatHex prints u as a hexadecimal integer in Go notation. -func formatHex(u uint64) string { - var f string - switch { - case u <= 0xff: - f = "0x%02x" - case u <= 0xffff: - f = "0x%04x" - case u <= 0xffffff: - f = "0x%06x" - case u <= 0xffffffff: - f = "0x%08x" - case u <= 0xffffffffff: - f = "0x%010x" - case u <= 0xffffffffffff: - f = "0x%012x" - case u <= 0xffffffffffffff: - f = "0x%014x" - case u <= 0xffffffffffffffff: - f = "0x%016x" - } - return fmt.Sprintf(f, u) -} diff --git a/vendor/github.com/google/go-cmp/cmp/report_slices.go b/vendor/github.com/google/go-cmp/cmp/report_slices.go deleted file mode 100644 index 23e444f62..000000000 --- a/vendor/github.com/google/go-cmp/cmp/report_slices.go +++ /dev/null @@ -1,614 +0,0 @@ -// Copyright 2019, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmp - -import ( - "bytes" - "fmt" - "math" - "reflect" - "strconv" - "strings" - "unicode" - "unicode/utf8" - - "github.com/google/go-cmp/cmp/internal/diff" -) - -// CanFormatDiffSlice reports whether we support custom formatting for nodes -// that are slices of primitive kinds or strings. -func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { - switch { - case opts.DiffMode != diffUnknown: - return false // Must be formatting in diff mode - case v.NumDiff == 0: - return false // No differences detected - case !v.ValueX.IsValid() || !v.ValueY.IsValid(): - return false // Both values must be valid - case v.NumIgnored > 0: - return false // Some ignore option was used - case v.NumTransformed > 0: - return false // Some transform option was used - case v.NumCompared > 1: - return false // More than one comparison was used - case v.NumCompared == 1 && v.Type.Name() != "": - // The need for cmp to check applicability of options on every element - // in a slice is a significant performance detriment for large []byte. - // The workaround is to specify Comparer(bytes.Equal), - // which enables cmp to compare []byte more efficiently. - // If they differ, we still want to provide batched diffing. - // The logic disallows named types since they tend to have their own - // String method, with nicer formatting than what this provides. - return false - } - - // Check whether this is an interface with the same concrete types. - t := v.Type - vx, vy := v.ValueX, v.ValueY - if t.Kind() == reflect.Interface && !vx.IsNil() && !vy.IsNil() && vx.Elem().Type() == vy.Elem().Type() { - vx, vy = vx.Elem(), vy.Elem() - t = vx.Type() - } - - // Check whether we provide specialized diffing for this type. - switch t.Kind() { - case reflect.String: - case reflect.Array, reflect.Slice: - // Only slices of primitive types have specialized handling. - switch t.Elem().Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, - reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: - default: - return false - } - - // Both slice values have to be non-empty. - if t.Kind() == reflect.Slice && (vx.Len() == 0 || vy.Len() == 0) { - return false - } - - // If a sufficient number of elements already differ, - // use specialized formatting even if length requirement is not met. - if v.NumDiff > v.NumSame { - return true - } - default: - return false - } - - // Use specialized string diffing for longer slices or strings. - const minLength = 32 - return vx.Len() >= minLength && vy.Len() >= minLength -} - -// FormatDiffSlice prints a diff for the slices (or strings) represented by v. -// This provides custom-tailored logic to make printing of differences in -// textual strings and slices of primitive kinds more readable. -func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { - assert(opts.DiffMode == diffUnknown) - t, vx, vy := v.Type, v.ValueX, v.ValueY - if t.Kind() == reflect.Interface { - vx, vy = vx.Elem(), vy.Elem() - t = vx.Type() - opts = opts.WithTypeMode(emitType) - } - - // Auto-detect the type of the data. - var sx, sy string - var ssx, ssy []string - var isString, isMostlyText, isPureLinedText, isBinary bool - switch { - case t.Kind() == reflect.String: - sx, sy = vx.String(), vy.String() - isString = true - case t.Kind() == reflect.Slice && t.Elem() == byteType: - sx, sy = string(vx.Bytes()), string(vy.Bytes()) - isString = true - case t.Kind() == reflect.Array: - // Arrays need to be addressable for slice operations to work. - vx2, vy2 := reflect.New(t).Elem(), reflect.New(t).Elem() - vx2.Set(vx) - vy2.Set(vy) - vx, vy = vx2, vy2 - } - if isString { - var numTotalRunes, numValidRunes, numLines, lastLineIdx, maxLineLen int - for i, r := range sx + sy { - numTotalRunes++ - if (unicode.IsPrint(r) || unicode.IsSpace(r)) && r != utf8.RuneError { - numValidRunes++ - } - if r == '\n' { - if maxLineLen < i-lastLineIdx { - maxLineLen = i - lastLineIdx - } - lastLineIdx = i + 1 - numLines++ - } - } - isPureText := numValidRunes == numTotalRunes - isMostlyText = float64(numValidRunes) > math.Floor(0.90*float64(numTotalRunes)) - isPureLinedText = isPureText && numLines >= 4 && maxLineLen <= 1024 - isBinary = !isMostlyText - - // Avoid diffing by lines if it produces a significantly more complex - // edit script than diffing by bytes. - if isPureLinedText { - ssx = strings.Split(sx, "\n") - ssy = strings.Split(sy, "\n") - esLines := diff.Difference(len(ssx), len(ssy), func(ix, iy int) diff.Result { - return diff.BoolResult(ssx[ix] == ssy[iy]) - }) - esBytes := diff.Difference(len(sx), len(sy), func(ix, iy int) diff.Result { - return diff.BoolResult(sx[ix] == sy[iy]) - }) - efficiencyLines := float64(esLines.Dist()) / float64(len(esLines)) - efficiencyBytes := float64(esBytes.Dist()) / float64(len(esBytes)) - quotedLength := len(strconv.Quote(sx + sy)) - unquotedLength := len(sx) + len(sy) - escapeExpansionRatio := float64(quotedLength) / float64(unquotedLength) - isPureLinedText = efficiencyLines < 4*efficiencyBytes || escapeExpansionRatio > 1.1 - } - } - - // Format the string into printable records. - var list textList - var delim string - switch { - // If the text appears to be multi-lined text, - // then perform differencing across individual lines. - case isPureLinedText: - list = opts.formatDiffSlice( - reflect.ValueOf(ssx), reflect.ValueOf(ssy), 1, "line", - func(v reflect.Value, d diffMode) textRecord { - s := formatString(v.Index(0).String()) - return textRecord{Diff: d, Value: textLine(s)} - }, - ) - delim = "\n" - - // If possible, use a custom triple-quote (""") syntax for printing - // differences in a string literal. This format is more readable, - // but has edge-cases where differences are visually indistinguishable. - // This format is avoided under the following conditions: - // - A line starts with `"""` - // - A line starts with "..." - // - A line contains non-printable characters - // - Adjacent different lines differ only by whitespace - // - // For example: - // - // """ - // ... // 3 identical lines - // foo - // bar - // - baz - // + BAZ - // """ - isTripleQuoted := true - prevRemoveLines := map[string]bool{} - prevInsertLines := map[string]bool{} - var list2 textList - list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) - for _, r := range list { - if !r.Value.Equal(textEllipsis) { - line, _ := strconv.Unquote(string(r.Value.(textLine))) - line = strings.TrimPrefix(strings.TrimSuffix(line, "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support - normLine := strings.Map(func(r rune) rune { - if unicode.IsSpace(r) { - return -1 // drop whitespace to avoid visually indistinguishable output - } - return r - }, line) - isPrintable := func(r rune) bool { - return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable - } - isTripleQuoted = !strings.HasPrefix(line, `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" - switch r.Diff { - case diffRemoved: - isTripleQuoted = isTripleQuoted && !prevInsertLines[normLine] - prevRemoveLines[normLine] = true - case diffInserted: - isTripleQuoted = isTripleQuoted && !prevRemoveLines[normLine] - prevInsertLines[normLine] = true - } - if !isTripleQuoted { - break - } - r.Value = textLine(line) - r.ElideComma = true - } - if !(r.Diff == diffRemoved || r.Diff == diffInserted) { // start a new non-adjacent difference group - prevRemoveLines = map[string]bool{} - prevInsertLines = map[string]bool{} - } - list2 = append(list2, r) - } - if r := list2[len(list2)-1]; r.Diff == diffIdentical && len(r.Value.(textLine)) == 0 { - list2 = list2[:len(list2)-1] // elide single empty line at the end - } - list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) - if isTripleQuoted { - var out textNode = &textWrap{Prefix: "(", Value: list2, Suffix: ")"} - switch t.Kind() { - case reflect.String: - if t != stringType { - out = opts.FormatType(t, out) - } - case reflect.Slice: - // Always emit type for slices since the triple-quote syntax - // looks like a string (not a slice). - opts = opts.WithTypeMode(emitType) - out = opts.FormatType(t, out) - } - return out - } - - // If the text appears to be single-lined text, - // then perform differencing in approximately fixed-sized chunks. - // The output is printed as quoted strings. - case isMostlyText: - list = opts.formatDiffSlice( - reflect.ValueOf(sx), reflect.ValueOf(sy), 64, "byte", - func(v reflect.Value, d diffMode) textRecord { - s := formatString(v.String()) - return textRecord{Diff: d, Value: textLine(s)} - }, - ) - - // If the text appears to be binary data, - // then perform differencing in approximately fixed-sized chunks. - // The output is inspired by hexdump. - case isBinary: - list = opts.formatDiffSlice( - reflect.ValueOf(sx), reflect.ValueOf(sy), 16, "byte", - func(v reflect.Value, d diffMode) textRecord { - var ss []string - for i := 0; i < v.Len(); i++ { - ss = append(ss, formatHex(v.Index(i).Uint())) - } - s := strings.Join(ss, ", ") - comment := commentString(fmt.Sprintf("%c|%v|", d, formatASCII(v.String()))) - return textRecord{Diff: d, Value: textLine(s), Comment: comment} - }, - ) - - // For all other slices of primitive types, - // then perform differencing in approximately fixed-sized chunks. - // The size of each chunk depends on the width of the element kind. - default: - var chunkSize int - if t.Elem().Kind() == reflect.Bool { - chunkSize = 16 - } else { - switch t.Elem().Bits() { - case 8: - chunkSize = 16 - case 16: - chunkSize = 12 - case 32: - chunkSize = 8 - default: - chunkSize = 8 - } - } - list = opts.formatDiffSlice( - vx, vy, chunkSize, t.Elem().Kind().String(), - func(v reflect.Value, d diffMode) textRecord { - var ss []string - for i := 0; i < v.Len(); i++ { - switch t.Elem().Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - ss = append(ss, fmt.Sprint(v.Index(i).Int())) - case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: - ss = append(ss, fmt.Sprint(v.Index(i).Uint())) - case reflect.Uint8, reflect.Uintptr: - ss = append(ss, formatHex(v.Index(i).Uint())) - case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: - ss = append(ss, fmt.Sprint(v.Index(i).Interface())) - } - } - s := strings.Join(ss, ", ") - return textRecord{Diff: d, Value: textLine(s)} - }, - ) - } - - // Wrap the output with appropriate type information. - var out textNode = &textWrap{Prefix: "{", Value: list, Suffix: "}"} - if !isMostlyText { - // The "{...}" byte-sequence literal is not valid Go syntax for strings. - // Emit the type for extra clarity (e.g. "string{...}"). - if t.Kind() == reflect.String { - opts = opts.WithTypeMode(emitType) - } - return opts.FormatType(t, out) - } - switch t.Kind() { - case reflect.String: - out = &textWrap{Prefix: "strings.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)} - if t != stringType { - out = opts.FormatType(t, out) - } - case reflect.Slice: - out = &textWrap{Prefix: "bytes.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)} - if t != bytesType { - out = opts.FormatType(t, out) - } - } - return out -} - -// formatASCII formats s as an ASCII string. -// This is useful for printing binary strings in a semi-legible way. -func formatASCII(s string) string { - b := bytes.Repeat([]byte{'.'}, len(s)) - for i := 0; i < len(s); i++ { - if ' ' <= s[i] && s[i] <= '~' { - b[i] = s[i] - } - } - return string(b) -} - -func (opts formatOptions) formatDiffSlice( - vx, vy reflect.Value, chunkSize int, name string, - makeRec func(reflect.Value, diffMode) textRecord, -) (list textList) { - eq := func(ix, iy int) bool { - return vx.Index(ix).Interface() == vy.Index(iy).Interface() - } - es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { - return diff.BoolResult(eq(ix, iy)) - }) - - appendChunks := func(v reflect.Value, d diffMode) int { - n0 := v.Len() - for v.Len() > 0 { - n := chunkSize - if n > v.Len() { - n = v.Len() - } - list = append(list, makeRec(v.Slice(0, n), d)) - v = v.Slice(n, v.Len()) - } - return n0 - v.Len() - } - - var numDiffs int - maxLen := -1 - if opts.LimitVerbosity { - maxLen = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc... - opts.VerbosityLevel-- - } - - groups := coalesceAdjacentEdits(name, es) - groups = coalesceInterveningIdentical(groups, chunkSize/4) - groups = cleanupSurroundingIdentical(groups, eq) - maxGroup := diffStats{Name: name} - for i, ds := range groups { - if maxLen >= 0 && numDiffs >= maxLen { - maxGroup = maxGroup.Append(ds) - continue - } - - // Print equal. - if ds.NumDiff() == 0 { - // Compute the number of leading and trailing equal bytes to print. - var numLo, numHi int - numEqual := ds.NumIgnored + ds.NumIdentical - for numLo < chunkSize*numContextRecords && numLo+numHi < numEqual && i != 0 { - numLo++ - } - for numHi < chunkSize*numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 { - numHi++ - } - if numEqual-(numLo+numHi) <= chunkSize && ds.NumIgnored == 0 { - numHi = numEqual - numLo // Avoid pointless coalescing of single equal row - } - - // Print the equal bytes. - appendChunks(vx.Slice(0, numLo), diffIdentical) - if numEqual > numLo+numHi { - ds.NumIdentical -= numLo + numHi - list.AppendEllipsis(ds) - } - appendChunks(vx.Slice(numEqual-numHi, numEqual), diffIdentical) - vx = vx.Slice(numEqual, vx.Len()) - vy = vy.Slice(numEqual, vy.Len()) - continue - } - - // Print unequal. - len0 := len(list) - nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved) - vx = vx.Slice(nx, vx.Len()) - ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted) - vy = vy.Slice(ny, vy.Len()) - numDiffs += len(list) - len0 - } - if maxGroup.IsZero() { - assert(vx.Len() == 0 && vy.Len() == 0) - } else { - list.AppendEllipsis(maxGroup) - } - return list -} - -// coalesceAdjacentEdits coalesces the list of edits into groups of adjacent -// equal or unequal counts. -// -// Example: -// -// Input: "..XXY...Y" -// Output: [ -// {NumIdentical: 2}, -// {NumRemoved: 2, NumInserted 1}, -// {NumIdentical: 3}, -// {NumInserted: 1}, -// ] -func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) { - var prevMode byte - lastStats := func(mode byte) *diffStats { - if prevMode != mode { - groups = append(groups, diffStats{Name: name}) - prevMode = mode - } - return &groups[len(groups)-1] - } - for _, e := range es { - switch e { - case diff.Identity: - lastStats('=').NumIdentical++ - case diff.UniqueX: - lastStats('!').NumRemoved++ - case diff.UniqueY: - lastStats('!').NumInserted++ - case diff.Modified: - lastStats('!').NumModified++ - } - } - return groups -} - -// coalesceInterveningIdentical coalesces sufficiently short (<= windowSize) -// equal groups into adjacent unequal groups that currently result in a -// dual inserted/removed printout. This acts as a high-pass filter to smooth -// out high-frequency changes within the windowSize. -// -// Example: -// -// WindowSize: 16, -// Input: [ -// {NumIdentical: 61}, // group 0 -// {NumRemoved: 3, NumInserted: 1}, // group 1 -// {NumIdentical: 6}, // ├── coalesce -// {NumInserted: 2}, // ├── coalesce -// {NumIdentical: 1}, // ├── coalesce -// {NumRemoved: 9}, // └── coalesce -// {NumIdentical: 64}, // group 2 -// {NumRemoved: 3, NumInserted: 1}, // group 3 -// {NumIdentical: 6}, // ├── coalesce -// {NumInserted: 2}, // ├── coalesce -// {NumIdentical: 1}, // ├── coalesce -// {NumRemoved: 7}, // ├── coalesce -// {NumIdentical: 1}, // ├── coalesce -// {NumRemoved: 2}, // └── coalesce -// {NumIdentical: 63}, // group 4 -// ] -// Output: [ -// {NumIdentical: 61}, -// {NumIdentical: 7, NumRemoved: 12, NumInserted: 3}, -// {NumIdentical: 64}, -// {NumIdentical: 8, NumRemoved: 12, NumInserted: 3}, -// {NumIdentical: 63}, -// ] -func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStats { - groups, groupsOrig := groups[:0], groups - for i, ds := range groupsOrig { - if len(groups) >= 2 && ds.NumDiff() > 0 { - prev := &groups[len(groups)-2] // Unequal group - curr := &groups[len(groups)-1] // Equal group - next := &groupsOrig[i] // Unequal group - hadX, hadY := prev.NumRemoved > 0, prev.NumInserted > 0 - hasX, hasY := next.NumRemoved > 0, next.NumInserted > 0 - if ((hadX || hasX) && (hadY || hasY)) && curr.NumIdentical <= windowSize { - *prev = prev.Append(*curr).Append(*next) - groups = groups[:len(groups)-1] // Truncate off equal group - continue - } - } - groups = append(groups, ds) - } - return groups -} - -// cleanupSurroundingIdentical scans through all unequal groups, and -// moves any leading sequence of equal elements to the preceding equal group and -// moves and trailing sequence of equal elements to the succeeding equal group. -// -// This is necessary since coalesceInterveningIdentical may coalesce edit groups -// together such that leading/trailing spans of equal elements becomes possible. -// Note that this can occur even with an optimal diffing algorithm. -// -// Example: -// -// Input: [ -// {NumIdentical: 61}, -// {NumIdentical: 1 , NumRemoved: 11, NumInserted: 2}, // assume 3 leading identical elements -// {NumIdentical: 67}, -// {NumIdentical: 7, NumRemoved: 12, NumInserted: 3}, // assume 10 trailing identical elements -// {NumIdentical: 54}, -// ] -// Output: [ -// {NumIdentical: 64}, // incremented by 3 -// {NumRemoved: 9}, -// {NumIdentical: 67}, -// {NumRemoved: 9}, -// {NumIdentical: 64}, // incremented by 10 -// ] -func cleanupSurroundingIdentical(groups []diffStats, eq func(i, j int) bool) []diffStats { - var ix, iy int // indexes into sequence x and y - for i, ds := range groups { - // Handle equal group. - if ds.NumDiff() == 0 { - ix += ds.NumIdentical - iy += ds.NumIdentical - continue - } - - // Handle unequal group. - nx := ds.NumIdentical + ds.NumRemoved + ds.NumModified - ny := ds.NumIdentical + ds.NumInserted + ds.NumModified - var numLeadingIdentical, numTrailingIdentical int - for j := 0; j < nx && j < ny && eq(ix+j, iy+j); j++ { - numLeadingIdentical++ - } - for j := 0; j < nx && j < ny && eq(ix+nx-1-j, iy+ny-1-j); j++ { - numTrailingIdentical++ - } - if numIdentical := numLeadingIdentical + numTrailingIdentical; numIdentical > 0 { - if numLeadingIdentical > 0 { - // Remove leading identical span from this group and - // insert it into the preceding group. - if i-1 >= 0 { - groups[i-1].NumIdentical += numLeadingIdentical - } else { - // No preceding group exists, so prepend a new group, - // but do so after we finish iterating over all groups. - defer func() { - groups = append([]diffStats{{Name: groups[0].Name, NumIdentical: numLeadingIdentical}}, groups...) - }() - } - // Increment indexes since the preceding group would have handled this. - ix += numLeadingIdentical - iy += numLeadingIdentical - } - if numTrailingIdentical > 0 { - // Remove trailing identical span from this group and - // insert it into the succeeding group. - if i+1 < len(groups) { - groups[i+1].NumIdentical += numTrailingIdentical - } else { - // No succeeding group exists, so append a new group, - // but do so after we finish iterating over all groups. - defer func() { - groups = append(groups, diffStats{Name: groups[len(groups)-1].Name, NumIdentical: numTrailingIdentical}) - }() - } - // Do not increment indexes since the succeeding group will handle this. - } - - // Update this group since some identical elements were removed. - nx -= numIdentical - ny -= numIdentical - groups[i] = diffStats{Name: ds.Name, NumRemoved: nx, NumInserted: ny} - } - ix += nx - iy += ny - } - return groups -} diff --git a/vendor/github.com/google/go-cmp/cmp/report_text.go b/vendor/github.com/google/go-cmp/cmp/report_text.go deleted file mode 100644 index 388fcf571..000000000 --- a/vendor/github.com/google/go-cmp/cmp/report_text.go +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright 2019, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmp - -import ( - "bytes" - "fmt" - "math/rand" - "strings" - "time" - "unicode/utf8" - - "github.com/google/go-cmp/cmp/internal/flags" -) - -var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 - -const maxColumnLength = 80 - -type indentMode int - -func (n indentMode) appendIndent(b []byte, d diffMode) []byte { - // The output of Diff is documented as being unstable to provide future - // flexibility in changing the output for more humanly readable reports. - // This logic intentionally introduces instability to the exact output - // so that users can detect accidental reliance on stability early on, - // rather than much later when an actual change to the format occurs. - if flags.Deterministic || randBool { - // Use regular spaces (U+0020). - switch d { - case diffUnknown, diffIdentical: - b = append(b, " "...) - case diffRemoved: - b = append(b, "- "...) - case diffInserted: - b = append(b, "+ "...) - } - } else { - // Use non-breaking spaces (U+00a0). - switch d { - case diffUnknown, diffIdentical: - b = append(b, "  "...) - case diffRemoved: - b = append(b, "- "...) - case diffInserted: - b = append(b, "+ "...) - } - } - return repeatCount(n).appendChar(b, '\t') -} - -type repeatCount int - -func (n repeatCount) appendChar(b []byte, c byte) []byte { - for ; n > 0; n-- { - b = append(b, c) - } - return b -} - -// textNode is a simplified tree-based representation of structured text. -// Possible node types are textWrap, textList, or textLine. -type textNode interface { - // Len reports the length in bytes of a single-line version of the tree. - // Nested textRecord.Diff and textRecord.Comment fields are ignored. - Len() int - // Equal reports whether the two trees are structurally identical. - // Nested textRecord.Diff and textRecord.Comment fields are compared. - Equal(textNode) bool - // String returns the string representation of the text tree. - // It is not guaranteed that len(x.String()) == x.Len(), - // nor that x.String() == y.String() implies that x.Equal(y). - String() string - - // formatCompactTo formats the contents of the tree as a single-line string - // to the provided buffer. Any nested textRecord.Diff and textRecord.Comment - // fields are ignored. - // - // However, not all nodes in the tree should be collapsed as a single-line. - // If a node can be collapsed as a single-line, it is replaced by a textLine - // node. Since the top-level node cannot replace itself, this also returns - // the current node itself. - // - // This does not mutate the receiver. - formatCompactTo([]byte, diffMode) ([]byte, textNode) - // formatExpandedTo formats the contents of the tree as a multi-line string - // to the provided buffer. In order for column alignment to operate well, - // formatCompactTo must be called before calling formatExpandedTo. - formatExpandedTo([]byte, diffMode, indentMode) []byte -} - -// textWrap is a wrapper that concatenates a prefix and/or a suffix -// to the underlying node. -type textWrap struct { - Prefix string // e.g., "bytes.Buffer{" - Value textNode // textWrap | textList | textLine - Suffix string // e.g., "}" - Metadata interface{} // arbitrary metadata; has no effect on formatting -} - -func (s *textWrap) Len() int { - return len(s.Prefix) + s.Value.Len() + len(s.Suffix) -} -func (s1 *textWrap) Equal(s2 textNode) bool { - if s2, ok := s2.(*textWrap); ok { - return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix - } - return false -} -func (s *textWrap) String() string { - var d diffMode - var n indentMode - _, s2 := s.formatCompactTo(nil, d) - b := n.appendIndent(nil, d) // Leading indent - b = s2.formatExpandedTo(b, d, n) // Main body - b = append(b, '\n') // Trailing newline - return string(b) -} -func (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { - n0 := len(b) // Original buffer length - b = append(b, s.Prefix...) - b, s.Value = s.Value.formatCompactTo(b, d) - b = append(b, s.Suffix...) - if _, ok := s.Value.(textLine); ok { - return b, textLine(b[n0:]) - } - return b, s -} -func (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { - b = append(b, s.Prefix...) - b = s.Value.formatExpandedTo(b, d, n) - b = append(b, s.Suffix...) - return b -} - -// textList is a comma-separated list of textWrap or textLine nodes. -// The list may be formatted as multi-lines or single-line at the discretion -// of the textList.formatCompactTo method. -type textList []textRecord -type textRecord struct { - Diff diffMode // e.g., 0 or '-' or '+' - Key string // e.g., "MyField" - Value textNode // textWrap | textLine - ElideComma bool // avoid trailing comma - Comment fmt.Stringer // e.g., "6 identical fields" -} - -// AppendEllipsis appends a new ellipsis node to the list if none already -// exists at the end. If cs is non-zero it coalesces the statistics with the -// previous diffStats. -func (s *textList) AppendEllipsis(ds diffStats) { - hasStats := !ds.IsZero() - if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) { - if hasStats { - *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds}) - } else { - *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true}) - } - return - } - if hasStats { - (*s)[len(*s)-1].Comment = (*s)[len(*s)-1].Comment.(diffStats).Append(ds) - } -} - -func (s textList) Len() (n int) { - for i, r := range s { - n += len(r.Key) - if r.Key != "" { - n += len(": ") - } - n += r.Value.Len() - if i < len(s)-1 { - n += len(", ") - } - } - return n -} - -func (s1 textList) Equal(s2 textNode) bool { - if s2, ok := s2.(textList); ok { - if len(s1) != len(s2) { - return false - } - for i := range s1 { - r1, r2 := s1[i], s2[i] - if !(r1.Diff == r2.Diff && r1.Key == r2.Key && r1.Value.Equal(r2.Value) && r1.Comment == r2.Comment) { - return false - } - } - return true - } - return false -} - -func (s textList) String() string { - return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String() -} - -func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { - s = append(textList(nil), s...) // Avoid mutating original - - // Determine whether we can collapse this list as a single line. - n0 := len(b) // Original buffer length - var multiLine bool - for i, r := range s { - if r.Diff == diffInserted || r.Diff == diffRemoved { - multiLine = true - } - b = append(b, r.Key...) - if r.Key != "" { - b = append(b, ": "...) - } - b, s[i].Value = r.Value.formatCompactTo(b, d|r.Diff) - if _, ok := s[i].Value.(textLine); !ok { - multiLine = true - } - if r.Comment != nil { - multiLine = true - } - if i < len(s)-1 { - b = append(b, ", "...) - } - } - // Force multi-lined output when printing a removed/inserted node that - // is sufficiently long. - if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength { - multiLine = true - } - if !multiLine { - return b, textLine(b[n0:]) - } - return b, s -} - -func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { - alignKeyLens := s.alignLens( - func(r textRecord) bool { - _, isLine := r.Value.(textLine) - return r.Key == "" || !isLine - }, - func(r textRecord) int { return utf8.RuneCountInString(r.Key) }, - ) - alignValueLens := s.alignLens( - func(r textRecord) bool { - _, isLine := r.Value.(textLine) - return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil - }, - func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) }, - ) - - // Format lists of simple lists in a batched form. - // If the list is sequence of only textLine values, - // then batch multiple values on a single line. - var isSimple bool - for _, r := range s { - _, isLine := r.Value.(textLine) - isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil - if !isSimple { - break - } - } - if isSimple { - n++ - var batch []byte - emitBatch := func() { - if len(batch) > 0 { - b = n.appendIndent(append(b, '\n'), d) - b = append(b, bytes.TrimRight(batch, " ")...) - batch = batch[:0] - } - } - for _, r := range s { - line := r.Value.(textLine) - if len(batch)+len(line)+len(", ") > maxColumnLength { - emitBatch() - } - batch = append(batch, line...) - batch = append(batch, ", "...) - } - emitBatch() - n-- - return n.appendIndent(append(b, '\n'), d) - } - - // Format the list as a multi-lined output. - n++ - for i, r := range s { - b = n.appendIndent(append(b, '\n'), d|r.Diff) - if r.Key != "" { - b = append(b, r.Key+": "...) - } - b = alignKeyLens[i].appendChar(b, ' ') - - b = r.Value.formatExpandedTo(b, d|r.Diff, n) - if !r.ElideComma { - b = append(b, ',') - } - b = alignValueLens[i].appendChar(b, ' ') - - if r.Comment != nil { - b = append(b, " // "+r.Comment.String()...) - } - } - n-- - - return n.appendIndent(append(b, '\n'), d) -} - -func (s textList) alignLens( - skipFunc func(textRecord) bool, - lenFunc func(textRecord) int, -) []repeatCount { - var startIdx, endIdx, maxLen int - lens := make([]repeatCount, len(s)) - for i, r := range s { - if skipFunc(r) { - for j := startIdx; j < endIdx && j < len(s); j++ { - lens[j] = repeatCount(maxLen - lenFunc(s[j])) - } - startIdx, endIdx, maxLen = i+1, i+1, 0 - } else { - if maxLen < lenFunc(r) { - maxLen = lenFunc(r) - } - endIdx = i + 1 - } - } - for j := startIdx; j < endIdx && j < len(s); j++ { - lens[j] = repeatCount(maxLen - lenFunc(s[j])) - } - return lens -} - -// textLine is a single-line segment of text and is always a leaf node -// in the textNode tree. -type textLine []byte - -var ( - textNil = textLine("nil") - textEllipsis = textLine("...") -) - -func (s textLine) Len() int { - return len(s) -} -func (s1 textLine) Equal(s2 textNode) bool { - if s2, ok := s2.(textLine); ok { - return bytes.Equal([]byte(s1), []byte(s2)) - } - return false -} -func (s textLine) String() string { - return string(s) -} -func (s textLine) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { - return append(b, s...), s -} -func (s textLine) formatExpandedTo(b []byte, _ diffMode, _ indentMode) []byte { - return append(b, s...) -} - -type diffStats struct { - Name string - NumIgnored int - NumIdentical int - NumRemoved int - NumInserted int - NumModified int -} - -func (s diffStats) IsZero() bool { - s.Name = "" - return s == diffStats{} -} - -func (s diffStats) NumDiff() int { - return s.NumRemoved + s.NumInserted + s.NumModified -} - -func (s diffStats) Append(ds diffStats) diffStats { - assert(s.Name == ds.Name) - s.NumIgnored += ds.NumIgnored - s.NumIdentical += ds.NumIdentical - s.NumRemoved += ds.NumRemoved - s.NumInserted += ds.NumInserted - s.NumModified += ds.NumModified - return s -} - -// String prints a humanly-readable summary of coalesced records. -// -// Example: -// -// diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields" -func (s diffStats) String() string { - var ss []string - var sum int - labels := [...]string{"ignored", "identical", "removed", "inserted", "modified"} - counts := [...]int{s.NumIgnored, s.NumIdentical, s.NumRemoved, s.NumInserted, s.NumModified} - for i, n := range counts { - if n > 0 { - ss = append(ss, fmt.Sprintf("%d %v", n, labels[i])) - } - sum += n - } - - // Pluralize the name (adjusting for some obscure English grammar rules). - name := s.Name - if sum > 1 { - name += "s" - if strings.HasSuffix(name, "ys") { - name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries" - } - } - - // Format the list according to English grammar (with Oxford comma). - switch n := len(ss); n { - case 0: - return "" - case 1, 2: - return strings.Join(ss, " and ") + " " + name - default: - return strings.Join(ss[:n-1], ", ") + ", and " + ss[n-1] + " " + name - } -} - -type commentString string - -func (s commentString) String() string { return string(s) } diff --git a/vendor/github.com/google/go-cmp/cmp/report_value.go b/vendor/github.com/google/go-cmp/cmp/report_value.go deleted file mode 100644 index 668d470fd..000000000 --- a/vendor/github.com/google/go-cmp/cmp/report_value.go +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2019, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmp - -import "reflect" - -// valueNode represents a single node within a report, which is a -// structured representation of the value tree, containing information -// regarding which nodes are equal or not. -type valueNode struct { - parent *valueNode - - Type reflect.Type - ValueX reflect.Value - ValueY reflect.Value - - // NumSame is the number of leaf nodes that are equal. - // All descendants are equal only if NumDiff is 0. - NumSame int - // NumDiff is the number of leaf nodes that are not equal. - NumDiff int - // NumIgnored is the number of leaf nodes that are ignored. - NumIgnored int - // NumCompared is the number of leaf nodes that were compared - // using an Equal method or Comparer function. - NumCompared int - // NumTransformed is the number of non-leaf nodes that were transformed. - NumTransformed int - // NumChildren is the number of transitive descendants of this node. - // This counts from zero; thus, leaf nodes have no descendants. - NumChildren int - // MaxDepth is the maximum depth of the tree. This counts from zero; - // thus, leaf nodes have a depth of zero. - MaxDepth int - - // Records is a list of struct fields, slice elements, or map entries. - Records []reportRecord // If populated, implies Value is not populated - - // Value is the result of a transformation, pointer indirect, of - // type assertion. - Value *valueNode // If populated, implies Records is not populated - - // TransformerName is the name of the transformer. - TransformerName string // If non-empty, implies Value is populated -} -type reportRecord struct { - Key reflect.Value // Invalid for slice element - Value *valueNode -} - -func (parent *valueNode) PushStep(ps PathStep) (child *valueNode) { - vx, vy := ps.Values() - child = &valueNode{parent: parent, Type: ps.Type(), ValueX: vx, ValueY: vy} - switch s := ps.(type) { - case StructField: - assert(parent.Value == nil) - parent.Records = append(parent.Records, reportRecord{Key: reflect.ValueOf(s.Name()), Value: child}) - case SliceIndex: - assert(parent.Value == nil) - parent.Records = append(parent.Records, reportRecord{Value: child}) - case MapIndex: - assert(parent.Value == nil) - parent.Records = append(parent.Records, reportRecord{Key: s.Key(), Value: child}) - case Indirect: - assert(parent.Value == nil && parent.Records == nil) - parent.Value = child - case TypeAssertion: - assert(parent.Value == nil && parent.Records == nil) - parent.Value = child - case Transform: - assert(parent.Value == nil && parent.Records == nil) - parent.Value = child - parent.TransformerName = s.Name() - parent.NumTransformed++ - default: - assert(parent == nil) // Must be the root step - } - return child -} - -func (r *valueNode) Report(rs Result) { - assert(r.MaxDepth == 0) // May only be called on leaf nodes - - if rs.ByIgnore() { - r.NumIgnored++ - } else { - if rs.Equal() { - r.NumSame++ - } else { - r.NumDiff++ - } - } - assert(r.NumSame+r.NumDiff+r.NumIgnored == 1) - - if rs.ByMethod() { - r.NumCompared++ - } - if rs.ByFunc() { - r.NumCompared++ - } - assert(r.NumCompared <= 1) -} - -func (child *valueNode) PopStep() (parent *valueNode) { - if child.parent == nil { - return nil - } - parent = child.parent - parent.NumSame += child.NumSame - parent.NumDiff += child.NumDiff - parent.NumIgnored += child.NumIgnored - parent.NumCompared += child.NumCompared - parent.NumTransformed += child.NumTransformed - parent.NumChildren += child.NumChildren + 1 - if parent.MaxDepth < child.MaxDepth+1 { - parent.MaxDepth = child.MaxDepth + 1 - } - return parent -} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md b/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md index 3879f14aa..f349b3357 100644 --- a/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md +++ b/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md @@ -8,6 +8,10 @@ but only releases after v1.0.3 properly adhere to it. ## [Unreleased] +## [1.4.1] - 2026-08-02 +### Fixed +- Corrected `D50ToD65` to use the CSS Color 4 matrix inverse of `D65ToD50` (#85). + ## [1.4.0] - 2026-03-28 ### Added - Constructors, decomposers, and blend functions for the CSS Color Level 4 wide-gamut RGB color spaces `DisplayP3`, `A98Rgb`, `ProPhotoRgb`, and `Rec2020` (#81) diff --git a/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go b/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go index 6805a2b96..63c3e878e 100644 --- a/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go +++ b/vendor/github.com/lucasb-eyer/go-colorful/widegamut.go @@ -10,9 +10,9 @@ import "math" // Bradford chromatic adaptation between D50 and D65 illuminants. func D50ToD65(x, y, z float64) (xo, yo, zo float64) { - xo = 0.9555766*x - 0.0230393*y + 0.0631636*z - yo = -0.0282895*x + 1.0099416*y + 0.0210077*z - zo = 0.0122982*x - 0.0204830*y + 1.3299098*z + xo = 0.9554734527042182*x - 0.023098536874261423*y + 0.06325964552894382*z + yo = -0.028369706963208136*x + 1.0099954580058226*y + 0.021041398966943008*z + zo = 0.012314001688319899*x - 0.020507696433477912*y + 1.3303659366080753*z return } diff --git a/vendor/github.com/sirupsen/logrus/.golangci.yml b/vendor/github.com/sirupsen/logrus/.golangci.yml index 792db3618..c9a840e0d 100644 --- a/vendor/github.com/sirupsen/logrus/.golangci.yml +++ b/vendor/github.com/sirupsen/logrus/.golangci.yml @@ -1,12 +1,9 @@ version: "2" -run: - tests: false linters: enable: - asasalint - asciicheck - bidichk - - bodyclose - contextcheck - durationcheck - errchkjson @@ -22,46 +19,21 @@ linters: - nilerr - nilnesserr - noctx - - protogetter - reassign - recvcheck - - rowserrcheck - - spancheck - - sqlclosecheck - testifylint - unparam - - zerologlint - disable: - - prealloc - settings: - errcheck: - check-type-assertions: false - check-blank: false - lll: - line-length: 100 - tab-width: 4 - prealloc: - simple: false - range-loops: false - for-loops: false - whitespace: - multi-if: false - multi-func: false exclusions: - generated: lax presets: - - comments - - common-false-positives - legacy - std-error-handling - paths: - - third_party$ - - builtin$ - - examples$ -formatters: - exclusions: - generated: lax - paths: - - third_party$ - - builtin$ - - examples$ + rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - gosec + - musttag + - noctx # TODO: enable once we switch to Go 1.24+. + - linters: # TODO: remove once golangci-lint is updated with https://github.com/golangci/golangci-lint/pull/6584 + - gocheckcompilerdirectives + text: 'compiler directive unrecognized: //go:fix' diff --git a/vendor/github.com/sirupsen/logrus/.travis.yml b/vendor/github.com/sirupsen/logrus/.travis.yml deleted file mode 100644 index c1dbd5a3a..000000000 --- a/vendor/github.com/sirupsen/logrus/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: go -go_import_path: github.com/sirupsen/logrus -git: - depth: 1 -env: - - GO111MODULE=on -go: 1.15.x -os: linux -install: - - ./travis/install.sh -script: - - cd ci - - go run mage.go -v -w ../ crossBuild - - go run mage.go -v -w ../ lint - - go run mage.go -v -w ../ test diff --git a/vendor/github.com/sirupsen/logrus/CHANGELOG.md b/vendor/github.com/sirupsen/logrus/CHANGELOG.md index 098608ff4..683cec908 100644 --- a/vendor/github.com/sirupsen/logrus/CHANGELOG.md +++ b/vendor/github.com/sirupsen/logrus/CHANGELOG.md @@ -1,90 +1,253 @@ -# 1.8.1 +# Changelog + +All notable changes to this project will be documented in this file. + +## 1.10.2 + +Changed: + + * Update `github.com/stretchr/testify` to v1.12.1, removing the legacy + `gopkg.in/yaml.v3` dependency. + +## 1.10.1 + +Fixes: + + * Fix a regression introduced in v1.10.0 where `TextFormatter` could panic + when formatting nil or panicking `error` and `fmt.Stringer` values. + * Allow function-backed implementations of `error` as field values. + +## 1.10.0 + +Fixes: + + * Fix reentrant logging deadlocks in formatter paths. + * Fix race conditions in formatter and entry handling. + * Fix generic `Log`, `Logf`, `Logln`, and `LogFn` methods unexpectedly + panicking when called with `PanicLevel`. Use the corresponding `Panic` + methods when panic behavior is desired. + * Improve concurrency safety around formatter and hook access. + +Features: + + * Add `slog` hook for forwarding Logrus entries to `log/slog`. + * Add `slog.Handler` for forwarding `log/slog` records to a Logrus logger, + including levels, fields, groups, context, time, and optional caller + reporting. The hook and handler can also be combined to help migrate + between Logrus and `log/slog`. + * Add minimal, composable logging interfaces for each log level. This enables + consumers to depend on narrower interfaces, making it easier to substitute + or adapt logging implementations. + * Allow `Entry.Caller` to be set explicitly and preserve it across derived + entries, enabling custom caller detection without Logrus overwriting + caller information when `ReportCaller` is enabled. + +Changed: + + * Raise minimum supported Go version to 1.23. + * TextFormatter now renders `[]byte` values as raw/quoted strings instead of slice-of-ints. + * TextFormatter now uses distinct dimmed colors for debug and trace output. + * TextFormatter now automatically enables colors on Windows terminals with ANSI support, + matching the behavior on other platforms. + * `Entry.HasCaller` is now deprecated in favor of checking `Entry.Caller` directly. + * Deprecated `MutexWrap`, which was unintentionally exposed as public API. + It remains available as an alias for compatibility but should not be used + directly. + +Performance: + + * Significantly improve TextFormatter performance and reduce allocations. + * Optimize common Entry and Logger hot paths. + * Reduce allocations in caller reporting. + * ~17% lower geomean runtime and ~27% higher formatter throughput overall. + * Common enabled logging paths are ~30–44% faster. + * TextFormatter paths are up to ~40% faster, with allocation counts reduced + by 25–74% across the measured formatter cases. + + +## 1.9.4 + +Fixes: + + * Remove uses of deprecated `ioutil` package + +Features: + + * Add GNU/Hurd support + * Add WASI wasip1 support + Code quality: + + * Update minimum supported Go version to 1.17 + * Documentation updates + + +## 1.9.3 + +Fixes: + + * Re-apply fix for potential denial of service in logrus.Writer() when logging >64KB single-line payloads without newlines (#1376) + * Fix panic in Writer + + +## 1.9.2 + +Fixes: + + * Revert Writer DoS fix (#1376) due to regression + + +## 1.9.1 + +Fixes: + + * Fix potential denial of service in logrus.Writer() when logging >64KB single-line payloads without newlines (#1376) + + +## 1.9.0 + +Fixes: + + * Multiple concurrency and race condition fixes + * Improve Windows terminal and ANSI handling + +Code quality: + + * Internal cleanups and modernization + + +## 1.8.3 + +Fixes: + + * Fix potential denial of service in logrus.Writer() when logging >64KB single-line payloads without newlines (#1376) + + +## 1.8.2 + +Features: + + * Add support for the logger private buffer pool (#1253) + +Fixes: + + * Fix race condition for SetFormatter and SetReportCaller + * Fix data race in hooks test package + +## 1.8.1 + +Code quality: + * move magefile in its own subdir/submodule to remove magefile dependency on logrus consumer * improve timestamp format documentation Fixes: + * fix race condition on logger hooks -# 1.8.0 +## 1.8.0 Correct versioning number replacing v1.7.1. -# 1.7.1 +## 1.7.1 Beware this release has introduced a new public API and its semver is therefore incorrect. Code quality: + * use go 1.15 in travis * use magefile as task runner Fixes: + * small fixes about new go 1.13 error formatting system * Fix for long time race condiction with mutating data hooks Features: + * build support for zos -# 1.7.0 +## 1.7.0 + Fixes: + * the dependency toward a windows terminal library has been removed Features: + * a new buffer pool management API has been added * a set of `Fn()` functions have been added -# 1.6.0 +## 1.6.0 + Fixes: + * end of line cleanup * revert the entry concurrency bug fix which leads to deadlock under some circumstances * update dependency on go-windows-terminal-sequences to fix a crash with go 1.14 Features: + * add an option to the `TextFormatter` to completely disable fields quoting -# 1.5.0 +## 1.5.0 + Code quality: + * add golangci linter run on travis Fixes: + * add mutex for hooks concurrent access on `Entry` data * caller function field for go1.14 * fix build issue for gopherjs target Feature: + * add an hooks/writer sub-package whose goal is to split output on different stream depending on the trace level * add a `DisableHTMLEscape` option in the `JSONFormatter` * add `ForceQuote` and `PadLevelText` options in the `TextFormatter` -# 1.4.2 +## 1.4.2 + * Fixes build break for plan9, nacl, solaris -# 1.4.1 + +## 1.4.1 + This new release introduces: + * Enhance TextFormatter to not print caller information when they are empty (#944) * Remove dependency on golang.org/x/crypto (#932, #943) Fixes: + * Fix Entry.WithContext method to return a copy of the initial entry (#941) -# 1.4.0 +## 1.4.0 + This new release introduces: + * Add `DeferExitHandler`, similar to `RegisterExitHandler` but prepending the handler to the list of handlers (semantically like `defer`) (#848). * Add `CallerPrettyfier` to `JSONFormatter` and `TextFormatter` (#909, #911) * Add `Entry.WithContext()` and `Entry.Context`, to set a context on entries to be used e.g. in hooks (#919). Fixes: + * Fix wrong method calls `Logger.Print` and `Logger.Warningln` (#893). * Update `Entry.Logf` to not do string formatting unless the log level is enabled (#903) * Fix infinite recursion on unknown `Level.String()` (#907) * Fix race condition in `getCaller` (#916). -# 1.3.0 +## 1.3.0 + This new release introduces: + * Log, Logf, Logln functions for Logger and Entry that take a Level Fixes: + * Building prometheus node_exporter on AIX (#840) * Race condition in TextFormatter (#468) * Travis CI import path (#868) @@ -92,20 +255,26 @@ Fixes: * Pointer to func as field in JSONFormatter (#870) * Properly marshal Levels (#873) -# 1.2.0 +## 1.2.0 + This new release introduces: + * A new method `SetReportCaller` in the `Logger` to enable the file, line and calling function from which the trace has been issued * A new trace level named `Trace` whose level is below `Debug` * A configurable exit function to be called upon a Fatal trace * The `Level` object now implements `encoding.TextUnmarshaler` interface -# 1.1.1 +## 1.1.1 + This is a bug fix release. + * fix the build break on Solaris * don't drop a whole trace in JSONFormatter when a field param is a function pointer which can not be serialized -# 1.1.0 +## 1.1.0 + This new release introduces: + * several fixes: * a fix for a race condition on entry formatting * proper cleanup of previously used entries before putting them back in the pool @@ -122,9 +291,10 @@ This new release introduces: * the field sort function is now configurable for text formatter * the CLICOLOR and CLICOLOR\_FORCE environment variable support in text formater -# 1.0.6 +## 1.0.6 This new release introduces: + * a new api WithTime which allows to easily force the time of the log entry which is mostly useful for logger wrapper * a fix reverting the immutability of the entry given as parameter to the hooks @@ -134,71 +304,71 @@ This new release introduces: * a new configuration of the textformatter to configure the name of the default keys * a new configuration of the text formatter to disable the level truncation -# 1.0.5 +## 1.0.5 * Fix hooks race (#707) * Fix panic deadlock (#695) -# 1.0.4 +## 1.0.4 * Fix race when adding hooks (#612) * Fix terminal check in AppEngine (#635) -# 1.0.3 +## 1.0.3 * Replace example files with testable examples -# 1.0.2 +## 1.0.2 * bug: quote non-string values in text formatter (#583) * Make (*Logger) SetLevel a public method -# 1.0.1 +## 1.0.1 * bug: fix escaping in text formatter (#575) -# 1.0.0 +## 1.0.0 * Officially changed name to lower-case * bug: colors on Windows 10 (#541) * bug: fix race in accessing level (#512) -# 0.11.5 +## 0.11.5 * feature: add writer and writerlevel to entry (#372) -# 0.11.4 +## 0.11.4 * bug: fix undefined variable on solaris (#493) -# 0.11.3 +## 0.11.3 * formatter: configure quoting of empty values (#484) * formatter: configure quoting character (default is `"`) (#484) * bug: fix not importing io correctly in non-linux environments (#481) -# 0.11.2 +## 0.11.2 * bug: fix windows terminal detection (#476) -# 0.11.1 +## 0.11.1 * bug: fix tty detection with custom out (#471) -# 0.11.0 +## 0.11.0 * performance: Use bufferpool to allocate (#370) * terminal: terminal detection for app-engine (#343) * feature: exit handler (#375) -# 0.10.0 +## 0.10.0 * feature: Add a test hook (#180) * feature: `ParseLevel` is now case-insensitive (#326) * feature: `FieldLogger` interface that generalizes `Logger` and `Entry` (#308) * performance: avoid re-allocations on `WithFields` (#335) -# 0.9.0 +## 0.9.0 * logrus/text_formatter: don't emit empty msg * logrus/hooks/airbrake: move out of main repository @@ -210,25 +380,25 @@ This new release introduces: * logrus/core: support `WithError` on logger * logrus/core: Solaris support -# 0.8.7 +## 0.8.7 * logrus/core: fix possible race (#216) * logrus/doc: small typo fixes and doc improvements -# 0.8.6 +## 0.8.6 * hooks/raven: allow passing an initialized client -# 0.8.5 +## 0.8.5 * logrus/core: revert #208 -# 0.8.4 +## 0.8.4 * formatter/text: fix data race (#218) -# 0.8.3 +## 0.8.3 * logrus/core: fix entry log level (#208) * logrus/core: improve performance of text formatter by 40% @@ -236,24 +406,24 @@ This new release introduces: * logrus/core: add support for DragonflyBSD and NetBSD * formatter/text: print structs more verbosely -# 0.8.2 +## 0.8.2 * logrus: fix more Fatal family functions -# 0.8.1 +## 0.8.1 * logrus: fix not exiting on `Fatalf` and `Fatalln` -# 0.8.0 +## 0.8.0 * logrus: defaults to stderr instead of stdout * hooks/sentry: add special field for `*http.Request` * formatter/text: ignore Windows for colors -# 0.7.3 +## 0.7.3 * formatter/\*: allow configuration of timestamp layout -# 0.7.2 +## 0.7.2 * formatter/text: Add configuration option for time format (#158) diff --git a/vendor/github.com/sirupsen/logrus/README.md b/vendor/github.com/sirupsen/logrus/README.md index cc5dab7eb..b2ff7affc 100644 --- a/vendor/github.com/sirupsen/logrus/README.md +++ b/vendor/github.com/sirupsen/logrus/README.md @@ -3,13 +3,10 @@ Logrus is a structured logger for Go (golang), completely API compatible with the standard library logger. -**Logrus is in maintenance-mode.** We will not be introducing new features. It's -simply too hard to do in a way that won't break many people's projects, which is -the last thing you want from your Logging library (again...). - -This does not mean Logrus is dead. Logrus will continue to be maintained for -security, (backwards compatible) bug fixes, and performance (where we are -limited by the interface). +**Logrus is in maintenance mode.** The project focuses on security, bug fixes, +and performance improvements. New features are not planned, aside from changes +required to provide interoperability with other logging ecosystems (e.g., Go's +[log/slog](https://pkg.go.dev/log/slog)). I believe Logrus' biggest contribution is to have played a part in today's widespread use of structured logging in Golang. There doesn't seem to be a @@ -23,18 +20,6 @@ about structured logging in Go today. Check out, for example, [zap]: https://github.com/uber-go/zap [apex]: https://github.com/apex/log -**Seeing weird case-sensitive problems?** It's in the past been possible to -import Logrus as both upper- and lower-case. Due to the Go package environment, -this caused issues in the community and we needed a standard. Some environments -experienced problems with the upper-case variant, so the lower-case was decided. -Everything using `logrus` will need to use the lower-case: -`github.com/sirupsen/logrus`. Any package that isn't, should be changed. - -To fix Glide, see [these -comments](https://github.com/sirupsen/logrus/issues/553#issuecomment-306591437). -For an in-depth explanation of the casing issue, see [this -comment](https://github.com/sirupsen/logrus/issues/570#issuecomment-313933276). - Nicely color-coded in development (when a TTY is attached, otherwise just plain text): @@ -43,35 +28,27 @@ plain text): With `logrus.SetFormatter(&logrus.JSONFormatter{})`, for easy parsing by logstash or Splunk: -```text -{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the -ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"} - -{"level":"warning","msg":"The group's number increased tremendously!", -"number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"} - -{"animal":"walrus","level":"info","msg":"A giant walrus appears!", -"size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"} - -{"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.", -"size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"} - -{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true, -"time":"2014-03-10 19:57:38.562543128 -0400 EDT"} +```json lines +{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"} +{"level":"warning","msg":"The group's number increased tremendously!","number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"} +{"animal":"walrus","level":"info","msg":"A giant walrus appears!","size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"} +{"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.","size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"} +{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,"time":"2014-03-10 19:57:38.562543128 -0400 EDT"} ``` With the default `logrus.SetFormatter(&logrus.TextFormatter{})` when a TTY is not attached, the output is compatible with the [logfmt](https://pkg.go.dev/github.com/kr/logfmt) format: -```text +```bash time="2015-03-26T01:27:38-04:00" level=debug msg="Started observing beach" animal=walrus number=8 time="2015-03-26T01:27:38-04:00" level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10 time="2015-03-26T01:27:38-04:00" level=warning msg="The group's number increased tremendously!" number=122 omg=true time="2015-03-26T01:27:38-04:00" level=debug msg="Temperature changes" temperature=-4 time="2015-03-26T01:27:38-04:00" level=panic msg="It's over 9000!" animal=orca size=9009 -time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic It's over 9000!} number=100 omg=true +time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" animal=orca err="It's over 9000!" number=100 omg=true size=9009 ``` + To ensure this behaviour even if a TTY is attached, set your formatter as follows: ```go @@ -88,29 +65,32 @@ If you wish to add the calling method as a field, instruct the logger via: ```go logrus.SetReportCaller(true) ``` + This adds the caller as 'method' like so: ```json -{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by", -"time":"2014-03-10 19:57:38.562543129 -0400 EDT"} +{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by","time":"2014-03-10 19:57:38.562543129 -0400 EDT"} ``` -```text +```bash time="2015-03-26T01:27:38-04:00" level=fatal method=github.com/sirupsen/arcticcreatures.migrate msg="a penguin swims by" animal=penguin ``` + Note that this does add measurable overhead - the cost will depend on the version of Go, but is between 20 and 40% in recent tests with 1.6 and 1.7. You can validate this in your environment via benchmarks: ```bash -go test -bench=.*CallerTracing +go test -bench=ReportCaller ``` #### Case-sensitivity -The organization's name was changed to lower-case--and this will not be changed -back. If you are getting import conflicts due to case sensitivity, please use -the lower-case import: `github.com/sirupsen/logrus`. +The organization's name was [changed to lower-case][1]. If you are getting import +conflicts due to case sensitivity, please use the lower-case import: +`github.com/sirupsen/logrus`. + +[1]: https://github.com/sirupsen/logrus/issues/570#issuecomment-313933276 #### Example @@ -289,6 +269,7 @@ func init() { } } ``` + Note: Syslog hooks also support connecting to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). For the detail, please check the [syslog hook README](hooks/syslog/README.md). A list of currently known service hooks can be found in this wiki [page](https://github.com/sirupsen/logrus/wiki/Hooks) @@ -367,18 +348,21 @@ Splunk or Logstash. The built-in logging formatters are: -* `logrus.TextFormatter`. Logs the event in colors if stdout is a tty, otherwise - without colors. - * *Note:* to force colored output when there is no TTY, set the `ForceColors` +* [`logrus.TextFormatter`](https://pkg.go.dev/github.com/sirupsen/logrus#TextFormatter) + logs the event in colors if the logger output is a TTY, otherwise without colors. + * To force colored output when there is no TTY, set the `ForceColors` field to `true`. To force no colored output even if there is a TTY set the - `DisableColors` field to `true`. For Windows, see - [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable). + `DisableColors` field to `true`. + * On modern Windows terminals with ANSI (Virtual Terminal) support, TextFormatter + automatically enables colored output. + * If your environment does not support ANSI escape sequences, wrap the logger output + using [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable) + and set `ForceColors` (or `CLICOLOR_FORCE=1`) to enable colors through the wrapper. * When colors are enabled, levels are truncated to 4 characters by default. To disable truncation set the `DisableLevelTruncation` field to `true`. * When outputting to a TTY, it's often helpful to visually scan down a column where all the levels are the same width. Setting the `PadLevelText` field to `true` enables this behavior, by adding padding to the level text. - * All options are listed in the [generated docs](https://pkg.go.dev/github.com/sirupsen/logrus#TextFormatter). -* `logrus.JSONFormatter`. Logs fields as JSON. - * All options are listed in the [generated docs](https://pkg.go.dev/github.com/sirupsen/logrus#JSONFormatter). +* [`logrus.JSONFormatter`](https://pkg.go.dev/github.com/sirupsen/logrus#JSONFormatter) + logs fields as JSON. Third-party logging formatters: @@ -390,10 +374,12 @@ Third-party logging formatters: * [`nested-logrus-formatter`](https://github.com/antonfisher/nested-logrus-formatter). Converts logrus fields to a nested structure. * [`powerful-logrus-formatter`](https://github.com/zput/zxcTool). get fileName, log's line number and the latest function's name when print log; Save log to files. * [`caption-json-formatter`](https://github.com/nolleh/caption_json_formatter). logrus's message json formatter with human-readable caption added. +* [`easy-logrus-formatter`](https://github.com/WeiZhixiong/easy-logrus-formatter). Provide a user-friendly formatter for logrus. +* [`redactrus`](https://github.com/ibreakthecloud/redactrus). Redacts sensitive information like password, apikeys, email, etc. from logs. You can define your formatter by implementing the `Formatter` interface, requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a -`Fields` type (`map[string]interface{}`) with all your fields as well as the +`Fields` type (`map[string]any`) with all your fields as well as the default ones (see Entries section above): ```go @@ -516,4 +502,4 @@ Situations when locking is not needed include: 2) logger.Out is an os.File handler opened with `O_APPEND` flag, and every write is smaller than 4k. (This allows multi-thread/multi-process writing) - (Refer to http://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/) + (Refer to ) diff --git a/vendor/github.com/sirupsen/logrus/alt_exit.go b/vendor/github.com/sirupsen/logrus/alt_exit.go index 8fd189e1c..1c35cf81c 100644 --- a/vendor/github.com/sirupsen/logrus/alt_exit.go +++ b/vendor/github.com/sirupsen/logrus/alt_exit.go @@ -57,7 +57,7 @@ func Exit(code int) { // // This method is useful when a caller wishes to use logrus to log a fatal // message but also needs to gracefully shutdown. An example usecase could be -// closing database connections, or sending a alert that the application is +// closing database connections, or sending an alert that the application is // closing. func RegisterExitHandler(handler func()) { handlers = append(handlers, handler) @@ -69,7 +69,7 @@ func RegisterExitHandler(handler func()) { // // This method is useful when a caller wishes to use logrus to log a fatal // message but also needs to gracefully shutdown. An example usecase could be -// closing database connections, or sending a alert that the application is +// closing database connections, or sending an alert that the application is // closing. func DeferExitHandler(handler func()) { handlers = append([]func(){handler}, handlers...) diff --git a/vendor/github.com/sirupsen/logrus/buffer_pool.go b/vendor/github.com/sirupsen/logrus/buffer_pool.go index c7787f77c..6b562d870 100644 --- a/vendor/github.com/sirupsen/logrus/buffer_pool.go +++ b/vendor/github.com/sirupsen/logrus/buffer_pool.go @@ -5,9 +5,13 @@ import ( "sync" ) -var ( - bufferPool BufferPool -) +var bufferPool BufferPool = &defaultPool{ + pool: &sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, + }, +} type BufferPool interface { Put(*bytes.Buffer) @@ -27,17 +31,7 @@ func (p *defaultPool) Get() *bytes.Buffer { } // SetBufferPool allows to replace the default logrus buffer pool -// to better meets the specific needs of an application. +// to better meet the specific needs of an application. func SetBufferPool(bp BufferPool) { bufferPool = bp } - -func init() { - SetBufferPool(&defaultPool{ - pool: &sync.Pool{ - New: func() interface{} { - return new(bytes.Buffer) - }, - }, - }) -} diff --git a/vendor/github.com/sirupsen/logrus/doc.go b/vendor/github.com/sirupsen/logrus/doc.go index da67aba06..75186dc2d 100644 --- a/vendor/github.com/sirupsen/logrus/doc.go +++ b/vendor/github.com/sirupsen/logrus/doc.go @@ -1,25 +1,25 @@ /* Package logrus is a structured logger for Go, completely API compatible with the standard library logger. - The simplest way to use Logrus is simply the package-level exported logger: - package main + package main - import ( - log "github.com/sirupsen/logrus" - ) + import ( + log "github.com/sirupsen/logrus" + ) - func main() { - log.WithFields(log.Fields{ - "animal": "walrus", - "number": 1, - "size": 10, - }).Info("A walrus appears") - } + func main() { + log.WithFields(log.Fields{ + "animal": "walrus", + "number": 1, + "size": 10, + }).Info("A walrus appears") + } Output: - time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10 + + time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10 For a full guide visit https://github.com/sirupsen/logrus */ diff --git a/vendor/github.com/sirupsen/logrus/entry.go b/vendor/github.com/sirupsen/logrus/entry.go index 71d796d0b..82de41f9f 100644 --- a/vendor/github.com/sirupsen/logrus/entry.go +++ b/vendor/github.com/sirupsen/logrus/entry.go @@ -4,9 +4,11 @@ import ( "bytes" "context" "fmt" + "maps" "os" "reflect" "runtime" + "strconv" "strings" "sync" "time" @@ -17,8 +19,10 @@ var ( // qualified package name, cached at first use logrusPackage string - // Positions in the call stack when tracing to report the calling method - minimumCallerDepth int + // Positions in the call stack when tracing to report the calling method. + // + // Start at the bottom of the stack before the package-name cache is primed. + minimumCallerDepth = 1 // Used for caller information initialisation callerInitOnce sync.Once @@ -29,68 +33,116 @@ const ( knownLogrusFrames int = 4 ) -func init() { - // start at the bottom of the stack before the package-name cache is primed - minimumCallerDepth = 1 -} - // ErrorKey defines the key when adding errors using [WithError], [Logger.WithError]. var ErrorKey = "error" -// Entry is the final or intermediate Logrus logging entry. It contains all -// the fields passed with WithField{,s}. It's finally logged when Trace, Debug, -// Info, Warn, Error, Fatal or Panic is called on it. These objects can be -// reused and passed around as much as you wish to avoid field duplication. +// Entry represents a single log event. It may be either an intermediate +// entry (created via WithField(s), WithContext, etc.) or a final entry +// that is emitted when one of the level methods (Trace, Debug, Info, +// Warn, Error, Fatal, Panic) is called. // -//nolint:recvcheck // the methods of "Entry" use pointer receiver and non-pointer receiver. +// An Entry always belongs to a Logger. A nil Logger is invalid and will +// cause a panic when the entry is logged. Use [NewEntry] or Logger methods +// to construct entries. +// +// Entries are safe to reuse for adding fields and may be passed around +// to avoid field duplication. Each log operation operates on a copy +// of the Entry’s data to avoid mutation during formatting. +// +//nolint:recvcheck // Entry methods intentionally use both pointer and value receivers. type Entry struct { + // Logger is the Logger that owns this entry and is responsible for + // formatting, hooks, and output. It must not be nil. An Entry without + // a Logger is invalid and will panic when logged. Logger *Logger - // Contains all the fields set by the user. + // Data contains all user-defined fields attached to this entry. Data Fields - // Time at which the log entry was created + // Time is the timestamp for the log event. If zero when the entry is + // logged, it defaults to the current time. Time time.Time - // Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic - // This field will be set on entry firing and the value will be equal to the one in Logger struct field. + // Level is the severity of the log entry. It is set when the entry + // is fired and reflects the level used for that log call. Level Level - // Calling method, with package name + // Caller contains the calling method information. + // + // When [Logger.ReportCaller] is enabled, Caller is populated automatically at + // log time if it is nil. Hooks and formatters may inspect Caller. + // + // Applications generally should not modify Caller unless they intentionally + // want to provide custom caller information. Caller *runtime.Frame - // Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic + // Message is the log message supplied to one of the logging methods + // (Trace, Debug, Info, Warn, Error, Fatal, or Panic). It is set when + // the entry is logged. Message string - // When formatter is called in entry.log(), a Buffer may be set to entry + // Buffer is a reusable buffer provided to the formatter. It is set + // before formatting in the normal log path; when nil, formatters + // allocate their own. Buffer *bytes.Buffer - // Contains the context set by the user. Useful for hook processing etc. + // Context carries user-provided context for hooks and formatters. Context context.Context - // err may contain a field formatting error + // err contains internal field-formatting errors. err string } +// NewEntry creates a new [Entry] associated with the provided Logger. +// The logger must not be nil. Passing a nil logger results in a +// panic when a logging method (e.g., [Entry.Info], [Entry.Error], etc.) +// is called. func NewEntry(logger *Logger) *Entry { return &Entry{ Logger: logger, - // Default is three fields, plus one optional. Give a little extra room. - Data: make(Fields, 6), + // Reserve default predefined fields and a little extra room. + Data: make(Fields, defaultFields+3), } } +// Dup creates a copy of the entry for further modification. +// +// Data is cloned to avoid mutating the original entry. Other fields +// (Logger, Time, Context, etc.) are copied by value. func (entry *Entry) Dup() *Entry { - data := make(Fields, len(entry.Data)) - for k, v := range entry.Data { - data[k] = v + dup := entry.dup() + dup.Data = maps.Clone(entry.Data) + return dup +} + +// dup copies the entry fields shared by derived entries except Data, which +// callers must copy or initialize as appropriate for their use. +func (entry *Entry) dup() *Entry { + return &Entry{ + Logger: entry.Logger, + Time: entry.Time, + Caller: entry.Caller, + Context: entry.Context, + err: entry.err, } - return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, Context: entry.Context, err: entry.err} } // Bytes returns the bytes representation of this entry from the formatter. func (entry *Entry) Bytes() ([]byte, error) { - return entry.Logger.Formatter.Format(entry) + // Snapshot the formatter under the lock to protect against concurrent + // SetFormatter calls, then release the lock before formatting. + // This avoids a data race and prevents a deadlock if Format() triggers + // reentrant logging (e.g., a field's MarshalJSON calls logrus). + // + // See: + // + // - https://github.com/sirupsen/logrus/issues/1440 + // - https://github.com/sirupsen/logrus/issues/1448 + entry.Logger.mu.Lock() + formatter := entry.Logger.Formatter + entry.Logger.mu.Unlock() + + return formatter.Format(entry) } // String returns the string representation from the reader and ultimately the @@ -112,58 +164,61 @@ func (entry *Entry) WithError(err error) *Entry { // WithContext adds a context to the Entry. func (entry *Entry) WithContext(ctx context.Context) *Entry { - dataCopy := make(Fields, len(entry.Data)) - for k, v := range entry.Data { - dataCopy[k] = v - } - return &Entry{Logger: entry.Logger, Data: dataCopy, Time: entry.Time, err: entry.err, Context: ctx} + dup := entry.dup() + dup.Data = maps.Clone(entry.Data) + dup.Context = ctx + return dup } // WithField adds a single field to the Entry. -func (entry *Entry) WithField(key string, value interface{}) *Entry { - return entry.WithFields(Fields{key: value}) +func (entry *Entry) WithField(key string, value any) *Entry { + dup := entry.dup() + dup.Data = maps.Clone(entry.Data) + dup.addField(key, value) + return dup } // WithFields adds a map of fields to the Entry. func (entry *Entry) WithFields(fields Fields) *Entry { - data := make(Fields, len(entry.Data)+len(fields)) - for k, v := range entry.Data { - data[k] = v + dup := entry.dup() + dup.Data = make(Fields, len(entry.Data)+len(fields)) + maps.Copy(dup.Data, entry.Data) + + for key, value := range fields { + dup.addField(key, value) } - fieldErr := entry.err - for k, v := range fields { - isErrField := false - if t := reflect.TypeOf(v); t != nil { - switch { - case t.Kind() == reflect.Func, t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Func: - isErrField = true - } - } - if isErrField { - tmp := fmt.Sprintf("can not add field %q", k) - if fieldErr != "" { - fieldErr = entry.err + ", " + tmp - } else { - fieldErr = tmp - } - } else { - data[k] = v - } - } - return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: fieldErr, Context: entry.Context} + return dup } // WithTime overrides the time of the Entry. func (entry *Entry) WithTime(t time.Time) *Entry { - dataCopy := make(Fields, len(entry.Data)) - for k, v := range entry.Data { - dataCopy[k] = v + dup := entry.dup() + dup.Data = maps.Clone(entry.Data) + dup.Time = t + return dup +} + +func (entry *Entry) addField(key string, value any) { + if _, ok := value.(error); !ok { + t := reflect.TypeOf(value) + if t != nil && (t.Kind() == reflect.Func || t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Func) { + if entry.err != "" { + entry.err += ", skipping unsupported field " + strconv.Quote(key) + } else { + entry.err = "skipping unsupported field " + strconv.Quote(key) + } + return + } } - return &Entry{Logger: entry.Logger, Data: dataCopy, Time: t, err: entry.err, Context: entry.Context} + + if entry.Data == nil { + entry.Data = make(Fields, 1) + } + entry.Data[key] = value } // getPackageName reduces a fully qualified function name to the package name -// There really ought to be to be a better way... +// There really ought to be a better way... func getPackageName(f string) string { for { lastPeriod := strings.LastIndex(f, ".") @@ -186,7 +241,7 @@ func getCaller() *runtime.Frame { _ = runtime.Callers(0, pcs) // dynamic get the package name and the minimum caller depth - for i := 0; i < maximumCallerDepth; i++ { + for i := range maximumCallerDepth { funcName := runtime.FuncForPC(pcs[i]).Name() if strings.Contains(funcName, "getCaller") { logrusPackage = getPackageName(funcName) @@ -215,16 +270,47 @@ func getCaller() *runtime.Frame { return nil } -func (entry Entry) HasCaller() (has bool) { - return entry.Logger != nil && - entry.Logger.ReportCaller && - entry.Caller != nil +// HasCaller reports whether this Entry contains caller information. +// +// Caller may be set explicitly, or populated at log time when +// [Logger.ReportCaller] is enabled. +// +// Deprecated: use [Entry.Caller] != nil instead. +// +//go:fix inline +func (entry Entry) HasCaller() bool { + return entry.Caller != nil } -func (entry *Entry) log(level Level, msg string) { - var buffer *bytes.Buffer +func (entry *Entry) logArgs(level Level, panicAfter bool, args ...any) { + entry.log(level, panicAfter, sprint(args...)) +} - newEntry := entry.Dup() +func (entry *Entry) logf(level Level, panicAfter bool, format string, args ...any) { + entry.log(level, panicAfter, fmt.Sprintf(format, args...)) +} + +// logln uses Sprintln for multiple arguments to preserve Println-style +// spacing between args, then trims the trailing newline. +func (entry *Entry) logln(level Level, panicAfter bool, args ...any) { + if len(args) <= 1 { + entry.log(level, panicAfter, sprint(args...)) + return + } + msg := fmt.Sprintln(args...) + msg = msg[:len(msg)-1] // Trim the newline added by Sprintln; logging adds its own. + entry.log(level, panicAfter, msg) +} + +// log writes msg at level. If panicAfter is true, it panics with the fully +// populated entry after hooks and output have completed. +// +// The explicit flag keeps panic behavior limited to Panic, Panicf, and +// Panicln while avoiding a return value used only as the panic value. +// See #1283 and commits f96066e and 5f8c666. +func (entry *Entry) log(level Level, panicAfter bool, msg string) { + newEntry := entry.dup() + newEntry.Data = maps.Clone(entry.Data) if newEntry.Time.IsZero() { newEntry.Time = time.Now() @@ -233,17 +319,24 @@ func (entry *Entry) log(level Level, msg string) { newEntry.Level = level newEntry.Message = msg - newEntry.Logger.mu.Lock() - reportCaller := newEntry.Logger.ReportCaller + logger := newEntry.Logger + logger.mu.Lock() + reportCaller := logger.ReportCaller bufPool := newEntry.getBufferPool() - newEntry.Logger.mu.Unlock() + logger.mu.Unlock() - if reportCaller { + // Preserve explicitly set caller information. + if reportCaller && newEntry.Caller == nil { newEntry.Caller = getCaller() } - newEntry.fireHooks() - buffer = bufPool.Get() + // Select hooks based on the level for this log call. Hooks receive the + // Entry and may mutate it, but that does not affect which hooks are + // fired for this event. + hooks := logger.hooksForLevel(level) + newEntry.fireHooks(hooks) + + buffer := bufPool.Get() defer func() { newEntry.Buffer = nil buffer.Reset() @@ -251,15 +344,12 @@ func (entry *Entry) log(level Level, msg string) { }() buffer.Reset() newEntry.Buffer = buffer - newEntry.write() - newEntry.Buffer = nil - // To avoid Entry#log() returning a value that only would make sense for - // panic() to use in Entry#Panic(), we avoid the allocation by checking - // directly here. - if level <= PanicLevel { + // Panic here so the panic value contains the fully populated entry without + // requiring log to return it to the caller. + if panicAfter { panic(newEntry) } } @@ -271,175 +361,207 @@ func (entry *Entry) getBufferPool() (pool BufferPool) { return bufferPool } -func (entry *Entry) fireHooks() { - var tmpHooks LevelHooks - entry.Logger.mu.Lock() - tmpHooks = make(LevelHooks, len(entry.Logger.Hooks)) - for k, v := range entry.Logger.Hooks { - tmpHooks[k] = v - } - entry.Logger.mu.Unlock() - - err := tmpHooks.Fire(entry.Level, entry) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err) +func (entry *Entry) fireHooks(hooks []Hook) { + for _, hook := range hooks { + if err := hook.Fire(entry); err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to fire hook:", err) + return + } } } func (entry *Entry) write() { + // Snapshot the formatter under the lock to protect against concurrent + // SetFormatter calls, then release the lock before formatting. + // This avoids a deadlock when Format() triggers reentrant logging (e.g., + // a field's MarshalJSON calls logrus). See #1448, #1440. entry.Logger.mu.Lock() - defer entry.Logger.mu.Unlock() - serialized, err := entry.Logger.Formatter.Format(entry) + formatter := entry.Logger.Formatter + entry.Logger.mu.Unlock() + + serialized, err := formatter.Format(entry) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err) + _, _ = fmt.Fprintln(os.Stderr, "Failed to format entry:", err) return } + + // Re-acquire the lock to serialize writes to the underlying io.Writer. + entry.Logger.mu.Lock() + defer entry.Logger.mu.Unlock() if _, err := entry.Logger.Out.Write(serialized); err != nil { - fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err) + _, _ = fmt.Fprintln(os.Stderr, "Failed to write to log:", err) } } -// Log will log a message at the level given as parameter. -// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit. -// For this behaviour Entry.Panic or Entry.Fatal should be used instead. -func (entry *Entry) Log(level Level, args ...interface{}) { +// Log logs a message at the specified level. +// +// Using Log with [PanicLevel] or [FatalLevel] intentionally does not +// trigger a panic or exit. Log treats the level as logging severity only; +// use [Entry.Panic] or [Entry.Fatal] when those side effects are desired. +func (entry *Entry) Log(level Level, args ...any) { + const panicAfter = false if entry.Logger.IsLevelEnabled(level) { - entry.log(level, fmt.Sprint(args...)) + entry.logArgs(level, panicAfter, args...) } } -func (entry *Entry) Trace(args ...interface{}) { +func (entry *Entry) Trace(args ...any) { entry.Log(TraceLevel, args...) } -func (entry *Entry) Debug(args ...interface{}) { +func (entry *Entry) Debug(args ...any) { entry.Log(DebugLevel, args...) } -func (entry *Entry) Print(args ...interface{}) { +func (entry *Entry) Print(args ...any) { entry.Info(args...) } -func (entry *Entry) Info(args ...interface{}) { +func (entry *Entry) Info(args ...any) { entry.Log(InfoLevel, args...) } -func (entry *Entry) Warn(args ...interface{}) { +func (entry *Entry) Warn(args ...any) { entry.Log(WarnLevel, args...) } -func (entry *Entry) Warning(args ...interface{}) { +func (entry *Entry) Warning(args ...any) { entry.Warn(args...) } -func (entry *Entry) Error(args ...interface{}) { +func (entry *Entry) Error(args ...any) { entry.Log(ErrorLevel, args...) } -func (entry *Entry) Fatal(args ...interface{}) { +func (entry *Entry) Fatal(args ...any) { entry.Log(FatalLevel, args...) entry.Logger.Exit(1) } -func (entry *Entry) Panic(args ...interface{}) { - entry.Log(PanicLevel, args...) +func (entry *Entry) Panic(args ...any) { + const panicAfter = true + if entry.Logger.IsLevelEnabled(PanicLevel) { + entry.logArgs(PanicLevel, panicAfter, args...) + } } // Entry Printf family functions -func (entry *Entry) Logf(level Level, format string, args ...interface{}) { +// Logf logs a formatted message at the specified level. +// +// Using Logf with [PanicLevel] or [FatalLevel] intentionally does not +// trigger a panic or exit. Logf treats the level as logging severity only; +// use [Entry.Panicf] or [Entry.Fatalf] when those side effects are desired. +func (entry *Entry) Logf(level Level, format string, args ...any) { + const panicAfter = false if entry.Logger.IsLevelEnabled(level) { - entry.Log(level, fmt.Sprintf(format, args...)) + entry.logf(level, panicAfter, format, args...) } } -func (entry *Entry) Tracef(format string, args ...interface{}) { +func (entry *Entry) Tracef(format string, args ...any) { entry.Logf(TraceLevel, format, args...) } -func (entry *Entry) Debugf(format string, args ...interface{}) { +func (entry *Entry) Debugf(format string, args ...any) { entry.Logf(DebugLevel, format, args...) } -func (entry *Entry) Infof(format string, args ...interface{}) { +func (entry *Entry) Infof(format string, args ...any) { entry.Logf(InfoLevel, format, args...) } -func (entry *Entry) Printf(format string, args ...interface{}) { +func (entry *Entry) Printf(format string, args ...any) { entry.Infof(format, args...) } -func (entry *Entry) Warnf(format string, args ...interface{}) { +func (entry *Entry) Warnf(format string, args ...any) { entry.Logf(WarnLevel, format, args...) } -func (entry *Entry) Warningf(format string, args ...interface{}) { +func (entry *Entry) Warningf(format string, args ...any) { entry.Warnf(format, args...) } -func (entry *Entry) Errorf(format string, args ...interface{}) { +func (entry *Entry) Errorf(format string, args ...any) { entry.Logf(ErrorLevel, format, args...) } -func (entry *Entry) Fatalf(format string, args ...interface{}) { +func (entry *Entry) Fatalf(format string, args ...any) { entry.Logf(FatalLevel, format, args...) entry.Logger.Exit(1) } -func (entry *Entry) Panicf(format string, args ...interface{}) { - entry.Logf(PanicLevel, format, args...) +func (entry *Entry) Panicf(format string, args ...any) { + const panicAfter = true + if entry.Logger.IsLevelEnabled(PanicLevel) { + entry.logf(PanicLevel, panicAfter, format, args...) + } } // Entry Println family functions -func (entry *Entry) Logln(level Level, args ...interface{}) { +// Logln logs a message at the specified level with Println-style spacing. +// +// Using Logln with [PanicLevel] or [FatalLevel] intentionally does not +// trigger a panic or exit. Logln treats the level as logging severity only; +// use [Entry.Panicln] or [Entry.Fatalln] when those side effects are desired. +func (entry *Entry) Logln(level Level, args ...any) { + const panicAfter = false if entry.Logger.IsLevelEnabled(level) { - entry.Log(level, entry.sprintlnn(args...)) + entry.logln(level, panicAfter, args...) } } -func (entry *Entry) Traceln(args ...interface{}) { +func (entry *Entry) Traceln(args ...any) { entry.Logln(TraceLevel, args...) } -func (entry *Entry) Debugln(args ...interface{}) { +func (entry *Entry) Debugln(args ...any) { entry.Logln(DebugLevel, args...) } -func (entry *Entry) Infoln(args ...interface{}) { +func (entry *Entry) Infoln(args ...any) { entry.Logln(InfoLevel, args...) } -func (entry *Entry) Println(args ...interface{}) { +func (entry *Entry) Println(args ...any) { entry.Infoln(args...) } -func (entry *Entry) Warnln(args ...interface{}) { +func (entry *Entry) Warnln(args ...any) { entry.Logln(WarnLevel, args...) } -func (entry *Entry) Warningln(args ...interface{}) { +func (entry *Entry) Warningln(args ...any) { entry.Warnln(args...) } -func (entry *Entry) Errorln(args ...interface{}) { +func (entry *Entry) Errorln(args ...any) { entry.Logln(ErrorLevel, args...) } -func (entry *Entry) Fatalln(args ...interface{}) { +func (entry *Entry) Fatalln(args ...any) { entry.Logln(FatalLevel, args...) entry.Logger.Exit(1) } -func (entry *Entry) Panicln(args ...interface{}) { - entry.Logln(PanicLevel, args...) +func (entry *Entry) Panicln(args ...any) { + const panicAfter = true + if entry.Logger.IsLevelEnabled(PanicLevel) { + entry.logln(PanicLevel, panicAfter, args...) + } } -// sprintlnn => Sprint no newline. This is to get the behavior of how -// fmt.Sprintln where spaces are always added between operands, regardless of -// their type. Instead of vendoring the Sprintln implementation to spare a -// string allocation, we do the simplest thing. -func (entry *Entry) sprintlnn(args ...interface{}) string { - msg := fmt.Sprintln(args...) - return msg[:len(msg)-1] +// sprint is fmt.Sprint with fast paths for zero or one string argument. +func sprint(args ...any) string { + switch len(args) { + case 0: + return "" + case 1: + if msg, ok := args[0].(string); ok { + return msg + } + } + return fmt.Sprint(args...) } diff --git a/vendor/github.com/sirupsen/logrus/exported.go b/vendor/github.com/sirupsen/logrus/exported.go index 017c30ce6..8b261c124 100644 --- a/vendor/github.com/sirupsen/logrus/exported.go +++ b/vendor/github.com/sirupsen/logrus/exported.go @@ -6,11 +6,12 @@ import ( "time" ) -var ( - // std is the name of the standard logger in stdlib `log` - std = New() -) +// std is the package-level standard logger, similar to the default logger +// in the stdlib [log] package. +var std = New() +// StandardLogger returns the package-level standard logger used by +// the top-level logging functions. func StandardLogger() *Logger { return std } @@ -41,7 +42,7 @@ func GetLevel() Level { return std.GetLevel() } -// IsLevelEnabled checks if the log level of the standard logger is greater than the level param +// IsLevelEnabled checks if logging for the given level is enabled for the standard logger. func IsLevelEnabled(level Level) bool { return std.IsLevelEnabled(level) } @@ -51,9 +52,10 @@ func AddHook(hook Hook) { std.AddHook(hook) } -// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key. +// WithError creates an entry from the standard logger and adds an error to it, +// using the value defined in [ErrorKey] as key. func WithError(err error) *Entry { - return std.WithField(ErrorKey, err) + return std.WithError(err) } // WithContext creates an entry from the standard logger and adds a context to it. @@ -61,210 +63,203 @@ func WithContext(ctx context.Context) *Entry { return std.WithContext(ctx) } -// WithField creates an entry from the standard logger and adds a field to -// it. If you want multiple fields, use `WithFields`. -// -// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal -// or Panic on the Entry it returns. -func WithField(key string, value interface{}) *Entry { +// WithField creates an entry from the standard logger and adds a single field. +// For multiple fields, prefer [WithFields] over chaining WithField calls. +func WithField(key string, value any) *Entry { return std.WithField(key, value) } -// WithFields creates an entry from the standard logger and adds multiple -// fields to it. This is simply a helper for `WithField`, invoking it -// once for each field. -// -// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal -// or Panic on the Entry it returns. +// WithFields creates an entry from the standard logger and adds the fields to it. func WithFields(fields Fields) *Entry { return std.WithFields(fields) } -// WithTime creates an entry from the standard logger and overrides the time of -// logs generated with it. -// -// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal -// or Panic on the Entry it returns. +// WithTime creates an entry from the standard logger and overrides the time +// used for logs generated with it. func WithTime(t time.Time) *Entry { return std.WithTime(t) } -// Trace logs a message at level Trace on the standard logger. -func Trace(args ...interface{}) { +// Trace logs a message at level [TraceLevel] on the standard logger. +func Trace(args ...any) { std.Trace(args...) } -// Debug logs a message at level Debug on the standard logger. -func Debug(args ...interface{}) { +// Debug logs a message at level [DebugLevel] on the standard logger. +func Debug(args ...any) { std.Debug(args...) } -// Print logs a message at level Info on the standard logger. -func Print(args ...interface{}) { +// Print logs a message at level [InfoLevel] on the standard logger. +func Print(args ...any) { std.Print(args...) } -// Info logs a message at level Info on the standard logger. -func Info(args ...interface{}) { +// Info logs a message at level [InfoLevel] on the standard logger. +func Info(args ...any) { std.Info(args...) } -// Warn logs a message at level Warn on the standard logger. -func Warn(args ...interface{}) { +// Warn logs a message at level [WarnLevel] on the standard logger. +func Warn(args ...any) { std.Warn(args...) } -// Warning logs a message at level Warn on the standard logger. -func Warning(args ...interface{}) { +// Warning logs a message at level [WarnLevel] on the standard logger. +func Warning(args ...any) { std.Warning(args...) } -// Error logs a message at level Error on the standard logger. -func Error(args ...interface{}) { +// Error logs a message at level [ErrorLevel] on the standard logger. +func Error(args ...any) { std.Error(args...) } -// Panic logs a message at level Panic on the standard logger. -func Panic(args ...interface{}) { +// Panic logs a message at level [PanicLevel] on the standard logger. +func Panic(args ...any) { std.Panic(args...) } -// Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1. -func Fatal(args ...interface{}) { +// Fatal logs a message at level [FatalLevel] on the standard logger, +// then exits the process with status 1. +func Fatal(args ...any) { std.Fatal(args...) } -// TraceFn logs a message from a func at level Trace on the standard logger. +// TraceFn logs a message from a func at level [TraceLevel] on the standard logger. func TraceFn(fn LogFunction) { std.TraceFn(fn) } -// DebugFn logs a message from a func at level Debug on the standard logger. +// DebugFn logs a message from a func at level [DebugLevel] on the standard logger. func DebugFn(fn LogFunction) { std.DebugFn(fn) } -// PrintFn logs a message from a func at level Info on the standard logger. +// PrintFn logs a message from a func at level [InfoLevel] on the standard logger. func PrintFn(fn LogFunction) { std.PrintFn(fn) } -// InfoFn logs a message from a func at level Info on the standard logger. +// InfoFn logs a message from a func at level [InfoLevel] on the standard logger. func InfoFn(fn LogFunction) { std.InfoFn(fn) } -// WarnFn logs a message from a func at level Warn on the standard logger. +// WarnFn logs a message from a func at level [WarnLevel] on the standard logger. func WarnFn(fn LogFunction) { std.WarnFn(fn) } -// WarningFn logs a message from a func at level Warn on the standard logger. +// WarningFn logs a message from a func at level [WarnLevel] on the standard logger. func WarningFn(fn LogFunction) { std.WarningFn(fn) } -// ErrorFn logs a message from a func at level Error on the standard logger. +// ErrorFn logs a message from a func at level [ErrorLevel] on the standard logger. func ErrorFn(fn LogFunction) { std.ErrorFn(fn) } -// PanicFn logs a message from a func at level Panic on the standard logger. +// PanicFn logs a message from a func at level [PanicLevel] on the standard logger. func PanicFn(fn LogFunction) { std.PanicFn(fn) } -// FatalFn logs a message from a func at level Fatal on the standard logger then the process will exit with status set to 1. +// FatalFn logs a message from a func at level [FatalLevel] on the standard logger, +// then exits the process with status 1. func FatalFn(fn LogFunction) { std.FatalFn(fn) } -// Tracef logs a message at level Trace on the standard logger. -func Tracef(format string, args ...interface{}) { +// Tracef logs a message at level [TraceLevel] on the standard logger. +func Tracef(format string, args ...any) { std.Tracef(format, args...) } -// Debugf logs a message at level Debug on the standard logger. -func Debugf(format string, args ...interface{}) { +// Debugf logs a message at level [DebugLevel] on the standard logger. +func Debugf(format string, args ...any) { std.Debugf(format, args...) } -// Printf logs a message at level Info on the standard logger. -func Printf(format string, args ...interface{}) { +// Printf logs a message at level [InfoLevel] on the standard logger. +func Printf(format string, args ...any) { std.Printf(format, args...) } -// Infof logs a message at level Info on the standard logger. -func Infof(format string, args ...interface{}) { +// Infof logs a message at level [InfoLevel] on the standard logger. +func Infof(format string, args ...any) { std.Infof(format, args...) } -// Warnf logs a message at level Warn on the standard logger. -func Warnf(format string, args ...interface{}) { +// Warnf logs a message at level [WarnLevel] on the standard logger. +func Warnf(format string, args ...any) { std.Warnf(format, args...) } -// Warningf logs a message at level Warn on the standard logger. -func Warningf(format string, args ...interface{}) { +// Warningf logs a message at level [WarnLevel] on the standard logger. +func Warningf(format string, args ...any) { std.Warningf(format, args...) } -// Errorf logs a message at level Error on the standard logger. -func Errorf(format string, args ...interface{}) { +// Errorf logs a message at level [ErrorLevel] on the standard logger. +func Errorf(format string, args ...any) { std.Errorf(format, args...) } -// Panicf logs a message at level Panic on the standard logger. -func Panicf(format string, args ...interface{}) { +// Panicf logs a message at level [PanicLevel] on the standard logger. +func Panicf(format string, args ...any) { std.Panicf(format, args...) } -// Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1. -func Fatalf(format string, args ...interface{}) { +// Fatalf logs a message at level [FatalLevel] on the standard logger, +// then exits the process with status 1. +func Fatalf(format string, args ...any) { std.Fatalf(format, args...) } -// Traceln logs a message at level Trace on the standard logger. -func Traceln(args ...interface{}) { +// Traceln logs a message at level [TraceLevel] on the standard logger. +func Traceln(args ...any) { std.Traceln(args...) } -// Debugln logs a message at level Debug on the standard logger. -func Debugln(args ...interface{}) { +// Debugln logs a message at level [DebugLevel] on the standard logger. +func Debugln(args ...any) { std.Debugln(args...) } -// Println logs a message at level Info on the standard logger. -func Println(args ...interface{}) { +// Println logs a message at level [InfoLevel] on the standard logger. +func Println(args ...any) { std.Println(args...) } -// Infoln logs a message at level Info on the standard logger. -func Infoln(args ...interface{}) { +// Infoln logs a message at level [InfoLevel] on the standard logger. +func Infoln(args ...any) { std.Infoln(args...) } -// Warnln logs a message at level Warn on the standard logger. -func Warnln(args ...interface{}) { +// Warnln logs a message at level [WarnLevel] on the standard logger. +func Warnln(args ...any) { std.Warnln(args...) } -// Warningln logs a message at level Warn on the standard logger. -func Warningln(args ...interface{}) { +// Warningln logs a message at level [WarnLevel] on the standard logger. +func Warningln(args ...any) { std.Warningln(args...) } -// Errorln logs a message at level Error on the standard logger. -func Errorln(args ...interface{}) { +// Errorln logs a message at level [ErrorLevel] on the standard logger. +func Errorln(args ...any) { std.Errorln(args...) } -// Panicln logs a message at level Panic on the standard logger. -func Panicln(args ...interface{}) { +// Panicln logs a message at level [PanicLevel] on the standard logger. +func Panicln(args ...any) { std.Panicln(args...) } -// Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1. -func Fatalln(args ...interface{}) { +// Fatalln logs a message at level [FatalLevel] on the standard logger, +// then exits the process with status 1. +func Fatalln(args ...any) { std.Fatalln(args...) } diff --git a/vendor/github.com/sirupsen/logrus/formatter.go b/vendor/github.com/sirupsen/logrus/formatter.go index 408883773..16f2e0e0f 100644 --- a/vendor/github.com/sirupsen/logrus/formatter.go +++ b/vendor/github.com/sirupsen/logrus/formatter.go @@ -2,27 +2,40 @@ package logrus import "time" -// Default key names for the default fields const ( + // defaultTimestampFormat is the layout used to format entry timestamps + // when a formatter has not specified a custom TimestampFormat. + // It follows time.RFC3339 and is applied unless timestamps are disabled. defaultTimestampFormat = time.RFC3339 - FieldKeyMsg = "msg" - FieldKeyLevel = "level" - FieldKeyTime = "time" - FieldKeyLogrusError = "logrus_error" - FieldKeyFunc = "func" - FieldKeyFile = "file" + + // defaultFields is the number of commonly included predefined log entry fields + // (msg, level, time). It is used as a capacity hint when constructing + // intermediate collections during formatting (for example, the fixed key list). + // + // It does not include the optional "logrus_error", "func", or "file" fields. + defaultFields = 3 ) -// The Formatter interface is used to implement a custom Formatter. It takes an -// `Entry`. It exposes all the fields, including the default ones: +// Default key names for the default fields +const ( + FieldKeyMsg = "msg" + FieldKeyLevel = "level" + FieldKeyTime = "time" + FieldKeyLogrusError = "logrus_error" + FieldKeyFunc = "func" + FieldKeyFile = "file" +) + +// Formatter is implemented by types that format log entries. It receives an +// [*Entry], which contains: // -// * `entry.Data["msg"]`. The message passed from Info, Warn, Error .. -// * `entry.Data["time"]`. The timestamp. -// * `entry.Data["level"]. The level the entry was logged at. +// - entry.Message: the message passed to logging methods such as [Info], [Warn], [Error] +// - entry.Time: the timestamp +// - entry.Level: the log level // -// Any additional fields added with `WithField` or `WithFields` are also in -// `entry.Data`. Format is expected to return an array of bytes which are then -// logged to `logger.Out`. +// Additional fields added with [WithField] or [WithFields] are available in +// [Entry.Data]. Format should return the formatted log entry as a byte slice, +// which is written to [Logger.Out]. type Formatter interface { Format(*Entry) ([]byte, error) } @@ -30,12 +43,12 @@ type Formatter interface { // This is to not silently overwrite `time`, `msg`, `func` and `level` fields when // dumping it. If this code wasn't there doing: // -// logrus.WithField("level", 1).Info("hello") +// logrus.WithField("level", 1).Info("hello") // // Would just silently drop the user provided level. Instead with this code // it'll logged as: // -// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."} +// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."} // // It's not exported because it's still using Data in an opinionated way. It's to // avoid code duplication between the two default formatters. diff --git a/vendor/github.com/sirupsen/logrus/json_formatter.go b/vendor/github.com/sirupsen/logrus/json_formatter.go index c96dc5636..fac7695e9 100644 --- a/vendor/github.com/sirupsen/logrus/json_formatter.go +++ b/vendor/github.com/sirupsen/logrus/json_formatter.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "runtime" + "strconv" ) type fieldKey string @@ -20,7 +21,13 @@ func (f FieldMap) resolve(key fieldKey) string { return string(key) } -// JSONFormatter formats logs into parsable json +// JSONFormatter formats logs into parsable JSON. +// +// Fields from [Entry.Data] are included in the JSON object together with the +// standard fields derived from the entry. If a field conflicts with a standard +// field, it is prefixed with "fields.". Standard field names can be customized +// through FieldMap. When DataKey is set, fields from [Entry.Data] are nested +// under that key instead. type JSONFormatter struct { // TimestampFormat sets the format used for marshaling timestamps. // The format to use is the same than for time.Format or time.Parse from the standard @@ -61,7 +68,8 @@ type JSONFormatter struct { // Format renders a single log entry func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { - data := make(Fields, len(entry.Data)+4) + caller := entry.Caller + data := make(Fields, len(entry.Data)+defaultFields) for k, v := range entry.Data { switch v := v.(type) { case error: @@ -73,13 +81,14 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { } } - if f.DataKey != "" { - newData := make(Fields, 4) + if f.DataKey != "" && len(entry.Data) > 0 { + newData := make(Fields, defaultFields+1) newData[f.DataKey] = data data = newData } - prefixFieldClashes(data, f.FieldMap, entry.HasCaller()) + hasCaller := caller != nil + prefixFieldClashes(data, f.FieldMap, hasCaller) timestampFormat := f.TimestampFormat if timestampFormat == "" { @@ -94,11 +103,13 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { } data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String() - if entry.HasCaller() { - funcVal := entry.Caller.Function - fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) + if caller != nil { + var funcVal, fileVal string if f.CallerPrettyfier != nil { - funcVal, fileVal = f.CallerPrettyfier(entry.Caller) + funcVal, fileVal = f.CallerPrettyfier(caller) + } else { + funcVal = caller.Function + fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10) } if funcVal != "" { data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal @@ -108,11 +119,9 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { } } - var b *bytes.Buffer - if entry.Buffer != nil { - b = entry.Buffer - } else { - b = &bytes.Buffer{} + b := entry.Buffer + if b == nil { + b = new(bytes.Buffer) } encoder := json.NewEncoder(b) diff --git a/vendor/github.com/sirupsen/logrus/level.go b/vendor/github.com/sirupsen/logrus/level.go new file mode 100644 index 000000000..7bd4255d3 --- /dev/null +++ b/vendor/github.com/sirupsen/logrus/level.go @@ -0,0 +1,101 @@ +package logrus + +import ( + "strings" + "sync" +) + +const ( + ansiReset = "\x1b[0m" // reset attributes + ansiRed = "\x1b[31m" // red + ansiYellow = "\x1b[33m" // yellow + ansiCyan = "\x1b[36m" // cyan + ansiDimCyan = "\x1b[2;36m" // dim cyan + ansiDimWhite = "\x1b[2;37m" // dim white (light gray) +) + +type lvlPrefix struct { + full string + truncated string + padded string +} + +func colorize(level Level, s string) string { + color := ansiCyan + switch level { + case TraceLevel: + color = ansiDimWhite + case DebugLevel: + color = ansiDimCyan + case WarnLevel: + color = ansiYellow + case ErrorLevel, FatalLevel, PanicLevel: + color = ansiRed + case InfoLevel: + color = ansiCyan + } + return color + s + ansiReset +} + +func formatLevel(level Level, disableTrunc, pad bool, maxLen int) string { + upper := strings.ToUpper(level.String()) + + if pad && maxLen > len(upper) { + upper += strings.Repeat(" ", maxLen-len(upper)) + } + + if !pad && !disableTrunc && len(upper) > 4 { + upper = upper[:4] + } + + return colorize(level, upper) +} + +var levelPrefixOnce = sync.OnceValues(func() (map[Level]lvlPrefix, lvlPrefix) { + var maxLevel Level + maxLen := 0 + for _, lvl := range AllLevels { + if lvl > maxLevel { + maxLevel = lvl + } + if l := len(lvl.String()); l > maxLen { + maxLen = l + } + } + + prefix := make(map[Level]lvlPrefix, len(AllLevels)) + for _, lvl := range AllLevels { + prefix[lvl] = lvlPrefix{ + full: formatLevel(lvl, true, false, maxLen), + truncated: formatLevel(lvl, false, false, maxLen), + padded: formatLevel(lvl, true, true, maxLen), + } + } + + unknownLevel := maxLevel + 1 + unknown := lvlPrefix{ + full: formatLevel(unknownLevel, true, false, maxLen), + truncated: formatLevel(unknownLevel, false, false, maxLen), + padded: formatLevel(unknownLevel, true, true, maxLen), + } + + return prefix, unknown +}) + +func levelPrefix(level Level, disableTrunc, pad bool) string { + prefix, unknown := levelPrefixOnce() + + p, ok := prefix[level] + if !ok { + p = unknown + } + + switch { + case pad: + return p.padded + case !disableTrunc: + return p.truncated + default: + return p.full + } +} diff --git a/vendor/github.com/sirupsen/logrus/logger.go b/vendor/github.com/sirupsen/logrus/logger.go index f5b8c439e..17a46e6a6 100644 --- a/vendor/github.com/sirupsen/logrus/logger.go +++ b/vendor/github.com/sirupsen/logrus/logger.go @@ -12,17 +12,19 @@ import ( // LogFunction For big messages, it can be more efficient to pass a function // and only call it if the log level is actually enables rather than // generating the log message and then checking if the level is enabled -type LogFunction func() []interface{} +type LogFunction func() []any type Logger struct { // The logs are `io.Copy`'d to this in a mutex. It's common to set this to a // file, or leave it default which is `os.Stderr`. You can also set this to // something more adventurous, such as logging to Kafka. Out io.Writer + // Hooks for the logger instance. These allow firing events based on logging // levels and log entries. For example, to send errors to an error tracking // service, log to StatsD or dump the core on fatal errors. Hooks LevelHooks + // All log entries pass through the formatter before logged to Out. The // included formatters are `TextFormatter` and `JSONFormatter` for which // TextFormatter is the default. In development (when a TTY is attached) it @@ -38,37 +40,44 @@ type Logger struct { // to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be // logged. Level Level + // Used to sync writing to the log. Locking is enabled by Default - mu MutexWrap + mu mutexWrap + // Reusable empty entry entryPool sync.Pool + // Function to exit the application, defaults to `os.Exit()` - ExitFunc exitFunc + ExitFunc func(int) + // The buffer pool used to format the log. If it is nil, the default global // buffer pool will be used. BufferPool BufferPool } -type exitFunc func(int) +// MutexWrap is the mutex implementation used by [Logger]. +// +// Deprecated: MutexWrap is an implementation detail of Logger and should not be used directly. +type MutexWrap = mutexWrap -type MutexWrap struct { +type mutexWrap struct { lock sync.Mutex disabled bool } -func (mw *MutexWrap) Lock() { +func (mw *mutexWrap) Lock() { if !mw.disabled { mw.lock.Lock() } } -func (mw *MutexWrap) Unlock() { +func (mw *mutexWrap) Unlock() { if !mw.disabled { mw.lock.Unlock() } } -func (mw *MutexWrap) Disable() { +func (mw *mutexWrap) Disable() { mw.disabled = true } @@ -104,7 +113,7 @@ func (logger *Logger) newEntry() *Entry { } func (logger *Logger) releaseEntry(entry *Entry) { - entry.Data = map[string]interface{}{} + entry.Data = map[string]any{} logger.entryPool.Put(entry) } @@ -112,7 +121,7 @@ func (logger *Logger) releaseEntry(entry *Entry) { // Debug, Print, Info, Warn, Error, Fatal or Panic must be then applied to // this new returned entry. // If you want multiple fields, use `WithFields`. -func (logger *Logger) WithField(key string, value interface{}) *Entry { +func (logger *Logger) WithField(key string, value any) *Entry { entry := logger.newEntry() defer logger.releaseEntry(entry) return entry.WithField(key, value) @@ -148,7 +157,12 @@ func (logger *Logger) WithTime(t time.Time) *Entry { return entry.WithTime(t) } -func (logger *Logger) Logf(level Level, format string, args ...interface{}) { +// Logf logs a formatted message at the specified level. +// +// Using Logf with [PanicLevel] or [FatalLevel] intentionally does not +// trigger a panic or exit. Logf treats the level as logging severity only; +// use [Logger.Panicf] or [Logger.Fatalf] when those side effects are desired. +func (logger *Logger) Logf(level Level, format string, args ...any) { if logger.IsLevelEnabled(level) { entry := logger.newEntry() entry.Logf(level, format, args...) @@ -156,49 +170,55 @@ func (logger *Logger) Logf(level Level, format string, args ...interface{}) { } } -func (logger *Logger) Tracef(format string, args ...interface{}) { +func (logger *Logger) Tracef(format string, args ...any) { logger.Logf(TraceLevel, format, args...) } -func (logger *Logger) Debugf(format string, args ...interface{}) { +func (logger *Logger) Debugf(format string, args ...any) { logger.Logf(DebugLevel, format, args...) } -func (logger *Logger) Infof(format string, args ...interface{}) { +func (logger *Logger) Infof(format string, args ...any) { logger.Logf(InfoLevel, format, args...) } -func (logger *Logger) Printf(format string, args ...interface{}) { +func (logger *Logger) Printf(format string, args ...any) { entry := logger.newEntry() entry.Printf(format, args...) logger.releaseEntry(entry) } -func (logger *Logger) Warnf(format string, args ...interface{}) { +func (logger *Logger) Warnf(format string, args ...any) { logger.Logf(WarnLevel, format, args...) } -func (logger *Logger) Warningf(format string, args ...interface{}) { +func (logger *Logger) Warningf(format string, args ...any) { logger.Warnf(format, args...) } -func (logger *Logger) Errorf(format string, args ...interface{}) { +func (logger *Logger) Errorf(format string, args ...any) { logger.Logf(ErrorLevel, format, args...) } -func (logger *Logger) Fatalf(format string, args ...interface{}) { +func (logger *Logger) Fatalf(format string, args ...any) { logger.Logf(FatalLevel, format, args...) logger.Exit(1) } -func (logger *Logger) Panicf(format string, args ...interface{}) { - logger.Logf(PanicLevel, format, args...) +func (logger *Logger) Panicf(format string, args ...any) { + if logger.IsLevelEnabled(PanicLevel) { + entry := logger.newEntry() + defer logger.releaseEntry(entry) + entry.Panicf(format, args...) + } } -// Log will log a message at the level given as parameter. -// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit. -// For this behaviour Logger.Panic or Logger.Fatal should be used instead. -func (logger *Logger) Log(level Level, args ...interface{}) { +// Log logs a message at the specified level. +// +// Using Log with [PanicLevel] or [FatalLevel] intentionally does not +// trigger a panic or exit. Log treats the level as logging severity only; +// use [Logger.Panic] or [Logger.Fatal] when those side effects are desired. +func (logger *Logger) Log(level Level, args ...any) { if logger.IsLevelEnabled(level) { entry := logger.newEntry() entry.Log(level, args...) @@ -206,6 +226,11 @@ func (logger *Logger) Log(level Level, args ...interface{}) { } } +// LogFn logs a message returned by fn at the specified level. +// +// Using LogFn with [PanicLevel] or [FatalLevel] intentionally does not +// trigger a panic or exit. LogFn treats the level as logging severity only; +// use [Logger.PanicFn] or [Logger.FatalFn] when those side effects are desired. func (logger *Logger) LogFn(level Level, fn LogFunction) { if logger.IsLevelEnabled(level) { entry := logger.newEntry() @@ -214,43 +239,47 @@ func (logger *Logger) LogFn(level Level, fn LogFunction) { } } -func (logger *Logger) Trace(args ...interface{}) { +func (logger *Logger) Trace(args ...any) { logger.Log(TraceLevel, args...) } -func (logger *Logger) Debug(args ...interface{}) { +func (logger *Logger) Debug(args ...any) { logger.Log(DebugLevel, args...) } -func (logger *Logger) Info(args ...interface{}) { +func (logger *Logger) Info(args ...any) { logger.Log(InfoLevel, args...) } -func (logger *Logger) Print(args ...interface{}) { +func (logger *Logger) Print(args ...any) { entry := logger.newEntry() entry.Print(args...) logger.releaseEntry(entry) } -func (logger *Logger) Warn(args ...interface{}) { +func (logger *Logger) Warn(args ...any) { logger.Log(WarnLevel, args...) } -func (logger *Logger) Warning(args ...interface{}) { +func (logger *Logger) Warning(args ...any) { logger.Warn(args...) } -func (logger *Logger) Error(args ...interface{}) { +func (logger *Logger) Error(args ...any) { logger.Log(ErrorLevel, args...) } -func (logger *Logger) Fatal(args ...interface{}) { +func (logger *Logger) Fatal(args ...any) { logger.Log(FatalLevel, args...) logger.Exit(1) } -func (logger *Logger) Panic(args ...interface{}) { - logger.Log(PanicLevel, args...) +func (logger *Logger) Panic(args ...any) { + if logger.IsLevelEnabled(PanicLevel) { + entry := logger.newEntry() + defer logger.releaseEntry(entry) + entry.Panic(args...) + } } func (logger *Logger) TraceFn(fn LogFunction) { @@ -289,10 +318,19 @@ func (logger *Logger) FatalFn(fn LogFunction) { } func (logger *Logger) PanicFn(fn LogFunction) { - logger.LogFn(PanicLevel, fn) + if logger.IsLevelEnabled(PanicLevel) { + entry := logger.newEntry() + defer logger.releaseEntry(entry) + entry.Panic(fn()...) + } } -func (logger *Logger) Logln(level Level, args ...interface{}) { +// Logln logs a message at the specified level with Println-style spacing. +// +// Using Logln with [PanicLevel] or [FatalLevel] intentionally does not +// trigger a panic or exit. Logln treats the level as logging severity only; +// use [Logger.Panicln] or [Logger.Fatalln] when those side effects are desired. +func (logger *Logger) Logln(level Level, args ...any) { if logger.IsLevelEnabled(level) { entry := logger.newEntry() entry.Logln(level, args...) @@ -300,43 +338,47 @@ func (logger *Logger) Logln(level Level, args ...interface{}) { } } -func (logger *Logger) Traceln(args ...interface{}) { +func (logger *Logger) Traceln(args ...any) { logger.Logln(TraceLevel, args...) } -func (logger *Logger) Debugln(args ...interface{}) { +func (logger *Logger) Debugln(args ...any) { logger.Logln(DebugLevel, args...) } -func (logger *Logger) Infoln(args ...interface{}) { +func (logger *Logger) Infoln(args ...any) { logger.Logln(InfoLevel, args...) } -func (logger *Logger) Println(args ...interface{}) { +func (logger *Logger) Println(args ...any) { entry := logger.newEntry() entry.Println(args...) logger.releaseEntry(entry) } -func (logger *Logger) Warnln(args ...interface{}) { +func (logger *Logger) Warnln(args ...any) { logger.Logln(WarnLevel, args...) } -func (logger *Logger) Warningln(args ...interface{}) { +func (logger *Logger) Warningln(args ...any) { logger.Warnln(args...) } -func (logger *Logger) Errorln(args ...interface{}) { +func (logger *Logger) Errorln(args ...any) { logger.Logln(ErrorLevel, args...) } -func (logger *Logger) Fatalln(args ...interface{}) { +func (logger *Logger) Fatalln(args ...any) { logger.Logln(FatalLevel, args...) logger.Exit(1) } -func (logger *Logger) Panicln(args ...interface{}) { - logger.Logln(PanicLevel, args...) +func (logger *Logger) Panicln(args ...any) { + if logger.IsLevelEnabled(PanicLevel) { + entry := logger.newEntry() + defer logger.releaseEntry(entry) + entry.Panicln(args...) + } } func (logger *Logger) Exit(code int) { @@ -375,7 +417,22 @@ func (logger *Logger) AddHook(hook Hook) { logger.Hooks.Add(hook) } -// IsLevelEnabled checks if the log level of the logger is greater than the level param +// hooksForLevel returns a snapshot of the hooks registered for the given level. +// The returned slice is a shallow copy and may be used without holding logger.mu. +func (logger *Logger) hooksForLevel(level Level) []Hook { + logger.mu.Lock() + hooks := logger.Hooks[level] + if len(hooks) == 0 { + logger.mu.Unlock() + return nil + } + out := make([]Hook, len(hooks)) + copy(out, hooks) + logger.mu.Unlock() + return out +} + +// IsLevelEnabled checks if logging for the given level is enabled. func (logger *Logger) IsLevelEnabled(level Level) bool { return logger.level() >= level } @@ -394,6 +451,7 @@ func (logger *Logger) SetOutput(output io.Writer) { logger.Out = output } +// SetReportCaller sets whether the caller stack frame must be logged. func (logger *Logger) SetReportCaller(reportCaller bool) { logger.mu.Lock() defer logger.mu.Unlock() @@ -403,9 +461,9 @@ func (logger *Logger) SetReportCaller(reportCaller bool) { // ReplaceHooks replaces the logger hooks and returns the old ones func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks { logger.mu.Lock() + defer logger.mu.Unlock() oldHooks := logger.Hooks logger.Hooks = hooks - logger.mu.Unlock() return oldHooks } diff --git a/vendor/github.com/sirupsen/logrus/logrus.go b/vendor/github.com/sirupsen/logrus/logrus.go index 37fc4fef8..d52b4ba73 100644 --- a/vendor/github.com/sirupsen/logrus/logrus.go +++ b/vendor/github.com/sirupsen/logrus/logrus.go @@ -1,13 +1,13 @@ package logrus import ( + "bytes" "fmt" "log" - "strings" ) // Fields type, used to pass to [WithFields]. -type Fields map[string]interface{} +type Fields map[string]any // Level type // @@ -16,39 +16,56 @@ type Level uint32 // Convert the Level to a string. E.g. [PanicLevel] becomes "panic". func (level Level) String() string { - if b, err := level.MarshalText(); err == nil { - return string(b) - } else { + switch level { + case TraceLevel: + return "trace" + case DebugLevel: + return "debug" + case InfoLevel: + return "info" + case WarnLevel: + return "warning" + case ErrorLevel: + return "error" + case FatalLevel: + return "fatal" + case PanicLevel: + return "panic" + default: return "unknown" } } // ParseLevel takes a string level and returns the Logrus log level constant. func ParseLevel(lvl string) (Level, error) { - switch strings.ToLower(lvl) { - case "panic": - return PanicLevel, nil - case "fatal": - return FatalLevel, nil - case "error": - return ErrorLevel, nil - case "warn", "warning": - return WarnLevel, nil - case "info": - return InfoLevel, nil - case "debug": - return DebugLevel, nil - case "trace": - return TraceLevel, nil - } + return parseLevel([]byte(lvl)) +} - var l Level - return l, fmt.Errorf("not a valid logrus Level: %q", lvl) +func parseLevel(b []byte) (Level, error) { + switch { + case bytes.EqualFold(b, []byte("panic")): + return PanicLevel, nil + case bytes.EqualFold(b, []byte("fatal")): + return FatalLevel, nil + case bytes.EqualFold(b, []byte("error")): + return ErrorLevel, nil + case bytes.EqualFold(b, []byte("warn")), + bytes.EqualFold(b, []byte("warning")): + return WarnLevel, nil + case bytes.EqualFold(b, []byte("info")): + return InfoLevel, nil + case bytes.EqualFold(b, []byte("debug")): + return DebugLevel, nil + case bytes.EqualFold(b, []byte("trace")): + return TraceLevel, nil + default: + return 0, fmt.Errorf("not a valid logrus Level: %q", b) + } } // UnmarshalText implements encoding.TextUnmarshaler. func (level *Level) UnmarshalText(text []byte) error { - l, err := ParseLevel(string(text)) + l, err := parseLevel(text) if err != nil { return err } @@ -60,23 +77,11 @@ func (level *Level) UnmarshalText(text []byte) error { func (level Level) MarshalText() ([]byte, error) { switch level { - case TraceLevel: - return []byte("trace"), nil - case DebugLevel: - return []byte("debug"), nil - case InfoLevel: - return []byte("info"), nil - case WarnLevel: - return []byte("warning"), nil - case ErrorLevel: - return []byte("error"), nil - case FatalLevel: - return []byte("fatal"), nil - case PanicLevel: - return []byte("panic"), nil + case TraceLevel, DebugLevel, InfoLevel, WarnLevel, ErrorLevel, FatalLevel, PanicLevel: + return []byte(level.String()), nil + default: + return nil, fmt.Errorf("not a valid logrus level %d", level) } - - return nil, fmt.Errorf("not a valid logrus level %d", level) } // AllLevels exposing all logging levels. @@ -91,7 +96,7 @@ var AllLevels = []Level{ } // These are the different logging levels. You can set the logging level to log -// on your instance of logger, obtained with `logrus.New()`. +// on your instance of logger, obtained with [logrus.New]. const ( // PanicLevel level, highest level of severity. Logs and then calls panic with the // message passed to Debug, Info, ... @@ -113,78 +118,110 @@ const ( TraceLevel ) -// Won't compile if StdLogger can't be realized by a log.Logger +// Compile-time interface assertions. var ( - _ StdLogger = &log.Logger{} - _ StdLogger = &Entry{} - _ StdLogger = &Logger{} + _ StdLogger = (*log.Logger)(nil) + _ StdLogger = (*Entry)(nil) + _ StdLogger = (*Logger)(nil) + + _ FieldLogger = (*Logger)(nil) + _ FieldLogger = (*Entry)(nil) + _ FieldLogger = Ext1FieldLogger(nil) + + _ DebugLogger = (*Logger)(nil) + _ InfoLogger = (*Logger)(nil) + _ WarnLogger = (*Logger)(nil) + _ ErrorLogger = (*Logger)(nil) + _ TraceLogger = (*Logger)(nil) + + _ DebugLogger = (*Entry)(nil) + _ InfoLogger = (*Entry)(nil) + _ WarnLogger = (*Entry)(nil) + _ ErrorLogger = (*Entry)(nil) + _ TraceLogger = (*Entry)(nil) + + _ Ext1FieldLogger = (*Logger)(nil) + _ Ext1FieldLogger = (*Entry)(nil) ) // StdLogger is what your logrus-enabled library should take, that way // it'll accept a stdlib logger ([log.Logger]) and a logrus logger. // There's no standard interface, so this is the closest we get, unfortunately. type StdLogger interface { - Print(...interface{}) - Printf(string, ...interface{}) - Println(...interface{}) + Print(args ...any) + Printf(format string, args ...any) + Println(args ...any) - Fatal(...interface{}) - Fatalf(string, ...interface{}) - Fatalln(...interface{}) + Fatal(args ...any) + Fatalf(format string, args ...any) + Fatalln(args ...any) - Panic(...interface{}) - Panicf(string, ...interface{}) - Panicln(...interface{}) + Panic(args ...any) + Panicf(format string, args ...any) + Panicln(args ...any) } // FieldLogger extends the [StdLogger] interface, generalizing // the [Entry] and [Logger] types. type FieldLogger interface { - WithField(key string, value interface{}) *Entry + WithField(key string, value any) *Entry WithFields(fields Fields) *Entry WithError(err error) *Entry - Debugf(format string, args ...interface{}) - Infof(format string, args ...interface{}) - Printf(format string, args ...interface{}) - Warnf(format string, args ...interface{}) - Warningf(format string, args ...interface{}) - Errorf(format string, args ...interface{}) - Fatalf(format string, args ...interface{}) - Panicf(format string, args ...interface{}) + StdLogger + DebugLogger + InfoLogger + WarnLogger + ErrorLogger - Debug(args ...interface{}) - Info(args ...interface{}) - Print(args ...interface{}) - Warn(args ...interface{}) - Warning(args ...interface{}) - Error(args ...interface{}) - Fatal(args ...interface{}) - Panic(args ...interface{}) + // Legacy warning aliases. These are kept on FieldLogger for backwards + // compatibility, but are intentionally omitted from [WarnLogger]. - Debugln(args ...interface{}) - Infoln(args ...interface{}) - Println(args ...interface{}) - Warnln(args ...interface{}) - Warningln(args ...interface{}) - Errorln(args ...interface{}) - Fatalln(args ...interface{}) - Panicln(args ...interface{}) - - // IsDebugEnabled() bool - // IsInfoEnabled() bool - // IsWarnEnabled() bool - // IsErrorEnabled() bool - // IsFatalEnabled() bool - // IsPanicEnabled() bool + Warning(args ...any) + Warningf(format string, args ...any) + Warningln(args ...any) } -// Ext1FieldLogger (the first extension to [FieldLogger]) is superfluous, it is -// here for consistency. Do not use. Use [FieldLogger], [Logger] or [Entry] -// instead. +// DebugLogger provides convenience functions to log messages at level [DebugLevel]. +type DebugLogger interface { + Debug(args ...any) + Debugf(format string, args ...any) + Debugln(args ...any) +} + +// InfoLogger provides convenience functions to log messages at level [InfoLevel]. +type InfoLogger interface { + Info(args ...any) + Infof(format string, args ...any) + Infoln(args ...any) +} + +// WarnLogger provides convenience functions to log messages at level [WarnLevel]. +type WarnLogger interface { + Warn(args ...any) + Warnf(format string, args ...any) + Warnln(args ...any) +} + +// ErrorLogger provides convenience functions to log messages at level [ErrorLevel]. +type ErrorLogger interface { + Error(args ...any) + Errorf(format string, args ...any) + Errorln(args ...any) +} + +// TraceLogger provides convenience functions to log messages at level [TraceLevel]. +type TraceLogger interface { + Trace(args ...any) + Tracef(format string, args ...any) + Traceln(args ...any) +} + +// Ext1FieldLogger is FieldLogger extended with Trace-level methods. +// +// New code should prefer the smallest applicable interface, such as +// [FieldLogger] or [TraceLogger], or use [Logger] or [Entry] directly. type Ext1FieldLogger interface { FieldLogger - Tracef(format string, args ...interface{}) - Trace(args ...interface{}) - Traceln(args ...interface{}) + TraceLogger } diff --git a/vendor/github.com/sirupsen/logrus/terminal_check_appengine.go b/vendor/github.com/sirupsen/logrus/terminal_check_appengine.go index 2403de981..1c6202b8c 100644 --- a/vendor/github.com/sirupsen/logrus/terminal_check_appengine.go +++ b/vendor/github.com/sirupsen/logrus/terminal_check_appengine.go @@ -1,11 +1,7 @@ -// +build appengine +//go:build appengine package logrus -import ( - "io" -) - -func checkIfTerminal(w io.Writer) bool { +func checkIfTerminal(_ any) bool { return true } diff --git a/vendor/github.com/sirupsen/logrus/terminal_check_bsd.go b/vendor/github.com/sirupsen/logrus/terminal_check_bsd.go index 69956b425..ff9531ac4 100644 --- a/vendor/github.com/sirupsen/logrus/terminal_check_bsd.go +++ b/vendor/github.com/sirupsen/logrus/terminal_check_bsd.go @@ -1,5 +1,4 @@ -// +build darwin dragonfly freebsd netbsd openbsd hurd -// +build !js +//go:build (darwin || dragonfly || freebsd || netbsd || openbsd || hurd) && !tinygo package logrus diff --git a/vendor/github.com/sirupsen/logrus/terminal_check_js.go b/vendor/github.com/sirupsen/logrus/terminal_check_js.go deleted file mode 100644 index ebdae3ec6..000000000 --- a/vendor/github.com/sirupsen/logrus/terminal_check_js.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build js - -package logrus - -func isTerminal(fd int) bool { - return false -} diff --git a/vendor/github.com/sirupsen/logrus/terminal_check_no_terminal.go b/vendor/github.com/sirupsen/logrus/terminal_check_no_terminal.go index 97af92c68..17ae9f04f 100644 --- a/vendor/github.com/sirupsen/logrus/terminal_check_no_terminal.go +++ b/vendor/github.com/sirupsen/logrus/terminal_check_no_terminal.go @@ -1,11 +1,7 @@ -// +build js nacl plan9 +//go:build js || nacl || plan9 || wasi || wasip1 || tinygo package logrus -import ( - "io" -) - -func checkIfTerminal(w io.Writer) bool { +func checkIfTerminal(_ any) bool { return false } diff --git a/vendor/github.com/sirupsen/logrus/terminal_check_notappengine.go b/vendor/github.com/sirupsen/logrus/terminal_check_notappengine.go index 3293fb3ca..780a42a57 100644 --- a/vendor/github.com/sirupsen/logrus/terminal_check_notappengine.go +++ b/vendor/github.com/sirupsen/logrus/terminal_check_notappengine.go @@ -1,4 +1,4 @@ -// +build !appengine,!js,!windows,!nacl,!plan9 +//go:build !appengine && !js && !windows && !nacl && !plan9 && !wasi && !wasip1 && !tinygo package logrus @@ -10,7 +10,11 @@ import ( func checkIfTerminal(w io.Writer) bool { switch v := w.(type) { case *os.File: - return isTerminal(int(v.Fd())) + fd := v.Fd() + if fd > uintptr(^uint(0)>>1) { + return false + } + return isTerminal(int(fd)) default: return false } diff --git a/vendor/github.com/sirupsen/logrus/terminal_check_solaris.go b/vendor/github.com/sirupsen/logrus/terminal_check_solaris.go index f6710b3bd..8d9b26fca 100644 --- a/vendor/github.com/sirupsen/logrus/terminal_check_solaris.go +++ b/vendor/github.com/sirupsen/logrus/terminal_check_solaris.go @@ -1,3 +1,5 @@ +//go:build solaris && !tinygo + package logrus import ( diff --git a/vendor/github.com/sirupsen/logrus/terminal_check_unix.go b/vendor/github.com/sirupsen/logrus/terminal_check_unix.go index c9aed267a..b161506d3 100644 --- a/vendor/github.com/sirupsen/logrus/terminal_check_unix.go +++ b/vendor/github.com/sirupsen/logrus/terminal_check_unix.go @@ -1,7 +1,4 @@ -//go:build (linux || aix || zos) && !js && !wasi -// +build linux aix zos -// +build !js -// +build !wasi +//go:build (linux || aix || zos) && !tinygo package logrus diff --git a/vendor/github.com/sirupsen/logrus/terminal_check_wasi.go b/vendor/github.com/sirupsen/logrus/terminal_check_wasi.go deleted file mode 100644 index 2822b212f..000000000 --- a/vendor/github.com/sirupsen/logrus/terminal_check_wasi.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build wasi -// +build wasi - -package logrus - -func isTerminal(fd int) bool { - return false -} diff --git a/vendor/github.com/sirupsen/logrus/terminal_check_wasip1.go b/vendor/github.com/sirupsen/logrus/terminal_check_wasip1.go deleted file mode 100644 index 108a6be12..000000000 --- a/vendor/github.com/sirupsen/logrus/terminal_check_wasip1.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build wasip1 -// +build wasip1 - -package logrus - -func isTerminal(fd int) bool { - return false -} diff --git a/vendor/github.com/sirupsen/logrus/terminal_check_windows.go b/vendor/github.com/sirupsen/logrus/terminal_check_windows.go index 2879eb50e..5fd3c4313 100644 --- a/vendor/github.com/sirupsen/logrus/terminal_check_windows.go +++ b/vendor/github.com/sirupsen/logrus/terminal_check_windows.go @@ -1,4 +1,4 @@ -// +build !appengine,!js,windows +//go:build windows && !appengine package logrus diff --git a/vendor/github.com/sirupsen/logrus/text_formatter.go b/vendor/github.com/sirupsen/logrus/text_formatter.go index 6dfeb18b1..82c1f3da2 100644 --- a/vendor/github.com/sirupsen/logrus/text_formatter.go +++ b/vendor/github.com/sirupsen/logrus/text_formatter.go @@ -3,30 +3,32 @@ package logrus import ( "bytes" "fmt" + "maps" "os" + "reflect" "runtime" - "sort" + "slices" "strconv" "strings" "sync" "time" - "unicode/utf8" ) -const ( - red = 31 - yellow = 33 - blue = 36 - gray = 37 -) +var baseTimestamp = time.Now() -var baseTimestamp time.Time - -func init() { - baseTimestamp = time.Now() -} - -// TextFormatter formats logs into text +// TextFormatter formats logs into text. +// +// Output is logfmt-like: key=value pairs separated by spaces. Fields from +// [Entry.Data] are included together with the standard fields derived from the +// entry. If a field conflicts with a standard field, it is prefixed with +// "fields.". Standard field names can be customized through FieldMap. +// +// Field keys are written as-is (unquoted and unescaped) in the plain +// (non-colored) format; only field values may be quoted depending on +// DisableQuote, ForceQuote, QuoteEmptyFields, and the value content. +// +// When colors are enabled, ANSI escape sequences may be added for presentation. +// For fully escaped structured output (including safe keys), use JSONFormatter. type TextFormatter struct { // Set to true to bypass checking for a TTY before outputting colors. ForceColors bool @@ -64,7 +66,7 @@ type TextFormatter struct { // be desired. DisableSorting bool - // The keys sorting function, when uninitialized it uses sort.Strings. + // The keys sorting function, when uninitialized it uses slices.Sort. SortingFunc func([]string) // Disables the truncation of the level text to 4 characters. @@ -77,16 +79,22 @@ type TextFormatter struct { // QuoteEmptyFields will wrap empty fields in quotes if true QuoteEmptyFields bool - // Whether the logger's out is to a terminal - isTerminal bool + // Whether the logger's out is to a terminal. Don't use this field + // directly; use TextFormatter.isTerminal instead. + terminal bool // FieldMap allows users to customize the names of keys for default fields. + // Mapped keys are written as-is, so they should be safe for plain-text output. + // // As an example: + // // formatter := &TextFormatter{ - // FieldMap: FieldMap{ - // FieldKeyTime: "@timestamp", - // FieldKeyLevel: "@level", - // FieldKeyMsg: "@message"}} + // FieldMap: FieldMap{ + // FieldKeyTime: "@timestamp", + // FieldKeyLevel: "@level", + // FieldKeyMsg: "@message", + // }, + // } FieldMap FieldMap // CallerPrettyfier can be set by the user to modify the content @@ -96,54 +104,78 @@ type TextFormatter struct { CallerPrettyfier func(*runtime.Frame) (function string, file string) terminalInitOnce sync.Once - - // The max length of the level text, generated dynamically on init - levelTextMaxLength int } -func (f *TextFormatter) init(entry *Entry) { - if entry.Logger != nil { - f.isTerminal = checkIfTerminal(entry.Logger.Out) - } - // Get the max length of the level text - for _, level := range AllLevels { - levelTextLength := utf8.RuneCount([]byte(level.String())) - if levelTextLength > f.levelTextMaxLength { - f.levelTextMaxLength = levelTextLength - } +func (f *TextFormatter) isTerminal(entry *Entry) bool { + if entry == nil || entry.Logger == nil { + // Don't run the terminalInitOnce without a logger, otherwise we'd + // cache the default (false) forever even if a logger is attached + // later. + return false } + + f.terminalInitOnce.Do(func() { + entry.Logger.mu.Lock() + out := entry.Logger.Out + entry.Logger.mu.Unlock() + + f.terminal = checkIfTerminal(out) + }) + + return f.terminal } -func (f *TextFormatter) isColored() bool { - isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows")) - - if f.EnvironmentOverrideColors { - switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); { - case ok && force != "0": - isColored = true - case ok && force == "0", os.Getenv("CLICOLOR") == "0": - isColored = false - } +func (f *TextFormatter) isColored(isTerminal bool) bool { + if f.DisableColors { + return false } - return isColored && !f.DisableColors + colored := f.ForceColors || isTerminal + if !f.EnvironmentOverrideColors { + return colored + } + if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok { + return force != "0" + } + if os.Getenv("CLICOLOR") == "0" { + return false + } + return colored } // Format renders a single log entry func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { - data := make(Fields) - for k, v := range entry.Data { - data[k] = v - } - prefixFieldClashes(data, f.FieldMap, entry.HasCaller()) + data := make(Fields, len(entry.Data)) + maps.Copy(data, entry.Data) + isColored := f.isColored(f.isTerminal(entry)) + + caller := entry.Caller + hasCaller := caller != nil + prefixFieldClashes(data, f.FieldMap, hasCaller) keys := make([]string, 0, len(data)) for k := range data { keys = append(keys, k) } - var funcVal, fileVal string + b := entry.Buffer + if b == nil { + b = new(bytes.Buffer) + } - fixedKeys := make([]string, 0, 4+len(data)) + if isColored { + f.printColored(b, entry, keys, data) + } else { + f.printPlain(b, entry, keys, data) + } + + return b.Bytes(), nil +} + +func (f *TextFormatter) printPlain(b *bytes.Buffer, entry *Entry, keys []string, data Fields) { + caller := entry.Caller + hasCaller := caller != nil + + fixedKeys := make([]string, 0, len(keys)+defaultFields) if !f.DisableTimestamp { fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime)) } @@ -154,12 +186,14 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { if entry.err != "" { fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError)) } - if entry.HasCaller() { + + var funcVal, fileVal string + if caller != nil { if f.CallerPrettyfier != nil { - funcVal, fileVal = f.CallerPrettyfier(entry.Caller) + funcVal, fileVal = f.CallerPrettyfier(caller) } else { - funcVal = entry.Caller.Function - fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) + funcVal = caller.Function + fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10) } if funcVal != "" { @@ -172,152 +206,108 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { if !f.DisableSorting { if f.SortingFunc == nil { - sort.Strings(keys) + // Default sorting does not sort the "fixed keys"; + // see https://github.com/sirupsen/logrus/commit/73bc94e60c753099e8bae902f81fbd6e7dd95f26 + slices.Sort(keys) fixedKeys = append(fixedKeys, keys...) } else { - if !f.isColored() { - fixedKeys = append(fixedKeys, keys...) - f.SortingFunc(fixedKeys) - } else { - f.SortingFunc(keys) - } + fixedKeys = append(fixedKeys, keys...) + f.SortingFunc(fixedKeys) } } else { fixedKeys = append(fixedKeys, keys...) } - var b *bytes.Buffer - if entry.Buffer != nil { - b = entry.Buffer - } else { - b = &bytes.Buffer{} - } - - f.terminalInitOnce.Do(func() { f.init(entry) }) - - timestampFormat := f.TimestampFormat - if timestampFormat == "" { - timestampFormat = defaultTimestampFormat - } - if f.isColored() { - f.printColored(b, entry, keys, data, timestampFormat) - } else { - - for _, key := range fixedKeys { - var value interface{} - switch { - case key == f.FieldMap.resolve(FieldKeyTime): - value = entry.Time.Format(timestampFormat) - case key == f.FieldMap.resolve(FieldKeyLevel): - value = entry.Level.String() - case key == f.FieldMap.resolve(FieldKeyMsg): - value = entry.Message - case key == f.FieldMap.resolve(FieldKeyLogrusError): - value = entry.err - case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller(): - value = funcVal - case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller(): - value = fileVal - default: - value = data[key] + for _, key := range fixedKeys { + var value any + switch { + case key == f.FieldMap.resolve(FieldKeyTime): + if f.TimestampFormat == "" { + value = entry.Time.Format(defaultTimestampFormat) + } else { + value = entry.Time.Format(f.TimestampFormat) } - f.appendKeyValue(b, key, value) + case key == f.FieldMap.resolve(FieldKeyLevel): + value = entry.Level.String() + case key == f.FieldMap.resolve(FieldKeyMsg): + value = entry.Message + case key == f.FieldMap.resolve(FieldKeyLogrusError): + value = entry.err + case key == f.FieldMap.resolve(FieldKeyFunc) && hasCaller: + value = funcVal + case key == f.FieldMap.resolve(FieldKeyFile) && hasCaller: + value = fileVal + default: + value = data[key] } + f.appendKeyValue(b, key, value) } b.WriteByte('\n') - return b.Bytes(), nil } -func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) { - var levelColor int - switch entry.Level { - case DebugLevel, TraceLevel: - levelColor = gray - case WarnLevel: - levelColor = yellow - case ErrorLevel, FatalLevel, PanicLevel: - levelColor = red - case InfoLevel: - levelColor = blue - default: - levelColor = blue - } - - levelText := strings.ToUpper(entry.Level.String()) - if !f.DisableLevelTruncation && !f.PadLevelText { - levelText = levelText[0:4] - } - if f.PadLevelText { - // Generates the format string used in the next line, for example "%-6s" or "%-7s". - // Based on the max level text length. - formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s" - // Formats the level text by appending spaces up to the max length, for example: - // - "INFO " - // - "WARNING" - levelText = fmt.Sprintf(formatString, levelText) - } - +func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields) { // Remove a single newline if it already exists in the message to keep // the behavior of logrus text_formatter the same as the stdlib log package entry.Message = strings.TrimSuffix(entry.Message, "\n") - caller := "" - if entry.HasCaller() { - funcVal := fmt.Sprintf("%s()", entry.Caller.Function) - fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) - + var callerText string + if caller := entry.Caller; caller != nil { + var funcVal, fileVal string if f.CallerPrettyfier != nil { - funcVal, fileVal = f.CallerPrettyfier(entry.Caller) + funcVal, fileVal = f.CallerPrettyfier(caller) + } else { + if caller.Function != "" { + funcVal = caller.Function + "()" + } + fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10) } if fileVal == "" { - caller = funcVal + callerText = funcVal } else if funcVal == "" { - caller = fileVal + callerText = fileVal } else { - caller = fileVal + " " + funcVal + callerText = fileVal + " " + funcVal } } + levelText := levelPrefix(entry.Level, f.DisableLevelTruncation, f.PadLevelText) switch { case f.DisableTimestamp: - fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message) + _, _ = fmt.Fprintf(b, "%s%s %-44s ", levelText, callerText, entry.Message) case !f.FullTimestamp: - fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message) + _, _ = fmt.Fprintf(b, "%s[%04d]%s %-44s ", levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), callerText, entry.Message) default: - fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message) + timestampFormat := f.TimestampFormat + if timestampFormat == "" { + timestampFormat = defaultTimestampFormat + } + _, _ = fmt.Fprintf(b, "%s[%s]%s %-44s ", levelText, entry.Time.Format(timestampFormat), callerText, entry.Message) } - for _, k := range keys { - v := data[k] - fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k) - f.appendValue(b, v) - } -} -func (f *TextFormatter) needsQuoting(text string) bool { - if f.ForceQuote { - return true - } - if f.QuoteEmptyFields && len(text) == 0 { - return true - } - if f.DisableQuote { - return false - } - for _, ch := range text { - //nolint:staticcheck // QF1001: could apply De Morgan's law - if !((ch >= 'a' && ch <= 'z') || - (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch <= '9') || - ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') { - return true + if !f.DisableSorting { + if f.SortingFunc == nil { + slices.Sort(keys) + } else { + f.SortingFunc(keys) } } - return false + + // Keys use the same color as the level-prefix. + for _, k := range keys { + b.WriteByte(' ') + b.WriteString(colorize(entry.Level, k)) + b.WriteByte('=') + f.appendValue(b, data[k]) + } + + b.WriteByte('\n') } -func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) { +// appendKeyValue writes key=value. Keys are written verbatim (unquoted/unescaped); +// values are subject to quoting/escaping. +func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value any) { if b.Len() > 0 { b.WriteByte(' ') } @@ -326,15 +316,168 @@ func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interf f.appendValue(b, value) } -func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) { - stringVal, ok := value.(string) - if !ok { - stringVal = fmt.Sprint(value) +func (f *TextFormatter) appendValue(b *bytes.Buffer, value any) { + // Fast paths. + switch v := value.(type) { + case string: + f.appendString(b, v) + return + case []byte: + f.appendBytes(b, v) + return + case bool: + var raw [8]byte + f.appendBytes(b, strconv.AppendBool(raw[:0], v)) + return + case error: + f.appendError(b, v) + return + case fmt.Stringer: + f.appendStringer(b, v) + return } - if !f.needsQuoting(stringVal) { - b.WriteString(stringVal) - } else { - fmt.Fprintf(b, "%q", stringVal) + // Handle common primitives. + var raw [64]byte + var num []byte + + switch v := value.(type) { + case int: + num = strconv.AppendInt(raw[:0], int64(v), 10) + case int8: + num = strconv.AppendInt(raw[:0], int64(v), 10) + case int16: + num = strconv.AppendInt(raw[:0], int64(v), 10) + case int32: + num = strconv.AppendInt(raw[:0], int64(v), 10) + case int64: + num = strconv.AppendInt(raw[:0], v, 10) + + case uint: + num = strconv.AppendUint(raw[:0], uint64(v), 10) + case uint8: + num = strconv.AppendUint(raw[:0], uint64(v), 10) + case uint16: + num = strconv.AppendUint(raw[:0], uint64(v), 10) + case uint32: + num = strconv.AppendUint(raw[:0], uint64(v), 10) + case uint64: + num = strconv.AppendUint(raw[:0], v, 10) + case uintptr: + num = strconv.AppendUint(raw[:0], uint64(v), 10) + + case float32: + num = strconv.AppendFloat(raw[:0], float64(v), 'g', -1, 32) + case float64: + num = strconv.AppendFloat(raw[:0], v, 'g', -1, 64) + + default: + f.appendString(b, fmt.Sprint(value)) + return + } + + f.appendNumeric(b, num) +} + +func (f *TextFormatter) appendString(b *bytes.Buffer, s string) { + quote := f.ForceQuote || (f.QuoteEmptyFields && len(s) == 0) || (!f.DisableQuote && needsQuoting(s)) + if !quote { + b.WriteString(s) + return + } + if len(s) == 0 { + b.WriteString(`""`) + return + } + + var tmp [128]byte + b.Write(strconv.AppendQuote(tmp[:0], s)) +} + +func (f *TextFormatter) appendBytes(b *bytes.Buffer, bs []byte) { + quote := f.ForceQuote || (f.QuoteEmptyFields && len(bs) == 0) || (!f.DisableQuote && needsQuotingBytes(bs)) + if !quote { + b.Write(bs) + return + } + if len(bs) == 0 { + b.WriteString(`""`) + return + } + + var tmp [128]byte + b.Write(strconv.AppendQuote(tmp[:0], string(bs))) +} + +func (f *TextFormatter) appendNumeric(b *bytes.Buffer, out []byte) { + if f.ForceQuote { + var tmp [128]byte + b.Write(strconv.AppendQuote(tmp[:0], string(out))) + return + } + b.Write(out) +} + +func (f *TextFormatter) appendError(b *bytes.Buffer, v error) { + defer f.recoverValue(b, v, "Error") + + f.appendString(b, v.Error()) +} + +func (f *TextFormatter) appendStringer(b *bytes.Buffer, v fmt.Stringer) { + defer f.recoverValue(b, v, "String") + + f.appendString(b, v.String()) +} + +func (f *TextFormatter) recoverValue(b *bytes.Buffer, v any, method string) { + if r := recover(); r != nil { + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Pointer && rv.IsNil() { + f.appendString(b, "") + } else { + f.appendString(b, fmt.Sprintf("%%!v(PANIC=%s method: %v)", method, r)) + } + } +} + +// needsQuoting returns true if the string contains any byte that +// requires quoting. It returns false when every byte is "safe" according +// to isSafeByte. +func needsQuoting(s string) bool { + // use an index loop (avoid rune decoding). + for i := range len(s) { + c := s[i] + if !isSafeByte(c) { + return true + } + } + return false +} + +// needsQuotingBytes returns true if the byte slice contains any byte that +// requires quoting. It returns false when every byte is "safe" according +// to isSafeByte. +func needsQuotingBytes(bs []byte) bool { + for _, c := range bs { + if !isSafeByte(c) { + return true + } + } + return false +} + +// isSafeByte returns true if the byte is allowed unquoted (ASCII and in the allowlist). +// It purposely uses byte arithmetic (no runes) for performance. +func isSafeByte(ch byte) bool { + ok := ch < 0x80 && ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')) + if ok { + return true + } + switch ch { + case '-', '.', '_', '/', '@', '^', '+': + return true + default: + return false } } diff --git a/vendor/github.com/sirupsen/logrus/writer.go b/vendor/github.com/sirupsen/logrus/writer.go index 074fd4b8b..30e34cda7 100644 --- a/vendor/github.com/sirupsen/logrus/writer.go +++ b/vendor/github.com/sirupsen/logrus/writer.go @@ -30,7 +30,7 @@ func (entry *Entry) Writer() *io.PipeWriter { func (entry *Entry) WriterLevel(level Level) *io.PipeWriter { reader, writer := io.Pipe() - var printFunc func(args ...interface{}) + printFunc := entry.Print // Determine which log function to use based on the specified log level switch level { @@ -48,8 +48,6 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter { printFunc = entry.Fatal case PanicLevel: printFunc = entry.Panic - default: - printFunc = entry.Print } // Start a new goroutine to scan the input and write it to the logger using the specified print function. @@ -63,7 +61,7 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter { } // writerScanner scans the input from the reader and writes it to the logger -func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) { +func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...any)) { scanner := bufio.NewScanner(reader) // Set the buffer size to the maximum token size to avoid buffer overflows diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go index c592f6ad5..a19a89279 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_format.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go @@ -84,7 +84,7 @@ func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, ar return Equal(t, expected, actual, append([]interface{}{msg}, args...)...) } -// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// EqualErrorf asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -124,7 +124,7 @@ func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg stri return EqualValues(t, expected, actual, append([]interface{}{msg}, args...)...) } -// Errorf asserts that a function returned an error (i.e. not `nil`). +// Errorf asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // assert.Errorf(t, err, "error message %s", "formatted") @@ -144,8 +144,8 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...) } -// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContainsf asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") @@ -190,10 +190,10 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick // time.Sleep(8*time.Second) // externalValue = true // }() -// assert.EventuallyWithTf(t, func(c *assert.CollectT, "error message %s", "formatted") { +// assert.EventuallyWithTf(t, func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "error message %s", "formatted") func EventuallyWithTf(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -552,7 +552,7 @@ func NoDirExistsf(t TestingT, path string, msg string, args ...interface{}) bool return NoDirExists(t, path, append([]interface{}{msg}, args...)...) } -// NoErrorf asserts that a function returned no error (i.e. `nil`). +// NoErrorf asserts that a function returned a nil error (ie. no error). // // actualObj, err := SomeFunction() // if assert.NoErrorf(t, err, "error message %s", "formatted") { @@ -849,7 +849,19 @@ func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, return WithinRange(t, actual, start, end, append([]interface{}{msg}, args...)...) } -// YAMLEqf asserts that two YAML strings are equivalent. +// YAMLEqf asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// assert.YAMLEqf(t, expected, actual, "error message %s", "formatted") func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go index 58db92845..cd2a86061 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_forward.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -146,7 +146,7 @@ func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs return Equal(a.t, expected, actual, msgAndArgs...) } -// EqualError asserts that a function returned an error (i.e. not `nil`) +// EqualError asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -158,7 +158,7 @@ func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ... return EqualError(a.t, theError, errString, msgAndArgs...) } -// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// EqualErrorf asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -240,7 +240,7 @@ func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string return Equalf(a.t, expected, actual, msg, args...) } -// Error asserts that a function returned an error (i.e. not `nil`). +// Error asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // a.Error(err) @@ -269,8 +269,8 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args .. return ErrorAsf(a.t, err, target, msg, args...) } -// ErrorContains asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContains asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // a.ErrorContains(err, expectedErrorSubString) @@ -281,8 +281,8 @@ func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs . return ErrorContains(a.t, theError, contains, msgAndArgs...) } -// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContainsf asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") @@ -311,7 +311,7 @@ func (a *Assertions) ErrorIsf(err error, target error, msg string, args ...inter return ErrorIsf(a.t, err, target, msg, args...) } -// Errorf asserts that a function returned an error (i.e. not `nil`). +// Errorf asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // a.Errorf(err, "error message %s", "formatted") @@ -372,10 +372,10 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor // time.Sleep(8*time.Second) // externalValue = true // }() -// a.EventuallyWithTf(func(c *assert.CollectT, "error message %s", "formatted") { +// a.EventuallyWithTf(func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "error message %s", "formatted") func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1096,7 +1096,7 @@ func (a *Assertions) NoDirExistsf(path string, msg string, args ...interface{}) return NoDirExistsf(a.t, path, msg, args...) } -// NoError asserts that a function returned no error (i.e. `nil`). +// NoError asserts that a function returned a nil error (ie. no error). // // actualObj, err := SomeFunction() // if a.NoError(err) { @@ -1109,7 +1109,7 @@ func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) bool { return NoError(a.t, err, msgAndArgs...) } -// NoErrorf asserts that a function returned no error (i.e. `nil`). +// NoErrorf asserts that a function returned a nil error (ie. no error). // // actualObj, err := SomeFunction() // if a.NoErrorf(err, "error message %s", "formatted") { @@ -1690,7 +1690,19 @@ func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Ti return WithinRangef(a.t, actual, start, end, msg, args...) } -// YAMLEq asserts that two YAML strings are equivalent. +// YAMLEq asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// a.YAMLEq(expected, actual) func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1698,7 +1710,19 @@ func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interf return YAMLEq(a.t, expected, actual, msgAndArgs...) } -// YAMLEqf asserts that two YAML strings are equivalent. +// YAMLEqf asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// a.YAMLEqf(expected, actual, "error message %s", "formatted") func (a *Assertions) YAMLEqf(expected string, actual string, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() diff --git a/vendor/github.com/stretchr/testify/assert/assertion_order.go b/vendor/github.com/stretchr/testify/assert/assertion_order.go index 2fdf80fdd..a44b40ed3 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_order.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_order.go @@ -9,7 +9,7 @@ import ( func isOrdered(t TestingT, object interface{}, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...interface{}) bool { objKind := reflect.TypeOf(object).Kind() if objKind != reflect.Slice && objKind != reflect.Array { - return false + return Fail(t, fmt.Sprintf("object %T is not an ordered collection", object), msgAndArgs...) } objValue := reflect.ValueOf(object) @@ -50,6 +50,9 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []compareR // assert.IsIncreasing(t, []float{1, 2}) // assert.IsIncreasing(t, []string{"a", "b"}) func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } return isOrdered(t, object, []compareResult{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } @@ -59,6 +62,9 @@ func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonIncreasing(t, []float{2, 1}) // assert.IsNonIncreasing(t, []string{"b", "a"}) func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } return isOrdered(t, object, []compareResult{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } @@ -68,6 +74,9 @@ func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) // assert.IsDecreasing(t, []float{2, 1}) // assert.IsDecreasing(t, []string{"b", "a"}) func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } return isOrdered(t, object, []compareResult{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } @@ -77,5 +86,8 @@ func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonDecreasing(t, []float{1, 2}) // assert.IsNonDecreasing(t, []string{"a", "b"}) func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } return isOrdered(t, object, []compareResult{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go index de8de0cb6..166f63726 100644 --- a/vendor/github.com/stretchr/testify/assert/assertions.go +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -17,11 +17,10 @@ import ( "unicode" "unicode/utf8" - "github.com/davecgh/go-spew/spew" - "github.com/pmezard/go-difflib/difflib" - - // Wrapper around gopkg.in/yaml.v3 + // Wrapper around go.yaml.in/yaml/v3 "github.com/stretchr/testify/assert/yaml" + "github.com/stretchr/testify/internal/difflib" + "github.com/stretchr/testify/internal/spew" ) //go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=assert -template=assertion_format.go.tmpl" @@ -33,19 +32,19 @@ type TestingT interface { // ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful // for table driven tests. -type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) bool +type ComparisonAssertionFunc = func(TestingT, interface{}, interface{}, ...interface{}) bool // ValueAssertionFunc is a common function prototype when validating a single value. Can be useful // for table driven tests. -type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) bool +type ValueAssertionFunc = func(TestingT, interface{}, ...interface{}) bool // BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful // for table driven tests. -type BoolAssertionFunc func(TestingT, bool, ...interface{}) bool +type BoolAssertionFunc = func(TestingT, bool, ...interface{}) bool // ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful // for table driven tests. -type ErrorAssertionFunc func(TestingT, error, ...interface{}) bool +type ErrorAssertionFunc = func(TestingT, error, ...interface{}) bool // PanicAssertionFunc is a common function prototype when validating a panic value. Can be useful // for table driven tests. @@ -325,13 +324,15 @@ func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { func indentMessageLines(message string, longestLabelLen int) string { outBuf := new(bytes.Buffer) - for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ { - // no need to align first line because it starts at the correct location (after the label) - if i != 0 { - // append alignLen+1 spaces to align with "{{longestLabel}}:" before adding tab - outBuf.WriteString("\n\t" + strings.Repeat(" ", longestLabelLen+1) + "\t") + scanner := bufio.NewScanner(strings.NewReader(message)) + for firstLine := true; scanner.Scan(); firstLine = false { + if !firstLine { + fmt.Fprint(outBuf, "\n\t"+strings.Repeat(" ", longestLabelLen+1)+"\t") } - outBuf.WriteString(scanner.Text()) + fmt.Fprint(outBuf, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return fmt.Sprintf("cannot display message: %s", err) } return outBuf.String() @@ -544,9 +545,8 @@ func Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) b if !same { // both are pointers but not the same type & pointing to the same address return Fail(t, fmt.Sprintf("Not same: \n"+ - "expected: %p %#[1]v\n"+ - "actual : %p %#[2]v", - expected, actual), msgAndArgs...) + "expected: %[2]s (%[1]T)(%[1]p)\n"+ + "actual : %[4]s (%[3]T)(%[3]p)", expected, truncatingFormat("%#v", expected), actual, truncatingFormat("%#v", actual)), msgAndArgs...) } return true @@ -571,8 +571,8 @@ func NotSame(t TestingT, expected, actual interface{}, msgAndArgs ...interface{} if same { return Fail(t, fmt.Sprintf( - "Expected and actual point to the same object: %p %#[1]v", - expected), msgAndArgs...) + "Expected and actual point to the same object: %p %s", + expected, truncatingFormat("%#v", expected)), msgAndArgs...) } return true } @@ -604,25 +604,26 @@ func samePointers(first, second interface{}) (same bool, ok bool) { // to a type conversion in the Go grammar. func formatUnequalValues(expected, actual interface{}) (e string, a string) { if reflect.TypeOf(expected) != reflect.TypeOf(actual) { - return fmt.Sprintf("%T(%s)", expected, truncatingFormat(expected)), - fmt.Sprintf("%T(%s)", actual, truncatingFormat(actual)) + return fmt.Sprintf("%T(%s)", expected, truncatingFormat("%#v", expected)), + fmt.Sprintf("%T(%s)", actual, truncatingFormat("%#v", actual)) } switch expected.(type) { case time.Duration: return fmt.Sprintf("%v", expected), fmt.Sprintf("%v", actual) } - return truncatingFormat(expected), truncatingFormat(actual) + return truncatingFormat("%#v", expected), truncatingFormat("%#v", actual) } // truncatingFormat formats the data and truncates it if it's too long. // // This helps keep formatted error messages lines from exceeding the // bufio.MaxScanTokenSize max line length that the go testing framework imposes. -func truncatingFormat(data interface{}) string { - value := fmt.Sprintf("%#v", data) - max := bufio.MaxScanTokenSize - 100 // Give us some space the type info too if needed. - if len(value) > max { - value = value[0:max] + "<... truncated>" +func truncatingFormat(format string, data interface{}) string { + value := fmt.Sprintf(format, data) + // Give us space for two truncated objects and the surrounding sentence. + maxMessageSize := bufio.MaxScanTokenSize/2 - 100 + if len(value) > maxMessageSize { + value = value[0:maxMessageSize] + "<... truncated>" } return value } @@ -743,7 +744,7 @@ func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - return Fail(t, fmt.Sprintf("Expected nil, but got: %#v", object), msgAndArgs...) + return Fail(t, fmt.Sprintf("Expected nil, but got: %s", truncatingFormat("%#v", object)), msgAndArgs...) } // isEmpty gets whether the specified object is considered empty or not. @@ -793,7 +794,7 @@ func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...) + Fail(t, fmt.Sprintf("Should be empty, but was %s", truncatingFormat("%v", object)), msgAndArgs...) } return pass @@ -836,11 +837,11 @@ func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) } l, ok := getLen(object) if !ok { - return Fail(t, fmt.Sprintf("\"%v\" could not be applied builtin len()", object), msgAndArgs...) + return Fail(t, fmt.Sprintf("%q could not be applied builtin len()", truncatingFormat("%v", object)), msgAndArgs...) } if l != length { - return Fail(t, fmt.Sprintf("\"%v\" should have %d item(s), but has %d", object, length, l), msgAndArgs...) + return Fail(t, fmt.Sprintf("%q should have %d item(s), but has %d", truncatingFormat("%v", object), length, l), msgAndArgs...) } return true } @@ -889,7 +890,7 @@ func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{ } if ObjectsAreEqual(expected, actual) { - return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...) + return Fail(t, fmt.Sprintf("Should not be: %s\n", truncatingFormat("%#v", actual)), msgAndArgs...) } return true @@ -904,7 +905,7 @@ func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...inte } if ObjectsAreEqualValues(expected, actual) { - return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...) + return Fail(t, fmt.Sprintf("Should not be: %s\n", truncatingFormat("%#v", actual)), msgAndArgs...) } return true @@ -964,10 +965,10 @@ func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bo ok, found := containsElement(s, contains) if !ok { - return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s could not be applied builtin len()", truncatingFormat("%#v", s)), msgAndArgs...) } if !found { - return Fail(t, fmt.Sprintf("%#v does not contain %#v", s, contains), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s does not contain %#v", truncatingFormat("%#v", s), contains), msgAndArgs...) } return true @@ -986,10 +987,10 @@ func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) ok, found := containsElement(s, contains) if !ok { - return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s could not be applied builtin len()", truncatingFormat("%#v", s)), msgAndArgs...) } if found { - return Fail(t, fmt.Sprintf("%#v should not contain %#v", s, contains), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s should not contain %#v", truncatingFormat("%#v", s), contains), msgAndArgs...) } return true @@ -1031,10 +1032,10 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok av := actualMap.MapIndex(k) if !av.IsValid() { - return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, subset), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s does not contain %s", truncatingFormat("%#v", list), truncatingFormat("%#v", subset)), msgAndArgs...) } if !ObjectsAreEqual(ev.Interface(), av.Interface()) { - return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, subset), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s does not contain %s", truncatingFormat("%#v", list), truncatingFormat("%#v", subset)), msgAndArgs...) } } @@ -1056,7 +1057,7 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", list), msgAndArgs...) } if !found { - return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, element), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s does not contain %#v", truncatingFormat("%#v", list), element), msgAndArgs...) } } @@ -1082,12 +1083,12 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) listKind := reflect.TypeOf(list).Kind() if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { - return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", list, listKind), msgAndArgs...) } subsetKind := reflect.TypeOf(subset).Kind() if subsetKind != reflect.Array && subsetKind != reflect.Slice && subsetKind != reflect.Map { - return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", subset, subsetKind), msgAndArgs...) } if subsetKind == reflect.Map && listKind == reflect.Map { @@ -1106,7 +1107,7 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) } } - return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s is a subset of %s", truncatingFormat("%#v", subset), truncatingFormat("%#v", list)), msgAndArgs...) } subsetList := reflect.ValueOf(subset) @@ -1121,14 +1122,14 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) element := subsetList.Index(i).Interface() ok, found := containsElement(list, element) if !ok { - return Fail(t, fmt.Sprintf("%q could not be applied builtin len()", list), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", list), msgAndArgs...) } if !found { return true } } - return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s is a subset of %s", truncatingFormat("%#v", subset), truncatingFormat("%#v", list)), msgAndArgs...) } // ElementsMatch asserts that the specified listA(array, slice...) is equal to specified @@ -1343,9 +1344,15 @@ func PanicsWithError(t TestingT, errString string, f PanicTestFunc, msgAndArgs . if !funcDidPanic { return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...) } - panicErr, ok := panicValue.(error) - if !ok || panicErr.Error() != errString { - return Fail(t, fmt.Sprintf("func %#v should panic with error message:\t%#v\n\tPanic value:\t%#v\n\tPanic stack:\t%s", f, errString, panicValue, panickedStack), msgAndArgs...) + panicErr, isError := panicValue.(error) + if !isError || panicErr.Error() != errString { + msg := fmt.Sprintf("func %#v should panic with error message:\t%#v\n", f, errString) + if isError { + msg += fmt.Sprintf("\tError message:\t%#v\n", panicErr.Error()) + } + msg += fmt.Sprintf("\tPanic value:\t%#v\n", panicValue) + msg += fmt.Sprintf("\tPanic stack:\t%s\n", panickedStack) + return Fail(t, msg, msgAndArgs...) } return true @@ -1624,7 +1631,7 @@ func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, m Errors */ -// NoError asserts that a function returned no error (i.e. `nil`). +// NoError asserts that a function returned a nil error (ie. no error). // // actualObj, err := SomeFunction() // if assert.NoError(t, err) { @@ -1635,13 +1642,13 @@ func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - return Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err), msgAndArgs...) + return Fail(t, fmt.Sprintf("Received unexpected error:\n%s", truncatingFormat("%+v", err)), msgAndArgs...) } return true } -// Error asserts that a function returned an error (i.e. not `nil`). +// Error asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // assert.Error(t, err) @@ -1656,7 +1663,7 @@ func Error(t TestingT, err error, msgAndArgs ...interface{}) bool { return true } -// EqualError asserts that a function returned an error (i.e. not `nil`) +// EqualError asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -1674,13 +1681,13 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte if expected != actual { return Fail(t, fmt.Sprintf("Error message not equal:\n"+ "expected: %q\n"+ - "actual : %q", expected, actual), msgAndArgs...) + "actual : %s", expected, truncatingFormat("%q", actual)), msgAndArgs...) } return true } -// ErrorContains asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContains asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // assert.ErrorContains(t, err, expectedErrorSubString) @@ -1694,7 +1701,7 @@ func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...in actual := theError.Error() if !strings.Contains(actual, contains) { - return Fail(t, fmt.Sprintf("Error %#v does not contain %#v", actual, contains), msgAndArgs...) + return Fail(t, fmt.Sprintf("Error %s does not contain %#v", truncatingFormat("%#v", actual), contains), msgAndArgs...) } return true @@ -1760,7 +1767,7 @@ func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { h.Helper() } if i != nil && !reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { - return Fail(t, fmt.Sprintf("Should be zero, but was %v", i), msgAndArgs...) + return Fail(t, fmt.Sprintf("Should be zero, but was %s", truncatingFormat("%v", i)), msgAndArgs...) } return true } @@ -1874,7 +1881,19 @@ func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{ return Equal(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...) } -// YAMLEq asserts that two YAML strings are equivalent. +// YAMLEq asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// assert.YAMLEq(t, expected, actual) func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -2188,8 +2207,8 @@ func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { chain := buildErrorChainString(err, false) return Fail(t, fmt.Sprintf("Target error should be in err chain:\n"+ - "expected: %q\n"+ - "in chain: %s", expectedText, chain, + "expected: %s\n"+ + "in chain: %s", truncatingFormat("%q", expectedText), truncatingFormat("%s", chain), ), msgAndArgs...) } @@ -2211,8 +2230,8 @@ func NotErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { chain := buildErrorChainString(err, false) return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ - "found: %q\n"+ - "in chain: %s", expectedText, chain, + "found: %s\n"+ + "in chain: %s", truncatingFormat("%q", expectedText), truncatingFormat("%s", chain), ), msgAndArgs...) } @@ -2236,7 +2255,7 @@ func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{ return Fail(t, fmt.Sprintf("Should be in error chain:\n"+ "expected: %s\n"+ - "in chain: %s", expectedType, chain, + "in chain: %s", expectedType, truncatingFormat("%s", chain), ), msgAndArgs...) } @@ -2254,7 +2273,7 @@ func NotErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interfa return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ "found: %s\n"+ - "in chain: %s", reflect.TypeOf(target).Elem().String(), chain, + "in chain: %s", reflect.TypeOf(target).Elem().String(), truncatingFormat("%s", chain), ), msgAndArgs...) } diff --git a/vendor/github.com/stretchr/testify/assert/doc.go b/vendor/github.com/stretchr/testify/assert/doc.go index a0b953aa5..c111589c7 100644 --- a/vendor/github.com/stretchr/testify/assert/doc.go +++ b/vendor/github.com/stretchr/testify/assert/doc.go @@ -40,8 +40,8 @@ // // # Assertions // -// Assertions allow you to easily write test code, and are global funcs in the `assert` package. -// All assertion functions take, as the first argument, the `*testing.T` object provided by the +// Assertions allow you to easily write test code, and are global funcs in the assert package. +// All assertion functions take, as the first argument, the [*testing.T] object provided by the // testing framework. This allows the assertion funcs to write the failings and other details to // the correct place. // diff --git a/vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go b/vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go index 5a74c4f4d..956227ca2 100644 --- a/vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go +++ b/vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go @@ -7,7 +7,7 @@ // go test -tags testify_yaml_custom // // This implementation can be used at build time to replace the default implementation -// to avoid linking with [gopkg.in/yaml.v3]. +// to avoid linking with [go.yaml.in/yaml/v3]. // // In your test package: // diff --git a/vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go b/vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go index 0bae80e34..dd89ac03a 100644 --- a/vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go +++ b/vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go @@ -6,7 +6,7 @@ // indirection with an alternative implementation of this package that uses // another implementation of YAML deserialization. This allows to not either not // use YAML deserialization at all, or to use another implementation than -// [gopkg.in/yaml.v3] (for example for license compatibility reasons, see [PR #1120]). +// [go.yaml.in/yaml/v3] (for example for license compatibility reasons, see [PR #1120]). // // Alternative implementations are selected using build tags: // @@ -28,9 +28,9 @@ // [PR #1120]: https://github.com/stretchr/testify/pull/1120 package yaml -import goyaml "gopkg.in/yaml.v3" +import goyaml "go.yaml.in/yaml/v3" -// Unmarshal is just a wrapper of [gopkg.in/yaml.v3.Unmarshal]. +// Unmarshal is just a wrapper of [go.yaml.in/yaml/v3.Unmarshal]. func Unmarshal(in []byte, out interface{}) error { return goyaml.Unmarshal(in, out) } diff --git a/vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go b/vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go index 8041803fd..a51d27925 100644 --- a/vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go +++ b/vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go @@ -3,7 +3,7 @@ // Package yaml is an implementation of YAML functions that always fail. // // This implementation can be used at build time to replace the default implementation -// to avoid linking with [gopkg.in/yaml.v3]: +// to avoid linking with [go.yaml.in/yaml/v3]: // // go test -tags testify_yaml_fail package yaml diff --git a/vendor/github.com/pmezard/go-difflib/LICENSE b/vendor/github.com/stretchr/testify/internal/difflib/LICENSE similarity index 95% rename from vendor/github.com/pmezard/go-difflib/LICENSE rename to vendor/github.com/stretchr/testify/internal/difflib/LICENSE index c67dad612..485be13c6 100644 --- a/vendor/github.com/pmezard/go-difflib/LICENSE +++ b/vendor/github.com/stretchr/testify/internal/difflib/LICENSE @@ -24,4 +24,4 @@ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/vendor/github.com/pmezard/go-difflib/difflib/difflib.go b/vendor/github.com/stretchr/testify/internal/difflib/difflib.go similarity index 77% rename from vendor/github.com/pmezard/go-difflib/difflib/difflib.go rename to vendor/github.com/stretchr/testify/internal/difflib/difflib.go index 003e99fad..9984599b4 100644 --- a/vendor/github.com/pmezard/go-difflib/difflib/difflib.go +++ b/vendor/github.com/stretchr/testify/internal/difflib/difflib.go @@ -8,11 +8,14 @@ // // - unified_diff // -// - context_diff -// // Getting unified diffs was the main goal of the port. Keep in mind this code // is mostly suitable to output text differences in a human friendly way, there // are no guarantees generated diffs are consumable by patch(1). +// +// This package was adopted from [github.com/pmezard/go-difflib] which +// is no longer maintained. +// +// [github.com/pmezard/go-difflib]: https://github.com/pmezard/go-difflib package difflib import ( @@ -37,13 +40,6 @@ func max(a, b int) int { return b } -func calculateRatio(matches, length int) float64 { - if length > 0 { - return 2.0 * float64(matches) / float64(length) - } - return 1.0 -} - type Match struct { A int B int @@ -103,14 +99,6 @@ func NewMatcher(a, b []string) *SequenceMatcher { return &m } -func NewMatcherWithJunk(a, b []string, autoJunk bool, - isJunk func(string) bool) *SequenceMatcher { - - m := SequenceMatcher{IsJunk: isJunk, autoJunk: autoJunk} - m.SetSeqs(a, b) - return &m -} - // Set two sequences to be compared. func (m *SequenceMatcher) SetSeqs(a, b []string) { m.SetSeq1(a) @@ -199,12 +187,15 @@ func (m *SequenceMatcher) isBJunk(s string) bool { // If IsJunk is not defined: // // Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where -// alo <= i <= i+k <= ahi -// blo <= j <= j+k <= bhi +// +// alo <= i <= i+k <= ahi +// blo <= j <= j+k <= bhi +// // and for all (i',j',k') meeting those conditions, -// k >= k' -// i <= i' -// and if i == i', j <= j' +// +// k >= k' +// i <= i' +// and if i == i', j <= j' // // In other words, of all maximal matching blocks, return one that // starts earliest in a, and of all those maximal matching blocks that @@ -451,66 +442,6 @@ func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { return groups } -// Return a measure of the sequences' similarity (float in [0,1]). -// -// Where T is the total number of elements in both sequences, and -// M is the number of matches, this is 2.0*M / T. -// Note that this is 1 if the sequences are identical, and 0 if -// they have nothing in common. -// -// .Ratio() is expensive to compute if you haven't already computed -// .GetMatchingBlocks() or .GetOpCodes(), in which case you may -// want to try .QuickRatio() or .RealQuickRation() first to get an -// upper bound. -func (m *SequenceMatcher) Ratio() float64 { - matches := 0 - for _, m := range m.GetMatchingBlocks() { - matches += m.Size - } - return calculateRatio(matches, len(m.a)+len(m.b)) -} - -// Return an upper bound on ratio() relatively quickly. -// -// This isn't defined beyond that it is an upper bound on .Ratio(), and -// is faster to compute. -func (m *SequenceMatcher) QuickRatio() float64 { - // viewing a and b as multisets, set matches to the cardinality - // of their intersection; this counts the number of matches - // without regard to order, so is clearly an upper bound - if m.fullBCount == nil { - m.fullBCount = map[string]int{} - for _, s := range m.b { - m.fullBCount[s] = m.fullBCount[s] + 1 - } - } - - // avail[x] is the number of times x appears in 'b' less the - // number of times we've seen it in 'a' so far ... kinda - avail := map[string]int{} - matches := 0 - for _, s := range m.a { - n, ok := avail[s] - if !ok { - n = m.fullBCount[s] - } - avail[s] = n - 1 - if n > 0 { - matches += 1 - } - } - return calculateRatio(matches, len(m.a)+len(m.b)) -} - -// Return an upper bound on ratio() very quickly. -// -// This isn't defined beyond that it is an upper bound on .Ratio(), and -// is faster to compute than either .Ratio() or .QuickRatio(). -func (m *SequenceMatcher) RealQuickRatio() float64 { - la, lb := len(m.a), len(m.b) - return calculateRatio(min(la, lb), la+lb) -} - // Convert range to the "ed" format func formatRangeUnified(start, stop int) string { // Per the diff spec at http://www.unix.org/single_unix_specification/ @@ -652,117 +583,6 @@ func formatRangeContext(start, stop int) string { return fmt.Sprintf("%d,%d", beginning, beginning+length-1) } -type ContextDiff UnifiedDiff - -// Compare two sequences of lines; generate the delta as a context diff. -// -// Context diffs are a compact way of showing line changes and a few -// lines of context. The number of context lines is set by diff.Context -// which defaults to three. -// -// By default, the diff control lines (those with *** or ---) are -// created with a trailing newline. -// -// For inputs that do not have trailing newlines, set the diff.Eol -// argument to "" so that the output will be uniformly newline free. -// -// The context diff format normally has a header for filenames and -// modification times. Any or all of these may be specified using -// strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate. -// The modification times are normally expressed in the ISO 8601 format. -// If not specified, the strings default to blanks. -func WriteContextDiff(writer io.Writer, diff ContextDiff) error { - buf := bufio.NewWriter(writer) - defer buf.Flush() - var diffErr error - wf := func(format string, args ...interface{}) { - _, err := buf.WriteString(fmt.Sprintf(format, args...)) - if diffErr == nil && err != nil { - diffErr = err - } - } - ws := func(s string) { - _, err := buf.WriteString(s) - if diffErr == nil && err != nil { - diffErr = err - } - } - - if len(diff.Eol) == 0 { - diff.Eol = "\n" - } - - prefix := map[byte]string{ - 'i': "+ ", - 'd': "- ", - 'r': "! ", - 'e': " ", - } - - started := false - m := NewMatcher(diff.A, diff.B) - for _, g := range m.GetGroupedOpCodes(diff.Context) { - if !started { - started = true - fromDate := "" - if len(diff.FromDate) > 0 { - fromDate = "\t" + diff.FromDate - } - toDate := "" - if len(diff.ToDate) > 0 { - toDate = "\t" + diff.ToDate - } - if diff.FromFile != "" || diff.ToFile != "" { - wf("*** %s%s%s", diff.FromFile, fromDate, diff.Eol) - wf("--- %s%s%s", diff.ToFile, toDate, diff.Eol) - } - } - - first, last := g[0], g[len(g)-1] - ws("***************" + diff.Eol) - - range1 := formatRangeContext(first.I1, last.I2) - wf("*** %s ****%s", range1, diff.Eol) - for _, c := range g { - if c.Tag == 'r' || c.Tag == 'd' { - for _, cc := range g { - if cc.Tag == 'i' { - continue - } - for _, line := range diff.A[cc.I1:cc.I2] { - ws(prefix[cc.Tag] + line) - } - } - break - } - } - - range2 := formatRangeContext(first.J1, last.J2) - wf("--- %s ----%s", range2, diff.Eol) - for _, c := range g { - if c.Tag == 'r' || c.Tag == 'i' { - for _, cc := range g { - if cc.Tag == 'd' { - continue - } - for _, line := range diff.B[cc.J1:cc.J2] { - ws(prefix[cc.Tag] + line) - } - } - break - } - } - } - return diffErr -} - -// Like WriteContextDiff but returns the diff a string. -func GetContextDiffString(diff ContextDiff) (string, error) { - w := &bytes.Buffer{} - err := WriteContextDiff(w, diff) - return string(w.Bytes()), err -} - // Split a string on "\n" while preserving them. The output can be used // as input for UnifiedDiff and ContextDiff structures. func SplitLines(s string) []string { diff --git a/vendor/github.com/davecgh/go-spew/LICENSE b/vendor/github.com/stretchr/testify/internal/spew/LICENSE similarity index 100% rename from vendor/github.com/davecgh/go-spew/LICENSE rename to vendor/github.com/stretchr/testify/internal/spew/LICENSE diff --git a/vendor/github.com/stretchr/testify/internal/spew/README.md b/vendor/github.com/stretchr/testify/internal/spew/README.md new file mode 100644 index 000000000..51a909e2e --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/spew/README.md @@ -0,0 +1,12 @@ +go-spew +======= + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) + +Go-spew implements a deep pretty printer for Go data structures to aid in +debugging. A comprehensive suite of tests with 100% test coverage is provided +to ensure proper functionality. + +## License + +Go-spew is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/vendor/github.com/davecgh/go-spew/spew/bypass.go b/vendor/github.com/stretchr/testify/internal/spew/bypass.go similarity index 98% rename from vendor/github.com/davecgh/go-spew/spew/bypass.go rename to vendor/github.com/stretchr/testify/internal/spew/bypass.go index 792994785..70ddeaad3 100644 --- a/vendor/github.com/davecgh/go-spew/spew/bypass.go +++ b/vendor/github.com/stretchr/testify/internal/spew/bypass.go @@ -18,6 +18,7 @@ // tag is deprecated and thus should not be used. // Go versions prior to 1.4 are disabled because they use a different layout // for interfaces which make the implementation of unsafeReflectValue more complex. +//go:build !js && !appengine && !safe && !disableunsafe && go1.4 // +build !js,!appengine,!safe,!disableunsafe,go1.4 package spew diff --git a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go b/vendor/github.com/stretchr/testify/internal/spew/bypasssafe.go similarity index 96% rename from vendor/github.com/davecgh/go-spew/spew/bypasssafe.go rename to vendor/github.com/stretchr/testify/internal/spew/bypasssafe.go index 205c28d68..5e2d890d6 100644 --- a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go +++ b/vendor/github.com/stretchr/testify/internal/spew/bypasssafe.go @@ -16,6 +16,7 @@ // when the code is running on Google App Engine, compiled by GopherJS, or // "-tags safe" is added to the go build command line. The "disableunsafe" // tag is deprecated and thus should not be used. +//go:build js || appengine || safe || disableunsafe || !go1.4 // +build js appengine safe disableunsafe !go1.4 package spew diff --git a/vendor/github.com/davecgh/go-spew/spew/common.go b/vendor/github.com/stretchr/testify/internal/spew/common.go similarity index 100% rename from vendor/github.com/davecgh/go-spew/spew/common.go rename to vendor/github.com/stretchr/testify/internal/spew/common.go diff --git a/vendor/github.com/davecgh/go-spew/spew/config.go b/vendor/github.com/stretchr/testify/internal/spew/config.go similarity index 95% rename from vendor/github.com/davecgh/go-spew/spew/config.go rename to vendor/github.com/stretchr/testify/internal/spew/config.go index 2e3d22f31..161895fc6 100644 --- a/vendor/github.com/davecgh/go-spew/spew/config.go +++ b/vendor/github.com/stretchr/testify/internal/spew/config.go @@ -254,15 +254,15 @@ pointer addresses used to indirect to the final value. It provides the following features over the built-in printing facilities provided by the fmt package: - * Pointers are dereferenced and followed - * Circular data structures are detected and handled properly - * Custom Stringer/error interfaces are optionally invoked, including - on unexported types - * Custom types which only implement the Stringer/error interfaces via - a pointer receiver are optionally invoked when passing non-pointer - variables - * Byte arrays and slices are dumped like the hexdump -C command which - includes offsets, byte values in hex, and ASCII output + - Pointers are dereferenced and followed + - Circular data structures are detected and handled properly + - Custom Stringer/error interfaces are optionally invoked, including + on unexported types + - Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + - Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output The configuration options are controlled by modifying the public members of c. See ConfigState for options documentation. @@ -295,12 +295,12 @@ func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) // NewDefaultConfig returns a ConfigState with the following default settings. // -// Indent: " " -// MaxDepth: 0 -// DisableMethods: false -// DisablePointerMethods: false -// ContinueOnMethod: false -// SortKeys: false +// Indent: " " +// MaxDepth: 0 +// DisableMethods: false +// DisablePointerMethods: false +// ContinueOnMethod: false +// SortKeys: false func NewDefaultConfig() *ConfigState { return &ConfigState{Indent: " "} } diff --git a/vendor/github.com/davecgh/go-spew/spew/doc.go b/vendor/github.com/stretchr/testify/internal/spew/doc.go similarity index 65% rename from vendor/github.com/davecgh/go-spew/spew/doc.go rename to vendor/github.com/stretchr/testify/internal/spew/doc.go index aacaac6f1..722e9aa79 100644 --- a/vendor/github.com/davecgh/go-spew/spew/doc.go +++ b/vendor/github.com/stretchr/testify/internal/spew/doc.go @@ -21,35 +21,36 @@ debugging. A quick overview of the additional features spew provides over the built-in printing facilities for Go data types are as follows: - * Pointers are dereferenced and followed - * Circular data structures are detected and handled properly - * Custom Stringer/error interfaces are optionally invoked, including - on unexported types - * Custom types which only implement the Stringer/error interfaces via - a pointer receiver are optionally invoked when passing non-pointer - variables - * Byte arrays and slices are dumped like the hexdump -C command which - includes offsets, byte values in hex, and ASCII output (only when using - Dump style) + - Pointers are dereferenced and followed + - Circular data structures are detected and handled properly + - Custom Stringer/error interfaces are optionally invoked, including + on unexported types + - Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + - Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output (only when using + Dump style) There are two different approaches spew allows for dumping Go data structures: - * Dump style which prints with newlines, customizable indentation, - and additional debug information such as types and all pointer addresses - used to indirect to the final value - * A custom Formatter interface that integrates cleanly with the standard fmt - package and replaces %v, %+v, %#v, and %#+v to provide inline printing - similar to the default %v while providing the additional functionality - outlined above and passing unsupported format verbs such as %x and %q - along to fmt + - Dump style which prints with newlines, customizable indentation, + and additional debug information such as types and all pointer addresses + used to indirect to the final value + - A custom Formatter interface that integrates cleanly with the standard fmt + package and replaces %v, %+v, %#v, and %#+v to provide inline printing + similar to the default %v while providing the additional functionality + outlined above and passing unsupported format verbs such as %x and %q + along to fmt -Quick Start +# Quick Start This section demonstrates how to quickly get started with spew. See the sections below for further details on formatting and configuration options. To dump a variable with full newlines, indentation, type, and pointer information use Dump, Fdump, or Sdump: + spew.Dump(myVar1, myVar2, ...) spew.Fdump(someWriter, myVar1, myVar2, ...) str := spew.Sdump(myVar1, myVar2, ...) @@ -58,12 +59,13 @@ Alternatively, if you would prefer to use format strings with a compacted inline printing style, use the convenience wrappers Printf, Fprintf, etc with %v (most compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types and pointer addresses): + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) -Configuration Options +# Configuration Options Configuration of spew is handled by fields in the ConfigState type. For convenience, all of the top-level functions use a global state available @@ -74,51 +76,52 @@ equivalent to the top-level functions. This allows concurrent configuration options. See the ConfigState documentation for more details. The following configuration options are available: - * Indent - String to use for each indentation level for Dump functions. - It is a single space by default. A popular alternative is "\t". - * MaxDepth - Maximum number of levels to descend into nested data structures. - There is no limit by default. + - Indent + String to use for each indentation level for Dump functions. + It is a single space by default. A popular alternative is "\t". - * DisableMethods - Disables invocation of error and Stringer interface methods. - Method invocation is enabled by default. + - MaxDepth + Maximum number of levels to descend into nested data structures. + There is no limit by default. - * DisablePointerMethods - Disables invocation of error and Stringer interface methods on types - which only accept pointer receivers from non-pointer variables. - Pointer method invocation is enabled by default. + - DisableMethods + Disables invocation of error and Stringer interface methods. + Method invocation is enabled by default. - * DisablePointerAddresses - DisablePointerAddresses specifies whether to disable the printing of - pointer addresses. This is useful when diffing data structures in tests. + - DisablePointerMethods + Disables invocation of error and Stringer interface methods on types + which only accept pointer receivers from non-pointer variables. + Pointer method invocation is enabled by default. - * DisableCapacities - DisableCapacities specifies whether to disable the printing of - capacities for arrays, slices, maps and channels. This is useful when - diffing data structures in tests. + - DisablePointerAddresses + DisablePointerAddresses specifies whether to disable the printing of + pointer addresses. This is useful when diffing data structures in tests. - * ContinueOnMethod - Enables recursion into types after invoking error and Stringer interface - methods. Recursion after method invocation is disabled by default. + - DisableCapacities + DisableCapacities specifies whether to disable the printing of + capacities for arrays, slices, maps and channels. This is useful when + diffing data structures in tests. - * SortKeys - Specifies map keys should be sorted before being printed. Use - this to have a more deterministic, diffable output. Note that - only native types (bool, int, uint, floats, uintptr and string) - and types which implement error or Stringer interfaces are - supported with other types sorted according to the - reflect.Value.String() output which guarantees display - stability. Natural map order is used by default. + - ContinueOnMethod + Enables recursion into types after invoking error and Stringer interface + methods. Recursion after method invocation is disabled by default. - * SpewKeys - Specifies that, as a last resort attempt, map keys should be - spewed to strings and sorted by those strings. This is only - considered if SortKeys is true. + - SortKeys + Specifies map keys should be sorted before being printed. Use + this to have a more deterministic, diffable output. Note that + only native types (bool, int, uint, floats, uintptr and string) + and types which implement error or Stringer interfaces are + supported with other types sorted according to the + reflect.Value.String() output which guarantees display + stability. Natural map order is used by default. -Dump Usage + - SpewKeys + Specifies that, as a last resort attempt, map keys should be + spewed to strings and sorted by those strings. This is only + considered if SortKeys is true. + +# Dump Usage Simply call spew.Dump with a list of variables you want to dump: @@ -133,7 +136,7 @@ A third option is to call spew.Sdump to get the formatted output as a string: str := spew.Sdump(myVar1, myVar2, ...) -Sample Dump Output +# Sample Dump Output See the Dump example for details on the setup of the types and variables being shown here. @@ -150,13 +153,14 @@ shown here. Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C command as shown. + ([]uint8) (len=32 cap=32) { 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| 00000020 31 32 |12| } -Custom Formatter +# Custom Formatter Spew provides a custom formatter that implements the fmt.Formatter interface so that it integrates cleanly with standard fmt package printing functions. The @@ -170,7 +174,7 @@ standard fmt package for formatting. In addition, the custom formatter ignores the width and precision arguments (however they will still work on the format specifiers not handled by the custom formatter). -Custom Formatter Usage +# Custom Formatter Usage The simplest way to make use of the spew custom formatter is to call one of the convenience functions such as spew.Printf, spew.Println, or spew.Printf. The @@ -184,15 +188,17 @@ functions have syntax you are most likely already familiar with: See the Index for the full list convenience functions. -Sample Formatter Output +# Sample Formatter Output Double pointer to a uint8: + %v: <**>5 %+v: <**>(0xf8400420d0->0xf8400420c8)5 %#v: (**uint8)5 %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 Pointer to circular struct with a uint8 field and a pointer to itself: + %v: <*>{1 <*>} %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)} %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)} @@ -201,7 +207,7 @@ Pointer to circular struct with a uint8 field and a pointer to itself: See the Printf example for details on the setup of variables being shown here. -Errors +# Errors Since it is possible for custom Stringer/error interfaces to panic, spew detects them and handles them internally by printing the panic information diff --git a/vendor/github.com/davecgh/go-spew/spew/dump.go b/vendor/github.com/stretchr/testify/internal/spew/dump.go similarity index 96% rename from vendor/github.com/davecgh/go-spew/spew/dump.go rename to vendor/github.com/stretchr/testify/internal/spew/dump.go index f78d89fc1..8323041a4 100644 --- a/vendor/github.com/davecgh/go-spew/spew/dump.go +++ b/vendor/github.com/stretchr/testify/internal/spew/dump.go @@ -488,15 +488,15 @@ pointer addresses used to indirect to the final value. It provides the following features over the built-in printing facilities provided by the fmt package: - * Pointers are dereferenced and followed - * Circular data structures are detected and handled properly - * Custom Stringer/error interfaces are optionally invoked, including - on unexported types - * Custom types which only implement the Stringer/error interfaces via - a pointer receiver are optionally invoked when passing non-pointer - variables - * Byte arrays and slices are dumped like the hexdump -C command which - includes offsets, byte values in hex, and ASCII output + - Pointers are dereferenced and followed + - Circular data structures are detected and handled properly + - Custom Stringer/error interfaces are optionally invoked, including + on unexported types + - Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + - Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output The configuration options are controlled by an exported package global, spew.Config. See ConfigState for options documentation. diff --git a/vendor/github.com/davecgh/go-spew/spew/format.go b/vendor/github.com/stretchr/testify/internal/spew/format.go similarity index 100% rename from vendor/github.com/davecgh/go-spew/spew/format.go rename to vendor/github.com/stretchr/testify/internal/spew/format.go diff --git a/vendor/github.com/davecgh/go-spew/spew/spew.go b/vendor/github.com/stretchr/testify/internal/spew/spew.go similarity index 100% rename from vendor/github.com/davecgh/go-spew/spew/spew.go rename to vendor/github.com/stretchr/testify/internal/spew/spew.go diff --git a/vendor/github.com/xo/terminfo/dec.go b/vendor/github.com/xo/terminfo/dec.go index dacc88e38..f650c2dd2 100644 --- a/vendor/github.com/xo/terminfo/dec.go +++ b/vendor/github.com/xo/terminfo/dec.go @@ -6,7 +6,7 @@ import ( const ( // maxFileLength is the max file length. - maxFileLength = 4096 + maxFileLength = 32768 // magic is the file magic for terminfo files. magic = 0o432 // magicExtended is the file magic for terminfo files with the extended diff --git a/vendor/go.yaml.in/yaml/v3/LICENSE b/vendor/go.yaml.in/yaml/v3/LICENSE new file mode 100644 index 000000000..2683e4bb1 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/LICENSE @@ -0,0 +1,50 @@ + +This project is covered by two different licenses: MIT and Apache. + +#### MIT License #### + +The following files were ported to Go from C files of libyaml, and thus +are still covered by their original MIT license, with the additional +copyright staring in 2011 when the project was ported over: + + apic.go emitterc.go parserc.go readerc.go scannerc.go + writerc.go yamlh.go yamlprivateh.go + +Copyright (c) 2006-2010 Kirill Simonov +Copyright (c) 2006-2011 Kirill Simonov + +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. + +### Apache License ### + +All the remaining project files are covered by the Apache license: + +Copyright (c) 2011-2019 Canonical Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/go.yaml.in/yaml/v3/NOTICE b/vendor/go.yaml.in/yaml/v3/NOTICE new file mode 100644 index 000000000..866d74a7a --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/NOTICE @@ -0,0 +1,13 @@ +Copyright 2011-2016 Canonical Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/go.yaml.in/yaml/v3/README.md b/vendor/go.yaml.in/yaml/v3/README.md new file mode 100644 index 000000000..15a85a635 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/README.md @@ -0,0 +1,171 @@ +go.yaml.in/yaml +=============== + +YAML Support for the Go Language + + +## Introduction + +The `yaml` package enables [Go](https://go.dev/) programs to comfortably encode +and decode [YAML](https://yaml.org/) values. + +It was originally developed within [Canonical](https://www.canonical.com) as +part of the [juju](https://juju.ubuntu.com) project, and is based on a pure Go +port of the well-known [libyaml](http://pyyaml.org/wiki/LibYAML) C library to +parse and generate YAML data quickly and reliably. + + +## Project Status + +This project started as a fork of the extremely popular [go-yaml]( +https://github.com/go-yaml/yaml/) +project, and is being maintained by the official [YAML organization]( +https://github.com/yaml/). + +The YAML team took over ongoing maintenance and development of the project after +discussion with go-yaml's author, @niemeyer, following his decision to +[label the project repository as "unmaintained"]( +https://github.com/go-yaml/yaml/blob/944c86a7d2/README.md) in April 2025. + +We have put together a team of dedicated maintainers including representatives +of go-yaml's most important downstream projects. + +We will strive to earn the trust of the various go-yaml forks to switch back to +this repository as their upstream. + +Please [contact us](https://cloud-native.slack.com/archives/C08PPAT8PS7) if you +would like to contribute or be involved. + + +## Compatibility + +The `yaml` package supports most of YAML 1.2, but preserves some behavior from +1.1 for backwards compatibility. + +Specifically, v3 of the `yaml` package: + +* Supports YAML 1.1 bools (`yes`/`no`, `on`/`off`) as long as they are being + decoded into a typed bool value. + Otherwise they behave as a string. + Booleans in YAML 1.2 are `true`/`false` only. +* Supports octals encoded and decoded as `0777` per YAML 1.1, rather than + `0o777` as specified in YAML 1.2, because most parsers still use the old + format. + Octals in the `0o777` format are supported though, so new files work. +* Does not support base-60 floats. + These are gone from YAML 1.2, and were actually never supported by this + package as it's clearly a poor choice. + + +## Installation and Usage + +The import path for the package is *go.yaml.in/yaml/v3*. + +To install it, run: + +```bash +go get go.yaml.in/yaml/v3 +``` + + +## API Documentation + +See: + + +## API Stability + +The package API for yaml v3 will remain stable as described in [gopkg.in]( +https://gopkg.in). + + +## Example + +```go +package main + +import ( + "fmt" + "log" + + "go.yaml.in/yaml/v3" +) + +var data = ` +a: Easy! +b: + c: 2 + d: [3, 4] +` + +// Note: struct fields must be public in order for unmarshal to +// correctly populate the data. +type T struct { + A string + B struct { + RenamedC int `yaml:"c"` + D []int `yaml:",flow"` + } +} + +func main() { + t := T{} + + err := yaml.Unmarshal([]byte(data), &t) + if err != nil { + log.Fatalf("error: %v", err) + } + fmt.Printf("--- t:\n%v\n\n", t) + + d, err := yaml.Marshal(&t) + if err != nil { + log.Fatalf("error: %v", err) + } + fmt.Printf("--- t dump:\n%s\n\n", string(d)) + + m := make(map[interface{}]interface{}) + + err = yaml.Unmarshal([]byte(data), &m) + if err != nil { + log.Fatalf("error: %v", err) + } + fmt.Printf("--- m:\n%v\n\n", m) + + d, err = yaml.Marshal(&m) + if err != nil { + log.Fatalf("error: %v", err) + } + fmt.Printf("--- m dump:\n%s\n\n", string(d)) +} +``` + +This example will generate the following output: + +``` +--- t: +{Easy! {2 [3 4]}} + +--- t dump: +a: Easy! +b: + c: 2 + d: [3, 4] + + +--- m: +map[a:Easy! b:map[c:2 d:[3 4]]] + +--- m dump: +a: Easy! +b: + c: 2 + d: + - 3 + - 4 +``` + + +## License + +The yaml package is licensed under the MIT and Apache License 2.0 licenses. +Please see the LICENSE file for details. diff --git a/vendor/go.yaml.in/yaml/v3/apic.go b/vendor/go.yaml.in/yaml/v3/apic.go new file mode 100644 index 000000000..05fd305da --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/apic.go @@ -0,0 +1,747 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// 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. + +package yaml + +import ( + "io" +) + +func yaml_insert_token(parser *yaml_parser_t, pos int, token *yaml_token_t) { + //fmt.Println("yaml_insert_token", "pos:", pos, "typ:", token.typ, "head:", parser.tokens_head, "len:", len(parser.tokens)) + + // Check if we can move the queue at the beginning of the buffer. + if parser.tokens_head > 0 && len(parser.tokens) == cap(parser.tokens) { + if parser.tokens_head != len(parser.tokens) { + copy(parser.tokens, parser.tokens[parser.tokens_head:]) + } + parser.tokens = parser.tokens[:len(parser.tokens)-parser.tokens_head] + parser.tokens_head = 0 + } + parser.tokens = append(parser.tokens, *token) + if pos < 0 { + return + } + copy(parser.tokens[parser.tokens_head+pos+1:], parser.tokens[parser.tokens_head+pos:]) + parser.tokens[parser.tokens_head+pos] = *token +} + +// Create a new parser object. +func yaml_parser_initialize(parser *yaml_parser_t) bool { + *parser = yaml_parser_t{ + raw_buffer: make([]byte, 0, input_raw_buffer_size), + buffer: make([]byte, 0, input_buffer_size), + } + return true +} + +// Destroy a parser object. +func yaml_parser_delete(parser *yaml_parser_t) { + *parser = yaml_parser_t{} +} + +// String read handler. +func yaml_string_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) { + if parser.input_pos == len(parser.input) { + return 0, io.EOF + } + n = copy(buffer, parser.input[parser.input_pos:]) + parser.input_pos += n + return n, nil +} + +// Reader read handler. +func yaml_reader_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) { + return parser.input_reader.Read(buffer) +} + +// Set a string input. +func yaml_parser_set_input_string(parser *yaml_parser_t, input []byte) { + if parser.read_handler != nil { + panic("must set the input source only once") + } + parser.read_handler = yaml_string_read_handler + parser.input = input + parser.input_pos = 0 +} + +// Set a file input. +func yaml_parser_set_input_reader(parser *yaml_parser_t, r io.Reader) { + if parser.read_handler != nil { + panic("must set the input source only once") + } + parser.read_handler = yaml_reader_read_handler + parser.input_reader = r +} + +// Set the source encoding. +func yaml_parser_set_encoding(parser *yaml_parser_t, encoding yaml_encoding_t) { + if parser.encoding != yaml_ANY_ENCODING { + panic("must set the encoding only once") + } + parser.encoding = encoding +} + +// Create a new emitter object. +func yaml_emitter_initialize(emitter *yaml_emitter_t) { + *emitter = yaml_emitter_t{ + buffer: make([]byte, output_buffer_size), + raw_buffer: make([]byte, 0, output_raw_buffer_size), + states: make([]yaml_emitter_state_t, 0, initial_stack_size), + events: make([]yaml_event_t, 0, initial_queue_size), + best_width: -1, + } +} + +// Destroy an emitter object. +func yaml_emitter_delete(emitter *yaml_emitter_t) { + *emitter = yaml_emitter_t{} +} + +// String write handler. +func yaml_string_write_handler(emitter *yaml_emitter_t, buffer []byte) error { + *emitter.output_buffer = append(*emitter.output_buffer, buffer...) + return nil +} + +// yaml_writer_write_handler uses emitter.output_writer to write the +// emitted text. +func yaml_writer_write_handler(emitter *yaml_emitter_t, buffer []byte) error { + _, err := emitter.output_writer.Write(buffer) + return err +} + +// Set a string output. +func yaml_emitter_set_output_string(emitter *yaml_emitter_t, output_buffer *[]byte) { + if emitter.write_handler != nil { + panic("must set the output target only once") + } + emitter.write_handler = yaml_string_write_handler + emitter.output_buffer = output_buffer +} + +// Set a file output. +func yaml_emitter_set_output_writer(emitter *yaml_emitter_t, w io.Writer) { + if emitter.write_handler != nil { + panic("must set the output target only once") + } + emitter.write_handler = yaml_writer_write_handler + emitter.output_writer = w +} + +// Set the output encoding. +func yaml_emitter_set_encoding(emitter *yaml_emitter_t, encoding yaml_encoding_t) { + if emitter.encoding != yaml_ANY_ENCODING { + panic("must set the output encoding only once") + } + emitter.encoding = encoding +} + +// Set the canonical output style. +func yaml_emitter_set_canonical(emitter *yaml_emitter_t, canonical bool) { + emitter.canonical = canonical +} + +// Set the indentation increment. +func yaml_emitter_set_indent(emitter *yaml_emitter_t, indent int) { + if indent < 2 || indent > 9 { + indent = 2 + } + emitter.best_indent = indent +} + +// Set the preferred line width. +func yaml_emitter_set_width(emitter *yaml_emitter_t, width int) { + if width < 0 { + width = -1 + } + emitter.best_width = width +} + +// Set if unescaped non-ASCII characters are allowed. +func yaml_emitter_set_unicode(emitter *yaml_emitter_t, unicode bool) { + emitter.unicode = unicode +} + +// Set the preferred line break character. +func yaml_emitter_set_break(emitter *yaml_emitter_t, line_break yaml_break_t) { + emitter.line_break = line_break +} + +///* +// * Destroy a token object. +// */ +// +//YAML_DECLARE(void) +//yaml_token_delete(yaml_token_t *token) +//{ +// assert(token); // Non-NULL token object expected. +// +// switch (token.type) +// { +// case YAML_TAG_DIRECTIVE_TOKEN: +// yaml_free(token.data.tag_directive.handle); +// yaml_free(token.data.tag_directive.prefix); +// break; +// +// case YAML_ALIAS_TOKEN: +// yaml_free(token.data.alias.value); +// break; +// +// case YAML_ANCHOR_TOKEN: +// yaml_free(token.data.anchor.value); +// break; +// +// case YAML_TAG_TOKEN: +// yaml_free(token.data.tag.handle); +// yaml_free(token.data.tag.suffix); +// break; +// +// case YAML_SCALAR_TOKEN: +// yaml_free(token.data.scalar.value); +// break; +// +// default: +// break; +// } +// +// memset(token, 0, sizeof(yaml_token_t)); +//} +// +///* +// * Check if a string is a valid UTF-8 sequence. +// * +// * Check 'reader.c' for more details on UTF-8 encoding. +// */ +// +//static int +//yaml_check_utf8(yaml_char_t *start, size_t length) +//{ +// yaml_char_t *end = start+length; +// yaml_char_t *pointer = start; +// +// while (pointer < end) { +// unsigned char octet; +// unsigned int width; +// unsigned int value; +// size_t k; +// +// octet = pointer[0]; +// width = (octet & 0x80) == 0x00 ? 1 : +// (octet & 0xE0) == 0xC0 ? 2 : +// (octet & 0xF0) == 0xE0 ? 3 : +// (octet & 0xF8) == 0xF0 ? 4 : 0; +// value = (octet & 0x80) == 0x00 ? octet & 0x7F : +// (octet & 0xE0) == 0xC0 ? octet & 0x1F : +// (octet & 0xF0) == 0xE0 ? octet & 0x0F : +// (octet & 0xF8) == 0xF0 ? octet & 0x07 : 0; +// if (!width) return 0; +// if (pointer+width > end) return 0; +// for (k = 1; k < width; k ++) { +// octet = pointer[k]; +// if ((octet & 0xC0) != 0x80) return 0; +// value = (value << 6) + (octet & 0x3F); +// } +// if (!((width == 1) || +// (width == 2 && value >= 0x80) || +// (width == 3 && value >= 0x800) || +// (width == 4 && value >= 0x10000))) return 0; +// +// pointer += width; +// } +// +// return 1; +//} +// + +// Create STREAM-START. +func yaml_stream_start_event_initialize(event *yaml_event_t, encoding yaml_encoding_t) { + *event = yaml_event_t{ + typ: yaml_STREAM_START_EVENT, + encoding: encoding, + } +} + +// Create STREAM-END. +func yaml_stream_end_event_initialize(event *yaml_event_t) { + *event = yaml_event_t{ + typ: yaml_STREAM_END_EVENT, + } +} + +// Create DOCUMENT-START. +func yaml_document_start_event_initialize( + event *yaml_event_t, + version_directive *yaml_version_directive_t, + tag_directives []yaml_tag_directive_t, + implicit bool, +) { + *event = yaml_event_t{ + typ: yaml_DOCUMENT_START_EVENT, + version_directive: version_directive, + tag_directives: tag_directives, + implicit: implicit, + } +} + +// Create DOCUMENT-END. +func yaml_document_end_event_initialize(event *yaml_event_t, implicit bool) { + *event = yaml_event_t{ + typ: yaml_DOCUMENT_END_EVENT, + implicit: implicit, + } +} + +// Create ALIAS. +func yaml_alias_event_initialize(event *yaml_event_t, anchor []byte) bool { + *event = yaml_event_t{ + typ: yaml_ALIAS_EVENT, + anchor: anchor, + } + return true +} + +// Create SCALAR. +func yaml_scalar_event_initialize(event *yaml_event_t, anchor, tag, value []byte, plain_implicit, quoted_implicit bool, style yaml_scalar_style_t) bool { + *event = yaml_event_t{ + typ: yaml_SCALAR_EVENT, + anchor: anchor, + tag: tag, + value: value, + implicit: plain_implicit, + quoted_implicit: quoted_implicit, + style: yaml_style_t(style), + } + return true +} + +// Create SEQUENCE-START. +func yaml_sequence_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_sequence_style_t) bool { + *event = yaml_event_t{ + typ: yaml_SEQUENCE_START_EVENT, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(style), + } + return true +} + +// Create SEQUENCE-END. +func yaml_sequence_end_event_initialize(event *yaml_event_t) bool { + *event = yaml_event_t{ + typ: yaml_SEQUENCE_END_EVENT, + } + return true +} + +// Create MAPPING-START. +func yaml_mapping_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_mapping_style_t) { + *event = yaml_event_t{ + typ: yaml_MAPPING_START_EVENT, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(style), + } +} + +// Create MAPPING-END. +func yaml_mapping_end_event_initialize(event *yaml_event_t) { + *event = yaml_event_t{ + typ: yaml_MAPPING_END_EVENT, + } +} + +// Destroy an event object. +func yaml_event_delete(event *yaml_event_t) { + *event = yaml_event_t{} +} + +///* +// * Create a document object. +// */ +// +//YAML_DECLARE(int) +//yaml_document_initialize(document *yaml_document_t, +// version_directive *yaml_version_directive_t, +// tag_directives_start *yaml_tag_directive_t, +// tag_directives_end *yaml_tag_directive_t, +// start_implicit int, end_implicit int) +//{ +// struct { +// error yaml_error_type_t +// } context +// struct { +// start *yaml_node_t +// end *yaml_node_t +// top *yaml_node_t +// } nodes = { NULL, NULL, NULL } +// version_directive_copy *yaml_version_directive_t = NULL +// struct { +// start *yaml_tag_directive_t +// end *yaml_tag_directive_t +// top *yaml_tag_directive_t +// } tag_directives_copy = { NULL, NULL, NULL } +// value yaml_tag_directive_t = { NULL, NULL } +// mark yaml_mark_t = { 0, 0, 0 } +// +// assert(document) // Non-NULL document object is expected. +// assert((tag_directives_start && tag_directives_end) || +// (tag_directives_start == tag_directives_end)) +// // Valid tag directives are expected. +// +// if (!STACK_INIT(&context, nodes, INITIAL_STACK_SIZE)) goto error +// +// if (version_directive) { +// version_directive_copy = yaml_malloc(sizeof(yaml_version_directive_t)) +// if (!version_directive_copy) goto error +// version_directive_copy.major = version_directive.major +// version_directive_copy.minor = version_directive.minor +// } +// +// if (tag_directives_start != tag_directives_end) { +// tag_directive *yaml_tag_directive_t +// if (!STACK_INIT(&context, tag_directives_copy, INITIAL_STACK_SIZE)) +// goto error +// for (tag_directive = tag_directives_start +// tag_directive != tag_directives_end; tag_directive ++) { +// assert(tag_directive.handle) +// assert(tag_directive.prefix) +// if (!yaml_check_utf8(tag_directive.handle, +// strlen((char *)tag_directive.handle))) +// goto error +// if (!yaml_check_utf8(tag_directive.prefix, +// strlen((char *)tag_directive.prefix))) +// goto error +// value.handle = yaml_strdup(tag_directive.handle) +// value.prefix = yaml_strdup(tag_directive.prefix) +// if (!value.handle || !value.prefix) goto error +// if (!PUSH(&context, tag_directives_copy, value)) +// goto error +// value.handle = NULL +// value.prefix = NULL +// } +// } +// +// DOCUMENT_INIT(*document, nodes.start, nodes.end, version_directive_copy, +// tag_directives_copy.start, tag_directives_copy.top, +// start_implicit, end_implicit, mark, mark) +// +// return 1 +// +//error: +// STACK_DEL(&context, nodes) +// yaml_free(version_directive_copy) +// while (!STACK_EMPTY(&context, tag_directives_copy)) { +// value yaml_tag_directive_t = POP(&context, tag_directives_copy) +// yaml_free(value.handle) +// yaml_free(value.prefix) +// } +// STACK_DEL(&context, tag_directives_copy) +// yaml_free(value.handle) +// yaml_free(value.prefix) +// +// return 0 +//} +// +///* +// * Destroy a document object. +// */ +// +//YAML_DECLARE(void) +//yaml_document_delete(document *yaml_document_t) +//{ +// struct { +// error yaml_error_type_t +// } context +// tag_directive *yaml_tag_directive_t +// +// context.error = YAML_NO_ERROR // Eliminate a compiler warning. +// +// assert(document) // Non-NULL document object is expected. +// +// while (!STACK_EMPTY(&context, document.nodes)) { +// node yaml_node_t = POP(&context, document.nodes) +// yaml_free(node.tag) +// switch (node.type) { +// case YAML_SCALAR_NODE: +// yaml_free(node.data.scalar.value) +// break +// case YAML_SEQUENCE_NODE: +// STACK_DEL(&context, node.data.sequence.items) +// break +// case YAML_MAPPING_NODE: +// STACK_DEL(&context, node.data.mapping.pairs) +// break +// default: +// assert(0) // Should not happen. +// } +// } +// STACK_DEL(&context, document.nodes) +// +// yaml_free(document.version_directive) +// for (tag_directive = document.tag_directives.start +// tag_directive != document.tag_directives.end +// tag_directive++) { +// yaml_free(tag_directive.handle) +// yaml_free(tag_directive.prefix) +// } +// yaml_free(document.tag_directives.start) +// +// memset(document, 0, sizeof(yaml_document_t)) +//} +// +///** +// * Get a document node. +// */ +// +//YAML_DECLARE(yaml_node_t *) +//yaml_document_get_node(document *yaml_document_t, index int) +//{ +// assert(document) // Non-NULL document object is expected. +// +// if (index > 0 && document.nodes.start + index <= document.nodes.top) { +// return document.nodes.start + index - 1 +// } +// return NULL +//} +// +///** +// * Get the root object. +// */ +// +//YAML_DECLARE(yaml_node_t *) +//yaml_document_get_root_node(document *yaml_document_t) +//{ +// assert(document) // Non-NULL document object is expected. +// +// if (document.nodes.top != document.nodes.start) { +// return document.nodes.start +// } +// return NULL +//} +// +///* +// * Add a scalar node to a document. +// */ +// +//YAML_DECLARE(int) +//yaml_document_add_scalar(document *yaml_document_t, +// tag *yaml_char_t, value *yaml_char_t, length int, +// style yaml_scalar_style_t) +//{ +// struct { +// error yaml_error_type_t +// } context +// mark yaml_mark_t = { 0, 0, 0 } +// tag_copy *yaml_char_t = NULL +// value_copy *yaml_char_t = NULL +// node yaml_node_t +// +// assert(document) // Non-NULL document object is expected. +// assert(value) // Non-NULL value is expected. +// +// if (!tag) { +// tag = (yaml_char_t *)YAML_DEFAULT_SCALAR_TAG +// } +// +// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error +// tag_copy = yaml_strdup(tag) +// if (!tag_copy) goto error +// +// if (length < 0) { +// length = strlen((char *)value) +// } +// +// if (!yaml_check_utf8(value, length)) goto error +// value_copy = yaml_malloc(length+1) +// if (!value_copy) goto error +// memcpy(value_copy, value, length) +// value_copy[length] = '\0' +// +// SCALAR_NODE_INIT(node, tag_copy, value_copy, length, style, mark, mark) +// if (!PUSH(&context, document.nodes, node)) goto error +// +// return document.nodes.top - document.nodes.start +// +//error: +// yaml_free(tag_copy) +// yaml_free(value_copy) +// +// return 0 +//} +// +///* +// * Add a sequence node to a document. +// */ +// +//YAML_DECLARE(int) +//yaml_document_add_sequence(document *yaml_document_t, +// tag *yaml_char_t, style yaml_sequence_style_t) +//{ +// struct { +// error yaml_error_type_t +// } context +// mark yaml_mark_t = { 0, 0, 0 } +// tag_copy *yaml_char_t = NULL +// struct { +// start *yaml_node_item_t +// end *yaml_node_item_t +// top *yaml_node_item_t +// } items = { NULL, NULL, NULL } +// node yaml_node_t +// +// assert(document) // Non-NULL document object is expected. +// +// if (!tag) { +// tag = (yaml_char_t *)YAML_DEFAULT_SEQUENCE_TAG +// } +// +// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error +// tag_copy = yaml_strdup(tag) +// if (!tag_copy) goto error +// +// if (!STACK_INIT(&context, items, INITIAL_STACK_SIZE)) goto error +// +// SEQUENCE_NODE_INIT(node, tag_copy, items.start, items.end, +// style, mark, mark) +// if (!PUSH(&context, document.nodes, node)) goto error +// +// return document.nodes.top - document.nodes.start +// +//error: +// STACK_DEL(&context, items) +// yaml_free(tag_copy) +// +// return 0 +//} +// +///* +// * Add a mapping node to a document. +// */ +// +//YAML_DECLARE(int) +//yaml_document_add_mapping(document *yaml_document_t, +// tag *yaml_char_t, style yaml_mapping_style_t) +//{ +// struct { +// error yaml_error_type_t +// } context +// mark yaml_mark_t = { 0, 0, 0 } +// tag_copy *yaml_char_t = NULL +// struct { +// start *yaml_node_pair_t +// end *yaml_node_pair_t +// top *yaml_node_pair_t +// } pairs = { NULL, NULL, NULL } +// node yaml_node_t +// +// assert(document) // Non-NULL document object is expected. +// +// if (!tag) { +// tag = (yaml_char_t *)YAML_DEFAULT_MAPPING_TAG +// } +// +// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error +// tag_copy = yaml_strdup(tag) +// if (!tag_copy) goto error +// +// if (!STACK_INIT(&context, pairs, INITIAL_STACK_SIZE)) goto error +// +// MAPPING_NODE_INIT(node, tag_copy, pairs.start, pairs.end, +// style, mark, mark) +// if (!PUSH(&context, document.nodes, node)) goto error +// +// return document.nodes.top - document.nodes.start +// +//error: +// STACK_DEL(&context, pairs) +// yaml_free(tag_copy) +// +// return 0 +//} +// +///* +// * Append an item to a sequence node. +// */ +// +//YAML_DECLARE(int) +//yaml_document_append_sequence_item(document *yaml_document_t, +// sequence int, item int) +//{ +// struct { +// error yaml_error_type_t +// } context +// +// assert(document) // Non-NULL document is required. +// assert(sequence > 0 +// && document.nodes.start + sequence <= document.nodes.top) +// // Valid sequence id is required. +// assert(document.nodes.start[sequence-1].type == YAML_SEQUENCE_NODE) +// // A sequence node is required. +// assert(item > 0 && document.nodes.start + item <= document.nodes.top) +// // Valid item id is required. +// +// if (!PUSH(&context, +// document.nodes.start[sequence-1].data.sequence.items, item)) +// return 0 +// +// return 1 +//} +// +///* +// * Append a pair of a key and a value to a mapping node. +// */ +// +//YAML_DECLARE(int) +//yaml_document_append_mapping_pair(document *yaml_document_t, +// mapping int, key int, value int) +//{ +// struct { +// error yaml_error_type_t +// } context +// +// pair yaml_node_pair_t +// +// assert(document) // Non-NULL document is required. +// assert(mapping > 0 +// && document.nodes.start + mapping <= document.nodes.top) +// // Valid mapping id is required. +// assert(document.nodes.start[mapping-1].type == YAML_MAPPING_NODE) +// // A mapping node is required. +// assert(key > 0 && document.nodes.start + key <= document.nodes.top) +// // Valid key id is required. +// assert(value > 0 && document.nodes.start + value <= document.nodes.top) +// // Valid value id is required. +// +// pair.key = key +// pair.value = value +// +// if (!PUSH(&context, +// document.nodes.start[mapping-1].data.mapping.pairs, pair)) +// return 0 +// +// return 1 +//} +// +// diff --git a/vendor/go.yaml.in/yaml/v3/decode.go b/vendor/go.yaml.in/yaml/v3/decode.go new file mode 100644 index 000000000..02e2b17bf --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/decode.go @@ -0,0 +1,1018 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package yaml + +import ( + "encoding" + "encoding/base64" + "fmt" + "io" + "math" + "reflect" + "strconv" + "time" +) + +// ---------------------------------------------------------------------------- +// Parser, produces a node tree out of a libyaml event stream. + +type parser struct { + parser yaml_parser_t + event yaml_event_t + doc *Node + anchors map[string]*Node + doneInit bool + textless bool +} + +func newParser(b []byte) *parser { + p := parser{} + if !yaml_parser_initialize(&p.parser) { + panic("failed to initialize YAML emitter") + } + if len(b) == 0 { + b = []byte{'\n'} + } + yaml_parser_set_input_string(&p.parser, b) + return &p +} + +func newParserFromReader(r io.Reader) *parser { + p := parser{} + if !yaml_parser_initialize(&p.parser) { + panic("failed to initialize YAML emitter") + } + yaml_parser_set_input_reader(&p.parser, r) + return &p +} + +func (p *parser) init() { + if p.doneInit { + return + } + p.anchors = make(map[string]*Node) + p.expect(yaml_STREAM_START_EVENT) + p.doneInit = true +} + +func (p *parser) destroy() { + if p.event.typ != yaml_NO_EVENT { + yaml_event_delete(&p.event) + } + yaml_parser_delete(&p.parser) +} + +// expect consumes an event from the event stream and +// checks that it's of the expected type. +func (p *parser) expect(e yaml_event_type_t) { + if p.event.typ == yaml_NO_EVENT { + if !yaml_parser_parse(&p.parser, &p.event) { + p.fail() + } + } + if p.event.typ == yaml_STREAM_END_EVENT { + failf("attempted to go past the end of stream; corrupted value?") + } + if p.event.typ != e { + p.parser.problem = fmt.Sprintf("expected %s event but got %s", e, p.event.typ) + p.fail() + } + yaml_event_delete(&p.event) + p.event.typ = yaml_NO_EVENT +} + +// peek peeks at the next event in the event stream, +// puts the results into p.event and returns the event type. +func (p *parser) peek() yaml_event_type_t { + if p.event.typ != yaml_NO_EVENT { + return p.event.typ + } + // It's curious choice from the underlying API to generally return a + // positive result on success, but on this case return true in an error + // scenario. This was the source of bugs in the past (issue #666). + if !yaml_parser_parse(&p.parser, &p.event) || p.parser.error != yaml_NO_ERROR { + p.fail() + } + return p.event.typ +} + +func (p *parser) fail() { + var where string + var line int + if p.parser.context_mark.line != 0 { + line = p.parser.context_mark.line + // Scanner errors don't iterate line before returning error + if p.parser.error == yaml_SCANNER_ERROR { + line++ + } + } else if p.parser.problem_mark.line != 0 { + line = p.parser.problem_mark.line + // Scanner errors don't iterate line before returning error + if p.parser.error == yaml_SCANNER_ERROR { + line++ + } + } + if line != 0 { + where = "line " + strconv.Itoa(line) + ": " + } + var msg string + if len(p.parser.problem) > 0 { + msg = p.parser.problem + } else { + msg = "unknown problem parsing YAML content" + } + failf("%s%s", where, msg) +} + +func (p *parser) anchor(n *Node, anchor []byte) { + if anchor != nil { + n.Anchor = string(anchor) + p.anchors[n.Anchor] = n + } +} + +func (p *parser) parse() *Node { + p.init() + switch p.peek() { + case yaml_SCALAR_EVENT: + return p.scalar() + case yaml_ALIAS_EVENT: + return p.alias() + case yaml_MAPPING_START_EVENT: + return p.mapping() + case yaml_SEQUENCE_START_EVENT: + return p.sequence() + case yaml_DOCUMENT_START_EVENT: + return p.document() + case yaml_STREAM_END_EVENT: + // Happens when attempting to decode an empty buffer. + return nil + case yaml_TAIL_COMMENT_EVENT: + panic("internal error: unexpected tail comment event (please report)") + default: + panic("internal error: attempted to parse unknown event (please report): " + p.event.typ.String()) + } +} + +func (p *parser) node(kind Kind, defaultTag, tag, value string) *Node { + var style Style + if tag != "" && tag != "!" { + tag = shortTag(tag) + style = TaggedStyle + } else if defaultTag != "" { + tag = defaultTag + } else if kind == ScalarNode { + tag, _ = resolve("", value) + } + n := &Node{ + Kind: kind, + Tag: tag, + Value: value, + Style: style, + } + if !p.textless { + n.Line = p.event.start_mark.line + 1 + n.Column = p.event.start_mark.column + 1 + n.HeadComment = string(p.event.head_comment) + n.LineComment = string(p.event.line_comment) + n.FootComment = string(p.event.foot_comment) + } + return n +} + +func (p *parser) parseChild(parent *Node) *Node { + child := p.parse() + parent.Content = append(parent.Content, child) + return child +} + +func (p *parser) document() *Node { + n := p.node(DocumentNode, "", "", "") + p.doc = n + p.expect(yaml_DOCUMENT_START_EVENT) + p.parseChild(n) + if p.peek() == yaml_DOCUMENT_END_EVENT { + n.FootComment = string(p.event.foot_comment) + } + p.expect(yaml_DOCUMENT_END_EVENT) + return n +} + +func (p *parser) alias() *Node { + n := p.node(AliasNode, "", "", string(p.event.anchor)) + n.Alias = p.anchors[n.Value] + if n.Alias == nil { + failf("unknown anchor '%s' referenced", n.Value) + } + p.expect(yaml_ALIAS_EVENT) + return n +} + +func (p *parser) scalar() *Node { + var parsedStyle = p.event.scalar_style() + var nodeStyle Style + switch { + case parsedStyle&yaml_DOUBLE_QUOTED_SCALAR_STYLE != 0: + nodeStyle = DoubleQuotedStyle + case parsedStyle&yaml_SINGLE_QUOTED_SCALAR_STYLE != 0: + nodeStyle = SingleQuotedStyle + case parsedStyle&yaml_LITERAL_SCALAR_STYLE != 0: + nodeStyle = LiteralStyle + case parsedStyle&yaml_FOLDED_SCALAR_STYLE != 0: + nodeStyle = FoldedStyle + } + var nodeValue = string(p.event.value) + var nodeTag = string(p.event.tag) + var defaultTag string + if nodeStyle == 0 { + if nodeValue == "<<" { + defaultTag = mergeTag + } + } else { + defaultTag = strTag + } + n := p.node(ScalarNode, defaultTag, nodeTag, nodeValue) + n.Style |= nodeStyle + p.anchor(n, p.event.anchor) + p.expect(yaml_SCALAR_EVENT) + return n +} + +func (p *parser) sequence() *Node { + n := p.node(SequenceNode, seqTag, string(p.event.tag), "") + if p.event.sequence_style()&yaml_FLOW_SEQUENCE_STYLE != 0 { + n.Style |= FlowStyle + } + p.anchor(n, p.event.anchor) + p.expect(yaml_SEQUENCE_START_EVENT) + for p.peek() != yaml_SEQUENCE_END_EVENT { + p.parseChild(n) + } + n.LineComment = string(p.event.line_comment) + n.FootComment = string(p.event.foot_comment) + p.expect(yaml_SEQUENCE_END_EVENT) + return n +} + +func (p *parser) mapping() *Node { + n := p.node(MappingNode, mapTag, string(p.event.tag), "") + block := true + if p.event.mapping_style()&yaml_FLOW_MAPPING_STYLE != 0 { + block = false + n.Style |= FlowStyle + } + p.anchor(n, p.event.anchor) + p.expect(yaml_MAPPING_START_EVENT) + for p.peek() != yaml_MAPPING_END_EVENT { + k := p.parseChild(n) + if block && k.FootComment != "" { + // Must be a foot comment for the prior value when being dedented. + if len(n.Content) > 2 { + n.Content[len(n.Content)-3].FootComment = k.FootComment + k.FootComment = "" + } + } + v := p.parseChild(n) + if k.FootComment == "" && v.FootComment != "" { + k.FootComment = v.FootComment + v.FootComment = "" + } + if p.peek() == yaml_TAIL_COMMENT_EVENT { + if k.FootComment == "" { + k.FootComment = string(p.event.foot_comment) + } + p.expect(yaml_TAIL_COMMENT_EVENT) + } + } + n.LineComment = string(p.event.line_comment) + n.FootComment = string(p.event.foot_comment) + if n.Style&FlowStyle == 0 && n.FootComment != "" && len(n.Content) > 1 { + n.Content[len(n.Content)-2].FootComment = n.FootComment + n.FootComment = "" + } + p.expect(yaml_MAPPING_END_EVENT) + return n +} + +// ---------------------------------------------------------------------------- +// Decoder, unmarshals a node into a provided value. + +type decoder struct { + doc *Node + aliases map[*Node]bool + terrors []string + + stringMapType reflect.Type + generalMapType reflect.Type + + knownFields bool + uniqueKeys bool + decodeCount int + aliasCount int + aliasDepth int + + mergedFields map[interface{}]bool +} + +var ( + nodeType = reflect.TypeOf(Node{}) + durationType = reflect.TypeOf(time.Duration(0)) + stringMapType = reflect.TypeOf(map[string]interface{}{}) + generalMapType = reflect.TypeOf(map[interface{}]interface{}{}) + ifaceType = generalMapType.Elem() + timeType = reflect.TypeOf(time.Time{}) + ptrTimeType = reflect.TypeOf(&time.Time{}) +) + +func newDecoder() *decoder { + d := &decoder{ + stringMapType: stringMapType, + generalMapType: generalMapType, + uniqueKeys: true, + } + d.aliases = make(map[*Node]bool) + return d +} + +func (d *decoder) terror(n *Node, tag string, out reflect.Value) { + if n.Tag != "" { + tag = n.Tag + } + value := n.Value + if tag != seqTag && tag != mapTag { + if len(value) > 10 { + value = " `" + value[:7] + "...`" + } else { + value = " `" + value + "`" + } + } + d.terrors = append(d.terrors, fmt.Sprintf("line %d: cannot unmarshal %s%s into %s", n.Line, shortTag(tag), value, out.Type())) +} + +func (d *decoder) callUnmarshaler(n *Node, u Unmarshaler) (good bool) { + err := u.UnmarshalYAML(n) + if e, ok := err.(*TypeError); ok { + d.terrors = append(d.terrors, e.Errors...) + return false + } + if err != nil { + fail(err) + } + return true +} + +func (d *decoder) callObsoleteUnmarshaler(n *Node, u obsoleteUnmarshaler) (good bool) { + terrlen := len(d.terrors) + err := u.UnmarshalYAML(func(v interface{}) (err error) { + defer handleErr(&err) + d.unmarshal(n, reflect.ValueOf(v)) + if len(d.terrors) > terrlen { + issues := d.terrors[terrlen:] + d.terrors = d.terrors[:terrlen] + return &TypeError{issues} + } + return nil + }) + if e, ok := err.(*TypeError); ok { + d.terrors = append(d.terrors, e.Errors...) + return false + } + if err != nil { + fail(err) + } + return true +} + +// d.prepare initializes and dereferences pointers and calls UnmarshalYAML +// if a value is found to implement it. +// It returns the initialized and dereferenced out value, whether +// unmarshalling was already done by UnmarshalYAML, and if so whether +// its types unmarshalled appropriately. +// +// If n holds a null value, prepare returns before doing anything. +func (d *decoder) prepare(n *Node, out reflect.Value) (newout reflect.Value, unmarshaled, good bool) { + if n.ShortTag() == nullTag { + return out, false, false + } + again := true + for again { + again = false + if out.Kind() == reflect.Ptr { + if out.IsNil() { + out.Set(reflect.New(out.Type().Elem())) + } + out = out.Elem() + again = true + } + if out.CanAddr() { + outi := out.Addr().Interface() + if u, ok := outi.(Unmarshaler); ok { + good = d.callUnmarshaler(n, u) + return out, true, good + } + if u, ok := outi.(obsoleteUnmarshaler); ok { + good = d.callObsoleteUnmarshaler(n, u) + return out, true, good + } + } + } + return out, false, false +} + +func (d *decoder) fieldByIndex(n *Node, v reflect.Value, index []int) (field reflect.Value) { + if n.ShortTag() == nullTag { + return reflect.Value{} + } + for _, num := range index { + for { + if v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + continue + } + break + } + v = v.Field(num) + } + return v +} + +const ( + // 400,000 decode operations is ~500kb of dense object declarations, or + // ~5kb of dense object declarations with 10000% alias expansion + alias_ratio_range_low = 400000 + + // 4,000,000 decode operations is ~5MB of dense object declarations, or + // ~4.5MB of dense object declarations with 10% alias expansion + alias_ratio_range_high = 4000000 + + // alias_ratio_range is the range over which we scale allowed alias ratios + alias_ratio_range = float64(alias_ratio_range_high - alias_ratio_range_low) +) + +func allowedAliasRatio(decodeCount int) float64 { + switch { + case decodeCount <= alias_ratio_range_low: + // allow 99% to come from alias expansion for small-to-medium documents + return 0.99 + case decodeCount >= alias_ratio_range_high: + // allow 10% to come from alias expansion for very large documents + return 0.10 + default: + // scale smoothly from 99% down to 10% over the range. + // this maps to 396,000 - 400,000 allowed alias-driven decodes over the range. + // 400,000 decode operations is ~100MB of allocations in worst-case scenarios (single-item maps). + return 0.99 - 0.89*(float64(decodeCount-alias_ratio_range_low)/alias_ratio_range) + } +} + +func (d *decoder) unmarshal(n *Node, out reflect.Value) (good bool) { + d.decodeCount++ + if d.aliasDepth > 0 { + d.aliasCount++ + } + if d.aliasCount > 100 && d.decodeCount > 1000 && float64(d.aliasCount)/float64(d.decodeCount) > allowedAliasRatio(d.decodeCount) { + failf("document contains excessive aliasing") + } + if out.Type() == nodeType { + out.Set(reflect.ValueOf(n).Elem()) + return true + } + switch n.Kind { + case DocumentNode: + return d.document(n, out) + case AliasNode: + return d.alias(n, out) + } + out, unmarshaled, good := d.prepare(n, out) + if unmarshaled { + return good + } + switch n.Kind { + case ScalarNode: + good = d.scalar(n, out) + case MappingNode: + good = d.mapping(n, out) + case SequenceNode: + good = d.sequence(n, out) + case 0: + if n.IsZero() { + return d.null(out) + } + fallthrough + default: + failf("cannot decode node with unknown kind %d", n.Kind) + } + return good +} + +func (d *decoder) document(n *Node, out reflect.Value) (good bool) { + if len(n.Content) == 1 { + d.doc = n + d.unmarshal(n.Content[0], out) + return true + } + return false +} + +func (d *decoder) alias(n *Node, out reflect.Value) (good bool) { + if d.aliases[n] { + // TODO this could actually be allowed in some circumstances. + failf("anchor '%s' value contains itself", n.Value) + } + d.aliases[n] = true + d.aliasDepth++ + good = d.unmarshal(n.Alias, out) + d.aliasDepth-- + delete(d.aliases, n) + return good +} + +var zeroValue reflect.Value + +func resetMap(out reflect.Value) { + for _, k := range out.MapKeys() { + out.SetMapIndex(k, zeroValue) + } +} + +func (d *decoder) null(out reflect.Value) bool { + if out.CanAddr() { + switch out.Kind() { + case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: + out.Set(reflect.Zero(out.Type())) + return true + } + } + return false +} + +func (d *decoder) scalar(n *Node, out reflect.Value) bool { + var tag string + var resolved interface{} + if n.indicatedString() { + tag = strTag + resolved = n.Value + } else { + tag, resolved = resolve(n.Tag, n.Value) + if tag == binaryTag { + data, err := base64.StdEncoding.DecodeString(resolved.(string)) + if err != nil { + failf("!!binary value contains invalid base64 data") + } + resolved = string(data) + } + } + if resolved == nil { + return d.null(out) + } + if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { + // We've resolved to exactly the type we want, so use that. + out.Set(resolvedv) + return true + } + // Perhaps we can use the value as a TextUnmarshaler to + // set its value. + if out.CanAddr() { + u, ok := out.Addr().Interface().(encoding.TextUnmarshaler) + if ok { + var text []byte + if tag == binaryTag { + text = []byte(resolved.(string)) + } else { + // We let any value be unmarshaled into TextUnmarshaler. + // That might be more lax than we'd like, but the + // TextUnmarshaler itself should bowl out any dubious values. + text = []byte(n.Value) + } + err := u.UnmarshalText(text) + if err != nil { + fail(err) + } + return true + } + } + switch out.Kind() { + case reflect.String: + if tag == binaryTag { + out.SetString(resolved.(string)) + return true + } + out.SetString(n.Value) + return true + case reflect.Interface: + out.Set(reflect.ValueOf(resolved)) + return true + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + // This used to work in v2, but it's very unfriendly. + isDuration := out.Type() == durationType + + switch resolved := resolved.(type) { + case int: + if !isDuration && !out.OverflowInt(int64(resolved)) { + out.SetInt(int64(resolved)) + return true + } + case int64: + if !isDuration && !out.OverflowInt(resolved) { + out.SetInt(resolved) + return true + } + case uint64: + if !isDuration && resolved <= math.MaxInt64 && !out.OverflowInt(int64(resolved)) { + out.SetInt(int64(resolved)) + return true + } + case float64: + if !isDuration && resolved <= math.MaxInt64 && !out.OverflowInt(int64(resolved)) { + out.SetInt(int64(resolved)) + return true + } + case string: + if out.Type() == durationType { + d, err := time.ParseDuration(resolved) + if err == nil { + out.SetInt(int64(d)) + return true + } + } + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + switch resolved := resolved.(type) { + case int: + if resolved >= 0 && !out.OverflowUint(uint64(resolved)) { + out.SetUint(uint64(resolved)) + return true + } + case int64: + if resolved >= 0 && !out.OverflowUint(uint64(resolved)) { + out.SetUint(uint64(resolved)) + return true + } + case uint64: + if !out.OverflowUint(uint64(resolved)) { + out.SetUint(uint64(resolved)) + return true + } + case float64: + if resolved <= math.MaxUint64 && !out.OverflowUint(uint64(resolved)) { + out.SetUint(uint64(resolved)) + return true + } + } + case reflect.Bool: + switch resolved := resolved.(type) { + case bool: + out.SetBool(resolved) + return true + case string: + // This offers some compatibility with the 1.1 spec (https://yaml.org/type/bool.html). + // It only works if explicitly attempting to unmarshal into a typed bool value. + switch resolved { + case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON": + out.SetBool(true) + return true + case "n", "N", "no", "No", "NO", "off", "Off", "OFF": + out.SetBool(false) + return true + } + } + case reflect.Float32, reflect.Float64: + switch resolved := resolved.(type) { + case int: + out.SetFloat(float64(resolved)) + return true + case int64: + out.SetFloat(float64(resolved)) + return true + case uint64: + out.SetFloat(float64(resolved)) + return true + case float64: + out.SetFloat(resolved) + return true + } + case reflect.Struct: + if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { + out.Set(resolvedv) + return true + } + case reflect.Ptr: + panic("yaml internal error: please report the issue") + } + d.terror(n, tag, out) + return false +} + +func settableValueOf(i interface{}) reflect.Value { + v := reflect.ValueOf(i) + sv := reflect.New(v.Type()).Elem() + sv.Set(v) + return sv +} + +func (d *decoder) sequence(n *Node, out reflect.Value) (good bool) { + l := len(n.Content) + + var iface reflect.Value + switch out.Kind() { + case reflect.Slice: + out.Set(reflect.MakeSlice(out.Type(), l, l)) + case reflect.Array: + if l != out.Len() { + failf("invalid array: want %d elements but got %d", out.Len(), l) + } + case reflect.Interface: + // No type hints. Will have to use a generic sequence. + iface = out + out = settableValueOf(make([]interface{}, l)) + default: + d.terror(n, seqTag, out) + return false + } + et := out.Type().Elem() + + j := 0 + for i := 0; i < l; i++ { + e := reflect.New(et).Elem() + if ok := d.unmarshal(n.Content[i], e); ok { + out.Index(j).Set(e) + j++ + } + } + if out.Kind() != reflect.Array { + out.Set(out.Slice(0, j)) + } + if iface.IsValid() { + iface.Set(out) + } + return true +} + +func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) { + l := len(n.Content) + if d.uniqueKeys { + nerrs := len(d.terrors) + for i := 0; i < l; i += 2 { + ni := n.Content[i] + for j := i + 2; j < l; j += 2 { + nj := n.Content[j] + if ni.Kind == nj.Kind && ni.Value == nj.Value { + d.terrors = append(d.terrors, fmt.Sprintf("line %d: mapping key %#v already defined at line %d", nj.Line, nj.Value, ni.Line)) + } + } + } + if len(d.terrors) > nerrs { + return false + } + } + switch out.Kind() { + case reflect.Struct: + return d.mappingStruct(n, out) + case reflect.Map: + // okay + case reflect.Interface: + iface := out + if isStringMap(n) { + out = reflect.MakeMap(d.stringMapType) + } else { + out = reflect.MakeMap(d.generalMapType) + } + iface.Set(out) + default: + d.terror(n, mapTag, out) + return false + } + + outt := out.Type() + kt := outt.Key() + et := outt.Elem() + + stringMapType := d.stringMapType + generalMapType := d.generalMapType + if outt.Elem() == ifaceType { + if outt.Key().Kind() == reflect.String { + d.stringMapType = outt + } else if outt.Key() == ifaceType { + d.generalMapType = outt + } + } + + mergedFields := d.mergedFields + d.mergedFields = nil + + var mergeNode *Node + + mapIsNew := false + if out.IsNil() { + out.Set(reflect.MakeMap(outt)) + mapIsNew = true + } + for i := 0; i < l; i += 2 { + if isMerge(n.Content[i]) { + mergeNode = n.Content[i+1] + continue + } + k := reflect.New(kt).Elem() + if d.unmarshal(n.Content[i], k) { + if mergedFields != nil { + ki := k.Interface() + if d.getPossiblyUnhashableKey(mergedFields, ki) { + continue + } + d.setPossiblyUnhashableKey(mergedFields, ki, true) + } + kkind := k.Kind() + if kkind == reflect.Interface { + kkind = k.Elem().Kind() + } + if kkind == reflect.Map || kkind == reflect.Slice { + failf("invalid map key: %#v", k.Interface()) + } + e := reflect.New(et).Elem() + if d.unmarshal(n.Content[i+1], e) || n.Content[i+1].ShortTag() == nullTag && (mapIsNew || !out.MapIndex(k).IsValid()) { + out.SetMapIndex(k, e) + } + } + } + + d.mergedFields = mergedFields + if mergeNode != nil { + d.merge(n, mergeNode, out) + } + + d.stringMapType = stringMapType + d.generalMapType = generalMapType + return true +} + +func isStringMap(n *Node) bool { + if n.Kind != MappingNode { + return false + } + l := len(n.Content) + for i := 0; i < l; i += 2 { + shortTag := n.Content[i].ShortTag() + if shortTag != strTag && shortTag != mergeTag { + return false + } + } + return true +} + +func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { + sinfo, err := getStructInfo(out.Type()) + if err != nil { + panic(err) + } + + var inlineMap reflect.Value + var elemType reflect.Type + if sinfo.InlineMap != -1 { + inlineMap = out.Field(sinfo.InlineMap) + elemType = inlineMap.Type().Elem() + } + + for _, index := range sinfo.InlineUnmarshalers { + field := d.fieldByIndex(n, out, index) + d.prepare(n, field) + } + + mergedFields := d.mergedFields + d.mergedFields = nil + var mergeNode *Node + var doneFields []bool + if d.uniqueKeys { + doneFields = make([]bool, len(sinfo.FieldsList)) + } + name := settableValueOf("") + l := len(n.Content) + for i := 0; i < l; i += 2 { + ni := n.Content[i] + if isMerge(ni) { + mergeNode = n.Content[i+1] + continue + } + if !d.unmarshal(ni, name) { + continue + } + sname := name.String() + if mergedFields != nil { + if mergedFields[sname] { + continue + } + mergedFields[sname] = true + } + if info, ok := sinfo.FieldsMap[sname]; ok { + if d.uniqueKeys { + if doneFields[info.Id] { + d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s already set in type %s", ni.Line, name.String(), out.Type())) + continue + } + doneFields[info.Id] = true + } + var field reflect.Value + if info.Inline == nil { + field = out.Field(info.Num) + } else { + field = d.fieldByIndex(n, out, info.Inline) + } + d.unmarshal(n.Content[i+1], field) + } else if sinfo.InlineMap != -1 { + if inlineMap.IsNil() { + inlineMap.Set(reflect.MakeMap(inlineMap.Type())) + } + value := reflect.New(elemType).Elem() + d.unmarshal(n.Content[i+1], value) + inlineMap.SetMapIndex(name, value) + } else if d.knownFields { + d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s not found in type %s", ni.Line, name.String(), out.Type())) + } + } + + d.mergedFields = mergedFields + if mergeNode != nil { + d.merge(n, mergeNode, out) + } + return true +} + +func failWantMap() { + failf("map merge requires map or sequence of maps as the value") +} + +func (d *decoder) setPossiblyUnhashableKey(m map[interface{}]bool, key interface{}, value bool) { + defer func() { + if err := recover(); err != nil { + failf("%v", err) + } + }() + m[key] = value +} + +func (d *decoder) getPossiblyUnhashableKey(m map[interface{}]bool, key interface{}) bool { + defer func() { + if err := recover(); err != nil { + failf("%v", err) + } + }() + return m[key] +} + +func (d *decoder) merge(parent *Node, merge *Node, out reflect.Value) { + mergedFields := d.mergedFields + if mergedFields == nil { + d.mergedFields = make(map[interface{}]bool) + for i := 0; i < len(parent.Content); i += 2 { + k := reflect.New(ifaceType).Elem() + if d.unmarshal(parent.Content[i], k) { + d.setPossiblyUnhashableKey(d.mergedFields, k.Interface(), true) + } + } + } + + switch merge.Kind { + case MappingNode: + d.unmarshal(merge, out) + case AliasNode: + if merge.Alias != nil && merge.Alias.Kind != MappingNode { + failWantMap() + } + d.unmarshal(merge, out) + case SequenceNode: + for i := 0; i < len(merge.Content); i++ { + ni := merge.Content[i] + if ni.Kind == AliasNode { + if ni.Alias != nil && ni.Alias.Kind != MappingNode { + failWantMap() + } + } else if ni.Kind != MappingNode { + failWantMap() + } + d.unmarshal(ni, out) + } + default: + failWantMap() + } + + d.mergedFields = mergedFields +} + +func isMerge(n *Node) bool { + return n.Kind == ScalarNode && n.Value == "<<" && (n.Tag == "" || n.Tag == "!" || shortTag(n.Tag) == mergeTag) +} diff --git a/vendor/go.yaml.in/yaml/v3/emitterc.go b/vendor/go.yaml.in/yaml/v3/emitterc.go new file mode 100644 index 000000000..ab4e03ba7 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/emitterc.go @@ -0,0 +1,2054 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// 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. + +package yaml + +import ( + "bytes" + "fmt" +) + +// Flush the buffer if needed. +func flush(emitter *yaml_emitter_t) bool { + if emitter.buffer_pos+5 >= len(emitter.buffer) { + return yaml_emitter_flush(emitter) + } + return true +} + +// Put a character to the output buffer. +func put(emitter *yaml_emitter_t, value byte) bool { + if emitter.buffer_pos+5 >= len(emitter.buffer) && !yaml_emitter_flush(emitter) { + return false + } + emitter.buffer[emitter.buffer_pos] = value + emitter.buffer_pos++ + emitter.column++ + return true +} + +// Put a line break to the output buffer. +func put_break(emitter *yaml_emitter_t) bool { + if emitter.buffer_pos+5 >= len(emitter.buffer) && !yaml_emitter_flush(emitter) { + return false + } + switch emitter.line_break { + case yaml_CR_BREAK: + emitter.buffer[emitter.buffer_pos] = '\r' + emitter.buffer_pos += 1 + case yaml_LN_BREAK: + emitter.buffer[emitter.buffer_pos] = '\n' + emitter.buffer_pos += 1 + case yaml_CRLN_BREAK: + emitter.buffer[emitter.buffer_pos+0] = '\r' + emitter.buffer[emitter.buffer_pos+1] = '\n' + emitter.buffer_pos += 2 + default: + panic("unknown line break setting") + } + if emitter.column == 0 { + emitter.space_above = true + } + emitter.column = 0 + emitter.line++ + // [Go] Do this here and below and drop from everywhere else (see commented lines). + emitter.indention = true + return true +} + +// Copy a character from a string into buffer. +func write(emitter *yaml_emitter_t, s []byte, i *int) bool { + if emitter.buffer_pos+5 >= len(emitter.buffer) && !yaml_emitter_flush(emitter) { + return false + } + p := emitter.buffer_pos + w := width(s[*i]) + switch w { + case 4: + emitter.buffer[p+3] = s[*i+3] + fallthrough + case 3: + emitter.buffer[p+2] = s[*i+2] + fallthrough + case 2: + emitter.buffer[p+1] = s[*i+1] + fallthrough + case 1: + emitter.buffer[p+0] = s[*i+0] + default: + panic("unknown character width") + } + emitter.column++ + emitter.buffer_pos += w + *i += w + return true +} + +// Write a whole string into buffer. +func write_all(emitter *yaml_emitter_t, s []byte) bool { + for i := 0; i < len(s); { + if !write(emitter, s, &i) { + return false + } + } + return true +} + +// Copy a line break character from a string into buffer. +func write_break(emitter *yaml_emitter_t, s []byte, i *int) bool { + if s[*i] == '\n' { + if !put_break(emitter) { + return false + } + *i++ + } else { + if !write(emitter, s, i) { + return false + } + if emitter.column == 0 { + emitter.space_above = true + } + emitter.column = 0 + emitter.line++ + // [Go] Do this here and above and drop from everywhere else (see commented lines). + emitter.indention = true + } + return true +} + +// Set an emitter error and return false. +func yaml_emitter_set_emitter_error(emitter *yaml_emitter_t, problem string) bool { + emitter.error = yaml_EMITTER_ERROR + emitter.problem = problem + return false +} + +// Emit an event. +func yaml_emitter_emit(emitter *yaml_emitter_t, event *yaml_event_t) bool { + emitter.events = append(emitter.events, *event) + for !yaml_emitter_need_more_events(emitter) { + event := &emitter.events[emitter.events_head] + if !yaml_emitter_analyze_event(emitter, event) { + return false + } + if !yaml_emitter_state_machine(emitter, event) { + return false + } + yaml_event_delete(event) + emitter.events_head++ + } + return true +} + +// Check if we need to accumulate more events before emitting. +// +// We accumulate extra +// - 1 event for DOCUMENT-START +// - 2 events for SEQUENCE-START +// - 3 events for MAPPING-START +func yaml_emitter_need_more_events(emitter *yaml_emitter_t) bool { + if emitter.events_head == len(emitter.events) { + return true + } + var accumulate int + switch emitter.events[emitter.events_head].typ { + case yaml_DOCUMENT_START_EVENT: + accumulate = 1 + break + case yaml_SEQUENCE_START_EVENT: + accumulate = 2 + break + case yaml_MAPPING_START_EVENT: + accumulate = 3 + break + default: + return false + } + if len(emitter.events)-emitter.events_head > accumulate { + return false + } + var level int + for i := emitter.events_head; i < len(emitter.events); i++ { + switch emitter.events[i].typ { + case yaml_STREAM_START_EVENT, yaml_DOCUMENT_START_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT: + level++ + case yaml_STREAM_END_EVENT, yaml_DOCUMENT_END_EVENT, yaml_SEQUENCE_END_EVENT, yaml_MAPPING_END_EVENT: + level-- + } + if level == 0 { + return false + } + } + return true +} + +// Append a directive to the directives stack. +func yaml_emitter_append_tag_directive(emitter *yaml_emitter_t, value *yaml_tag_directive_t, allow_duplicates bool) bool { + for i := 0; i < len(emitter.tag_directives); i++ { + if bytes.Equal(value.handle, emitter.tag_directives[i].handle) { + if allow_duplicates { + return true + } + return yaml_emitter_set_emitter_error(emitter, "duplicate %TAG directive") + } + } + + // [Go] Do we actually need to copy this given garbage collection + // and the lack of deallocating destructors? + tag_copy := yaml_tag_directive_t{ + handle: make([]byte, len(value.handle)), + prefix: make([]byte, len(value.prefix)), + } + copy(tag_copy.handle, value.handle) + copy(tag_copy.prefix, value.prefix) + emitter.tag_directives = append(emitter.tag_directives, tag_copy) + return true +} + +// Increase the indentation level. +func yaml_emitter_increase_indent_compact(emitter *yaml_emitter_t, flow, indentless bool, compact_seq bool) bool { + emitter.indents = append(emitter.indents, emitter.indent) + if emitter.indent < 0 { + if flow { + emitter.indent = emitter.best_indent + } else { + emitter.indent = 0 + } + } else if !indentless { + // [Go] This was changed so that indentations are more regular. + if emitter.states[len(emitter.states)-1] == yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE { + // The first indent inside a sequence will just skip the "- " indicator. + emitter.indent += 2 + } else { + // Everything else aligns to the chosen indentation. + emitter.indent = emitter.best_indent * ((emitter.indent + emitter.best_indent) / emitter.best_indent) + if compact_seq { + // The value compact_seq passed in is almost always set to `false` when this function is called, + // except when we are dealing with sequence nodes. So this gets triggered to subtract 2 only when we + // are increasing the indent to account for sequence nodes, which will be correct because we need to + // subtract 2 to account for the - at the beginning of the sequence node. + emitter.indent = emitter.indent - 2 + } + } + } + return true +} + +// State dispatcher. +func yaml_emitter_state_machine(emitter *yaml_emitter_t, event *yaml_event_t) bool { + switch emitter.state { + default: + case yaml_EMIT_STREAM_START_STATE: + return yaml_emitter_emit_stream_start(emitter, event) + + case yaml_EMIT_FIRST_DOCUMENT_START_STATE: + return yaml_emitter_emit_document_start(emitter, event, true) + + case yaml_EMIT_DOCUMENT_START_STATE: + return yaml_emitter_emit_document_start(emitter, event, false) + + case yaml_EMIT_DOCUMENT_CONTENT_STATE: + return yaml_emitter_emit_document_content(emitter, event) + + case yaml_EMIT_DOCUMENT_END_STATE: + return yaml_emitter_emit_document_end(emitter, event) + + case yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE: + return yaml_emitter_emit_flow_sequence_item(emitter, event, true, false) + + case yaml_EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE: + return yaml_emitter_emit_flow_sequence_item(emitter, event, false, true) + + case yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE: + return yaml_emitter_emit_flow_sequence_item(emitter, event, false, false) + + case yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE: + return yaml_emitter_emit_flow_mapping_key(emitter, event, true, false) + + case yaml_EMIT_FLOW_MAPPING_TRAIL_KEY_STATE: + return yaml_emitter_emit_flow_mapping_key(emitter, event, false, true) + + case yaml_EMIT_FLOW_MAPPING_KEY_STATE: + return yaml_emitter_emit_flow_mapping_key(emitter, event, false, false) + + case yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE: + return yaml_emitter_emit_flow_mapping_value(emitter, event, true) + + case yaml_EMIT_FLOW_MAPPING_VALUE_STATE: + return yaml_emitter_emit_flow_mapping_value(emitter, event, false) + + case yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE: + return yaml_emitter_emit_block_sequence_item(emitter, event, true) + + case yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE: + return yaml_emitter_emit_block_sequence_item(emitter, event, false) + + case yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE: + return yaml_emitter_emit_block_mapping_key(emitter, event, true) + + case yaml_EMIT_BLOCK_MAPPING_KEY_STATE: + return yaml_emitter_emit_block_mapping_key(emitter, event, false) + + case yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE: + return yaml_emitter_emit_block_mapping_value(emitter, event, true) + + case yaml_EMIT_BLOCK_MAPPING_VALUE_STATE: + return yaml_emitter_emit_block_mapping_value(emitter, event, false) + + case yaml_EMIT_END_STATE: + return yaml_emitter_set_emitter_error(emitter, "expected nothing after STREAM-END") + } + panic("invalid emitter state") +} + +// Expect STREAM-START. +func yaml_emitter_emit_stream_start(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if event.typ != yaml_STREAM_START_EVENT { + return yaml_emitter_set_emitter_error(emitter, "expected STREAM-START") + } + if emitter.encoding == yaml_ANY_ENCODING { + emitter.encoding = event.encoding + if emitter.encoding == yaml_ANY_ENCODING { + emitter.encoding = yaml_UTF8_ENCODING + } + } + if emitter.best_indent < 2 || emitter.best_indent > 9 { + emitter.best_indent = 2 + } + if emitter.best_width >= 0 && emitter.best_width <= emitter.best_indent*2 { + emitter.best_width = 80 + } + if emitter.best_width < 0 { + emitter.best_width = 1<<31 - 1 + } + if emitter.line_break == yaml_ANY_BREAK { + emitter.line_break = yaml_LN_BREAK + } + + emitter.indent = -1 + emitter.line = 0 + emitter.column = 0 + emitter.whitespace = true + emitter.indention = true + emitter.space_above = true + emitter.foot_indent = -1 + + if emitter.encoding != yaml_UTF8_ENCODING { + if !yaml_emitter_write_bom(emitter) { + return false + } + } + emitter.state = yaml_EMIT_FIRST_DOCUMENT_START_STATE + return true +} + +// Expect DOCUMENT-START or STREAM-END. +func yaml_emitter_emit_document_start(emitter *yaml_emitter_t, event *yaml_event_t, first bool) bool { + + if event.typ == yaml_DOCUMENT_START_EVENT { + + if event.version_directive != nil { + if !yaml_emitter_analyze_version_directive(emitter, event.version_directive) { + return false + } + } + + for i := 0; i < len(event.tag_directives); i++ { + tag_directive := &event.tag_directives[i] + if !yaml_emitter_analyze_tag_directive(emitter, tag_directive) { + return false + } + if !yaml_emitter_append_tag_directive(emitter, tag_directive, false) { + return false + } + } + + for i := 0; i < len(default_tag_directives); i++ { + tag_directive := &default_tag_directives[i] + if !yaml_emitter_append_tag_directive(emitter, tag_directive, true) { + return false + } + } + + implicit := event.implicit + if !first || emitter.canonical { + implicit = false + } + + if emitter.open_ended && (event.version_directive != nil || len(event.tag_directives) > 0) { + if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if event.version_directive != nil { + implicit = false + if !yaml_emitter_write_indicator(emitter, []byte("%YAML"), true, false, false) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte("1.1"), true, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if len(event.tag_directives) > 0 { + implicit = false + for i := 0; i < len(event.tag_directives); i++ { + tag_directive := &event.tag_directives[i] + if !yaml_emitter_write_indicator(emitter, []byte("%TAG"), true, false, false) { + return false + } + if !yaml_emitter_write_tag_handle(emitter, tag_directive.handle) { + return false + } + if !yaml_emitter_write_tag_content(emitter, tag_directive.prefix, true) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + } + + if yaml_emitter_check_empty_document(emitter) { + implicit = false + } + if !implicit { + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte("---"), true, false, false) { + return false + } + if emitter.canonical || true { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + } + + if len(emitter.head_comment) > 0 { + if !yaml_emitter_process_head_comment(emitter) { + return false + } + if !put_break(emitter) { + return false + } + } + + emitter.state = yaml_EMIT_DOCUMENT_CONTENT_STATE + return true + } + + if event.typ == yaml_STREAM_END_EVENT { + if emitter.open_ended { + if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_flush(emitter) { + return false + } + emitter.state = yaml_EMIT_END_STATE + return true + } + + return yaml_emitter_set_emitter_error(emitter, "expected DOCUMENT-START or STREAM-END") +} + +// yaml_emitter_increase_indent preserves the original signature and delegates to +// yaml_emitter_increase_indent_compact without compact-sequence indentation +func yaml_emitter_increase_indent(emitter *yaml_emitter_t, flow, indentless bool) bool { + return yaml_emitter_increase_indent_compact(emitter, flow, indentless, false) +} + +// yaml_emitter_process_line_comment preserves the original signature and delegates to +// yaml_emitter_process_line_comment_linebreak passing false for linebreak +func yaml_emitter_process_line_comment(emitter *yaml_emitter_t) bool { + return yaml_emitter_process_line_comment_linebreak(emitter, false) +} + +// Expect the root node. +func yaml_emitter_emit_document_content(emitter *yaml_emitter_t, event *yaml_event_t) bool { + emitter.states = append(emitter.states, yaml_EMIT_DOCUMENT_END_STATE) + + if !yaml_emitter_process_head_comment(emitter) { + return false + } + if !yaml_emitter_emit_node(emitter, event, true, false, false, false) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + return true +} + +// Expect DOCUMENT-END. +func yaml_emitter_emit_document_end(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if event.typ != yaml_DOCUMENT_END_EVENT { + return yaml_emitter_set_emitter_error(emitter, "expected DOCUMENT-END") + } + // [Go] Force document foot separation. + emitter.foot_indent = 0 + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + emitter.foot_indent = -1 + if !yaml_emitter_write_indent(emitter) { + return false + } + if !event.implicit { + // [Go] Allocate the slice elsewhere. + if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_flush(emitter) { + return false + } + emitter.state = yaml_EMIT_DOCUMENT_START_STATE + emitter.tag_directives = emitter.tag_directives[:0] + return true +} + +// Expect a flow item node. +func yaml_emitter_emit_flow_sequence_item(emitter *yaml_emitter_t, event *yaml_event_t, first, trail bool) bool { + if first { + if !yaml_emitter_write_indicator(emitter, []byte{'['}, true, true, false) { + return false + } + if !yaml_emitter_increase_indent(emitter, true, false) { + return false + } + emitter.flow_level++ + } + + if event.typ == yaml_SEQUENCE_END_EVENT { + if emitter.canonical && !first && !trail { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + emitter.flow_level-- + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + if emitter.column == 0 || emitter.canonical && !first { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_write_indicator(emitter, []byte{']'}, false, false, false) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + + return true + } + + if !first && !trail { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + + if !yaml_emitter_process_head_comment(emitter) { + return false + } + if emitter.column == 0 { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if emitter.canonical || emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 { + emitter.states = append(emitter.states, yaml_EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE) + } else { + emitter.states = append(emitter.states, yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE) + } + if !yaml_emitter_emit_node(emitter, event, false, true, false, false) { + return false + } + if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + return true +} + +// Expect a flow key node. +func yaml_emitter_emit_flow_mapping_key(emitter *yaml_emitter_t, event *yaml_event_t, first, trail bool) bool { + if first { + if !yaml_emitter_write_indicator(emitter, []byte{'{'}, true, true, false) { + return false + } + if !yaml_emitter_increase_indent(emitter, true, false) { + return false + } + emitter.flow_level++ + } + + if event.typ == yaml_MAPPING_END_EVENT { + if (emitter.canonical || len(emitter.head_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0) && !first && !trail { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + if !yaml_emitter_process_head_comment(emitter) { + return false + } + emitter.flow_level-- + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + if emitter.canonical && !first { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_write_indicator(emitter, []byte{'}'}, false, false, false) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + return true + } + + if !first && !trail { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + + if !yaml_emitter_process_head_comment(emitter) { + return false + } + + if emitter.column == 0 { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if emitter.canonical || emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if !emitter.canonical && yaml_emitter_check_simple_key(emitter) { + emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE) + return yaml_emitter_emit_node(emitter, event, false, false, true, true) + } + if !yaml_emitter_write_indicator(emitter, []byte{'?'}, true, false, false) { + return false + } + emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_VALUE_STATE) + return yaml_emitter_emit_node(emitter, event, false, false, true, false) +} + +// Expect a flow value node. +func yaml_emitter_emit_flow_mapping_value(emitter *yaml_emitter_t, event *yaml_event_t, simple bool) bool { + if simple { + if !yaml_emitter_write_indicator(emitter, []byte{':'}, false, false, false) { + return false + } + } else { + if emitter.canonical || emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_write_indicator(emitter, []byte{':'}, true, false, false) { + return false + } + } + if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 { + emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_TRAIL_KEY_STATE) + } else { + emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_KEY_STATE) + } + if !yaml_emitter_emit_node(emitter, event, false, false, true, false) { + return false + } + if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + return true +} + +// Expect a block item node. +func yaml_emitter_emit_block_sequence_item(emitter *yaml_emitter_t, event *yaml_event_t, first bool) bool { + if first { + // emitter.mapping context tells us if we are currently in a mapping context. + // emiiter.column tells us which column we are in in the yaml output. 0 is the first char of the column. + // emitter.indentation tells us if the last character was an indentation character. + // emitter.compact_sequence_indent tells us if '- ' is considered part of the indentation for sequence elements. + // So, `seq` means that we are in a mapping context, and we are either at the first char of the column or + // the last character was not an indentation character, and we consider '- ' part of the indentation + // for sequence elements. + seq := emitter.mapping_context && (emitter.column == 0 || !emitter.indention) && + emitter.compact_sequence_indent + if !yaml_emitter_increase_indent_compact(emitter, false, false, seq) { + return false + } + } + if event.typ == yaml_SEQUENCE_END_EVENT { + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + return true + } + if !yaml_emitter_process_head_comment(emitter) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte{'-'}, true, false, true) { + return false + } + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE) + if !yaml_emitter_emit_node(emitter, event, false, true, false, false) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + return true +} + +// Expect a block key node. +func yaml_emitter_emit_block_mapping_key(emitter *yaml_emitter_t, event *yaml_event_t, first bool) bool { + if first { + if !yaml_emitter_increase_indent(emitter, false, false) { + return false + } + } + if !yaml_emitter_process_head_comment(emitter) { + return false + } + if event.typ == yaml_MAPPING_END_EVENT { + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + return true + } + if !yaml_emitter_write_indent(emitter) { + return false + } + if len(emitter.line_comment) > 0 { + // [Go] A line comment was provided for the key. That's unusual as the + // scanner associates line comments with the value. Either way, + // save the line comment and render it appropriately later. + emitter.key_line_comment = emitter.line_comment + emitter.line_comment = nil + } + if yaml_emitter_check_simple_key(emitter) { + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE) + return yaml_emitter_emit_node(emitter, event, false, false, true, true) + } + if !yaml_emitter_write_indicator(emitter, []byte{'?'}, true, false, true) { + return false + } + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_VALUE_STATE) + return yaml_emitter_emit_node(emitter, event, false, false, true, false) +} + +// Expect a block value node. +func yaml_emitter_emit_block_mapping_value(emitter *yaml_emitter_t, event *yaml_event_t, simple bool) bool { + if simple { + if !yaml_emitter_write_indicator(emitter, []byte{':'}, false, false, false) { + return false + } + } else { + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte{':'}, true, false, true) { + return false + } + } + if len(emitter.key_line_comment) > 0 { + // [Go] Line comments are generally associated with the value, but when there's + // no value on the same line as a mapping key they end up attached to the + // key itself. + if event.typ == yaml_SCALAR_EVENT { + if len(emitter.line_comment) == 0 { + // A scalar is coming and it has no line comments by itself yet, + // so just let it handle the line comment as usual. If it has a + // line comment, we can't have both so the one from the key is lost. + emitter.line_comment = emitter.key_line_comment + emitter.key_line_comment = nil + } + } else if event.sequence_style() != yaml_FLOW_SEQUENCE_STYLE && (event.typ == yaml_MAPPING_START_EVENT || event.typ == yaml_SEQUENCE_START_EVENT) { + // An indented block follows, so write the comment right now. + emitter.line_comment, emitter.key_line_comment = emitter.key_line_comment, emitter.line_comment + if !yaml_emitter_process_line_comment(emitter) { + return false + } + emitter.line_comment, emitter.key_line_comment = emitter.key_line_comment, emitter.line_comment + } + } + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_KEY_STATE) + if !yaml_emitter_emit_node(emitter, event, false, false, true, false) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + return true +} + +func yaml_emitter_silent_nil_event(emitter *yaml_emitter_t, event *yaml_event_t) bool { + return event.typ == yaml_SCALAR_EVENT && event.implicit && !emitter.canonical && len(emitter.scalar_data.value) == 0 +} + +// Expect a node. +func yaml_emitter_emit_node(emitter *yaml_emitter_t, event *yaml_event_t, + root bool, sequence bool, mapping bool, simple_key bool) bool { + + emitter.root_context = root + emitter.sequence_context = sequence + emitter.mapping_context = mapping + emitter.simple_key_context = simple_key + + switch event.typ { + case yaml_ALIAS_EVENT: + return yaml_emitter_emit_alias(emitter, event) + case yaml_SCALAR_EVENT: + return yaml_emitter_emit_scalar(emitter, event) + case yaml_SEQUENCE_START_EVENT: + return yaml_emitter_emit_sequence_start(emitter, event) + case yaml_MAPPING_START_EVENT: + return yaml_emitter_emit_mapping_start(emitter, event) + default: + return yaml_emitter_set_emitter_error(emitter, + fmt.Sprintf("expected SCALAR, SEQUENCE-START, MAPPING-START, or ALIAS, but got %v", event.typ)) + } +} + +// Expect ALIAS. +func yaml_emitter_emit_alias(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_process_anchor(emitter) { + return false + } + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + return true +} + +// Expect SCALAR. +func yaml_emitter_emit_scalar(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_select_scalar_style(emitter, event) { + return false + } + if !yaml_emitter_process_anchor(emitter) { + return false + } + if !yaml_emitter_process_tag(emitter) { + return false + } + if !yaml_emitter_increase_indent(emitter, true, false) { + return false + } + if !yaml_emitter_process_scalar(emitter) { + return false + } + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + return true +} + +// Expect SEQUENCE-START. +func yaml_emitter_emit_sequence_start(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_process_anchor(emitter) { + return false + } + if !yaml_emitter_process_tag(emitter) { + return false + } + if emitter.flow_level > 0 || emitter.canonical || event.sequence_style() == yaml_FLOW_SEQUENCE_STYLE || + yaml_emitter_check_empty_sequence(emitter) { + emitter.state = yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE + } else { + emitter.state = yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE + } + return true +} + +// Expect MAPPING-START. +func yaml_emitter_emit_mapping_start(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_process_anchor(emitter) { + return false + } + if !yaml_emitter_process_tag(emitter) { + return false + } + if emitter.flow_level > 0 || emitter.canonical || event.mapping_style() == yaml_FLOW_MAPPING_STYLE || + yaml_emitter_check_empty_mapping(emitter) { + emitter.state = yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE + } else { + emitter.state = yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE + } + return true +} + +// Check if the document content is an empty scalar. +func yaml_emitter_check_empty_document(emitter *yaml_emitter_t) bool { + return false // [Go] Huh? +} + +// Check if the next events represent an empty sequence. +func yaml_emitter_check_empty_sequence(emitter *yaml_emitter_t) bool { + if len(emitter.events)-emitter.events_head < 2 { + return false + } + return emitter.events[emitter.events_head].typ == yaml_SEQUENCE_START_EVENT && + emitter.events[emitter.events_head+1].typ == yaml_SEQUENCE_END_EVENT +} + +// Check if the next events represent an empty mapping. +func yaml_emitter_check_empty_mapping(emitter *yaml_emitter_t) bool { + if len(emitter.events)-emitter.events_head < 2 { + return false + } + return emitter.events[emitter.events_head].typ == yaml_MAPPING_START_EVENT && + emitter.events[emitter.events_head+1].typ == yaml_MAPPING_END_EVENT +} + +// Check if the next node can be expressed as a simple key. +func yaml_emitter_check_simple_key(emitter *yaml_emitter_t) bool { + length := 0 + switch emitter.events[emitter.events_head].typ { + case yaml_ALIAS_EVENT: + length += len(emitter.anchor_data.anchor) + case yaml_SCALAR_EVENT: + if emitter.scalar_data.multiline { + return false + } + length += len(emitter.anchor_data.anchor) + + len(emitter.tag_data.handle) + + len(emitter.tag_data.suffix) + + len(emitter.scalar_data.value) + case yaml_SEQUENCE_START_EVENT: + if !yaml_emitter_check_empty_sequence(emitter) { + return false + } + length += len(emitter.anchor_data.anchor) + + len(emitter.tag_data.handle) + + len(emitter.tag_data.suffix) + case yaml_MAPPING_START_EVENT: + if !yaml_emitter_check_empty_mapping(emitter) { + return false + } + length += len(emitter.anchor_data.anchor) + + len(emitter.tag_data.handle) + + len(emitter.tag_data.suffix) + default: + return false + } + return length <= 128 +} + +// Determine an acceptable scalar style. +func yaml_emitter_select_scalar_style(emitter *yaml_emitter_t, event *yaml_event_t) bool { + + no_tag := len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0 + if no_tag && !event.implicit && !event.quoted_implicit { + return yaml_emitter_set_emitter_error(emitter, "neither tag nor implicit flags are specified") + } + + style := event.scalar_style() + if style == yaml_ANY_SCALAR_STYLE { + style = yaml_PLAIN_SCALAR_STYLE + } + if emitter.canonical { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + if emitter.simple_key_context && emitter.scalar_data.multiline { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + + if style == yaml_PLAIN_SCALAR_STYLE { + if emitter.flow_level > 0 && !emitter.scalar_data.flow_plain_allowed || + emitter.flow_level == 0 && !emitter.scalar_data.block_plain_allowed { + style = yaml_SINGLE_QUOTED_SCALAR_STYLE + } + if len(emitter.scalar_data.value) == 0 && (emitter.flow_level > 0 || emitter.simple_key_context) { + style = yaml_SINGLE_QUOTED_SCALAR_STYLE + } + if no_tag && !event.implicit { + style = yaml_SINGLE_QUOTED_SCALAR_STYLE + } + } + if style == yaml_SINGLE_QUOTED_SCALAR_STYLE { + if !emitter.scalar_data.single_quoted_allowed { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + } + if style == yaml_LITERAL_SCALAR_STYLE || style == yaml_FOLDED_SCALAR_STYLE { + if !emitter.scalar_data.block_allowed || emitter.flow_level > 0 || emitter.simple_key_context { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + } + + if no_tag && !event.quoted_implicit && style != yaml_PLAIN_SCALAR_STYLE { + emitter.tag_data.handle = []byte{'!'} + } + emitter.scalar_data.style = style + return true +} + +// Write an anchor. +func yaml_emitter_process_anchor(emitter *yaml_emitter_t) bool { + if emitter.anchor_data.anchor == nil { + return true + } + c := []byte{'&'} + if emitter.anchor_data.alias { + c[0] = '*' + } + if !yaml_emitter_write_indicator(emitter, c, true, false, false) { + return false + } + return yaml_emitter_write_anchor(emitter, emitter.anchor_data.anchor) +} + +// Write a tag. +func yaml_emitter_process_tag(emitter *yaml_emitter_t) bool { + if len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0 { + return true + } + if len(emitter.tag_data.handle) > 0 { + if !yaml_emitter_write_tag_handle(emitter, emitter.tag_data.handle) { + return false + } + if len(emitter.tag_data.suffix) > 0 { + if !yaml_emitter_write_tag_content(emitter, emitter.tag_data.suffix, false) { + return false + } + } + } else { + // [Go] Allocate these slices elsewhere. + if !yaml_emitter_write_indicator(emitter, []byte("!<"), true, false, false) { + return false + } + if !yaml_emitter_write_tag_content(emitter, emitter.tag_data.suffix, false) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte{'>'}, false, false, false) { + return false + } + } + return true +} + +// Write a scalar. +func yaml_emitter_process_scalar(emitter *yaml_emitter_t) bool { + switch emitter.scalar_data.style { + case yaml_PLAIN_SCALAR_STYLE: + return yaml_emitter_write_plain_scalar(emitter, emitter.scalar_data.value, !emitter.simple_key_context) + + case yaml_SINGLE_QUOTED_SCALAR_STYLE: + return yaml_emitter_write_single_quoted_scalar(emitter, emitter.scalar_data.value, !emitter.simple_key_context) + + case yaml_DOUBLE_QUOTED_SCALAR_STYLE: + return yaml_emitter_write_double_quoted_scalar(emitter, emitter.scalar_data.value, !emitter.simple_key_context) + + case yaml_LITERAL_SCALAR_STYLE: + return yaml_emitter_write_literal_scalar(emitter, emitter.scalar_data.value) + + case yaml_FOLDED_SCALAR_STYLE: + return yaml_emitter_write_folded_scalar(emitter, emitter.scalar_data.value) + } + panic("unknown scalar style") +} + +// Write a head comment. +func yaml_emitter_process_head_comment(emitter *yaml_emitter_t) bool { + if len(emitter.tail_comment) > 0 { + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_comment(emitter, emitter.tail_comment) { + return false + } + emitter.tail_comment = emitter.tail_comment[:0] + emitter.foot_indent = emitter.indent + if emitter.foot_indent < 0 { + emitter.foot_indent = 0 + } + } + + if len(emitter.head_comment) == 0 { + return true + } + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_comment(emitter, emitter.head_comment) { + return false + } + emitter.head_comment = emitter.head_comment[:0] + return true +} + +// Write an line comment. +func yaml_emitter_process_line_comment_linebreak(emitter *yaml_emitter_t, linebreak bool) bool { + if len(emitter.line_comment) == 0 { + // The next 3 lines are needed to resolve an issue with leading newlines + // See https://github.com/go-yaml/yaml/issues/755 + // When linebreak is set to true, put_break will be called and will add + // the needed newline. + if linebreak && !put_break(emitter) { + return false + } + return true + } + if !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + if !yaml_emitter_write_comment(emitter, emitter.line_comment) { + return false + } + emitter.line_comment = emitter.line_comment[:0] + return true +} + +// Write a foot comment. +func yaml_emitter_process_foot_comment(emitter *yaml_emitter_t) bool { + if len(emitter.foot_comment) == 0 { + return true + } + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_comment(emitter, emitter.foot_comment) { + return false + } + emitter.foot_comment = emitter.foot_comment[:0] + emitter.foot_indent = emitter.indent + if emitter.foot_indent < 0 { + emitter.foot_indent = 0 + } + return true +} + +// Check if a %YAML directive is valid. +func yaml_emitter_analyze_version_directive(emitter *yaml_emitter_t, version_directive *yaml_version_directive_t) bool { + if version_directive.major != 1 || version_directive.minor != 1 { + return yaml_emitter_set_emitter_error(emitter, "incompatible %YAML directive") + } + return true +} + +// Check if a %TAG directive is valid. +func yaml_emitter_analyze_tag_directive(emitter *yaml_emitter_t, tag_directive *yaml_tag_directive_t) bool { + handle := tag_directive.handle + prefix := tag_directive.prefix + if len(handle) == 0 { + return yaml_emitter_set_emitter_error(emitter, "tag handle must not be empty") + } + if handle[0] != '!' { + return yaml_emitter_set_emitter_error(emitter, "tag handle must start with '!'") + } + if handle[len(handle)-1] != '!' { + return yaml_emitter_set_emitter_error(emitter, "tag handle must end with '!'") + } + for i := 1; i < len(handle)-1; i += width(handle[i]) { + if !is_alpha(handle, i) { + return yaml_emitter_set_emitter_error(emitter, "tag handle must contain alphanumerical characters only") + } + } + if len(prefix) == 0 { + return yaml_emitter_set_emitter_error(emitter, "tag prefix must not be empty") + } + return true +} + +// Check if an anchor is valid. +func yaml_emitter_analyze_anchor(emitter *yaml_emitter_t, anchor []byte, alias bool) bool { + if len(anchor) == 0 { + problem := "anchor value must not be empty" + if alias { + problem = "alias value must not be empty" + } + return yaml_emitter_set_emitter_error(emitter, problem) + } + for i := 0; i < len(anchor); i += width(anchor[i]) { + if !is_alpha(anchor, i) { + problem := "anchor value must contain alphanumerical characters only" + if alias { + problem = "alias value must contain alphanumerical characters only" + } + return yaml_emitter_set_emitter_error(emitter, problem) + } + } + emitter.anchor_data.anchor = anchor + emitter.anchor_data.alias = alias + return true +} + +// Check if a tag is valid. +func yaml_emitter_analyze_tag(emitter *yaml_emitter_t, tag []byte) bool { + if len(tag) == 0 { + return yaml_emitter_set_emitter_error(emitter, "tag value must not be empty") + } + for i := 0; i < len(emitter.tag_directives); i++ { + tag_directive := &emitter.tag_directives[i] + if bytes.HasPrefix(tag, tag_directive.prefix) { + emitter.tag_data.handle = tag_directive.handle + emitter.tag_data.suffix = tag[len(tag_directive.prefix):] + return true + } + } + emitter.tag_data.suffix = tag + return true +} + +// Check if a scalar is valid. +func yaml_emitter_analyze_scalar(emitter *yaml_emitter_t, value []byte) bool { + var ( + block_indicators = false + flow_indicators = false + line_breaks = false + special_characters = false + tab_characters = false + + leading_space = false + leading_break = false + trailing_space = false + trailing_break = false + break_space = false + space_break = false + + preceded_by_whitespace = false + followed_by_whitespace = false + previous_space = false + previous_break = false + ) + + emitter.scalar_data.value = value + + if len(value) == 0 { + emitter.scalar_data.multiline = false + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = true + emitter.scalar_data.single_quoted_allowed = true + emitter.scalar_data.block_allowed = false + return true + } + + if len(value) >= 3 && ((value[0] == '-' && value[1] == '-' && value[2] == '-') || (value[0] == '.' && value[1] == '.' && value[2] == '.')) { + block_indicators = true + flow_indicators = true + } + + preceded_by_whitespace = true + for i, w := 0, 0; i < len(value); i += w { + w = width(value[i]) + followed_by_whitespace = i+w >= len(value) || is_blank(value, i+w) + + if i == 0 { + switch value[i] { + case '#', ',', '[', ']', '{', '}', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`': + flow_indicators = true + block_indicators = true + case '?', ':': + flow_indicators = true + if followed_by_whitespace { + block_indicators = true + } + case '-': + if followed_by_whitespace { + flow_indicators = true + block_indicators = true + } + } + } else { + switch value[i] { + case ',', '?', '[', ']', '{', '}': + flow_indicators = true + case ':': + flow_indicators = true + if followed_by_whitespace { + block_indicators = true + } + case '#': + if preceded_by_whitespace { + flow_indicators = true + block_indicators = true + } + } + } + + if value[i] == '\t' { + tab_characters = true + } else if !is_printable(value, i) || !is_ascii(value, i) && !emitter.unicode { + special_characters = true + } + if is_space(value, i) { + if i == 0 { + leading_space = true + } + if i+width(value[i]) == len(value) { + trailing_space = true + } + if previous_break { + break_space = true + } + previous_space = true + previous_break = false + } else if is_break(value, i) { + line_breaks = true + if i == 0 { + leading_break = true + } + if i+width(value[i]) == len(value) { + trailing_break = true + } + if previous_space { + space_break = true + } + previous_space = false + previous_break = true + } else { + previous_space = false + previous_break = false + } + + // [Go]: Why 'z'? Couldn't be the end of the string as that's the loop condition. + preceded_by_whitespace = is_blankz(value, i) + } + + emitter.scalar_data.multiline = line_breaks + emitter.scalar_data.flow_plain_allowed = true + emitter.scalar_data.block_plain_allowed = true + emitter.scalar_data.single_quoted_allowed = true + emitter.scalar_data.block_allowed = true + + if leading_space || leading_break || trailing_space || trailing_break { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + } + if trailing_space { + emitter.scalar_data.block_allowed = false + } + if break_space { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + emitter.scalar_data.single_quoted_allowed = false + } + if space_break || tab_characters || special_characters { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + emitter.scalar_data.single_quoted_allowed = false + } + if space_break || special_characters { + emitter.scalar_data.block_allowed = false + } + if line_breaks { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + } + if flow_indicators { + emitter.scalar_data.flow_plain_allowed = false + } + if block_indicators { + emitter.scalar_data.block_plain_allowed = false + } + return true +} + +// Check if the event data is valid. +func yaml_emitter_analyze_event(emitter *yaml_emitter_t, event *yaml_event_t) bool { + + emitter.anchor_data.anchor = nil + emitter.tag_data.handle = nil + emitter.tag_data.suffix = nil + emitter.scalar_data.value = nil + + if len(event.head_comment) > 0 { + emitter.head_comment = event.head_comment + } + if len(event.line_comment) > 0 { + emitter.line_comment = event.line_comment + } + if len(event.foot_comment) > 0 { + emitter.foot_comment = event.foot_comment + } + if len(event.tail_comment) > 0 { + emitter.tail_comment = event.tail_comment + } + + switch event.typ { + case yaml_ALIAS_EVENT: + if !yaml_emitter_analyze_anchor(emitter, event.anchor, true) { + return false + } + + case yaml_SCALAR_EVENT: + if len(event.anchor) > 0 { + if !yaml_emitter_analyze_anchor(emitter, event.anchor, false) { + return false + } + } + if len(event.tag) > 0 && (emitter.canonical || (!event.implicit && !event.quoted_implicit)) { + if !yaml_emitter_analyze_tag(emitter, event.tag) { + return false + } + } + if !yaml_emitter_analyze_scalar(emitter, event.value) { + return false + } + + case yaml_SEQUENCE_START_EVENT: + if len(event.anchor) > 0 { + if !yaml_emitter_analyze_anchor(emitter, event.anchor, false) { + return false + } + } + if len(event.tag) > 0 && (emitter.canonical || !event.implicit) { + if !yaml_emitter_analyze_tag(emitter, event.tag) { + return false + } + } + + case yaml_MAPPING_START_EVENT: + if len(event.anchor) > 0 { + if !yaml_emitter_analyze_anchor(emitter, event.anchor, false) { + return false + } + } + if len(event.tag) > 0 && (emitter.canonical || !event.implicit) { + if !yaml_emitter_analyze_tag(emitter, event.tag) { + return false + } + } + } + return true +} + +// Write the BOM character. +func yaml_emitter_write_bom(emitter *yaml_emitter_t) bool { + if !flush(emitter) { + return false + } + pos := emitter.buffer_pos + emitter.buffer[pos+0] = '\xEF' + emitter.buffer[pos+1] = '\xBB' + emitter.buffer[pos+2] = '\xBF' + emitter.buffer_pos += 3 + return true +} + +func yaml_emitter_write_indent(emitter *yaml_emitter_t) bool { + indent := emitter.indent + if indent < 0 { + indent = 0 + } + if !emitter.indention || emitter.column > indent || (emitter.column == indent && !emitter.whitespace) { + if !put_break(emitter) { + return false + } + } + if emitter.foot_indent == indent { + if !put_break(emitter) { + return false + } + } + for emitter.column < indent { + if !put(emitter, ' ') { + return false + } + } + emitter.whitespace = true + //emitter.indention = true + emitter.space_above = false + emitter.foot_indent = -1 + return true +} + +func yaml_emitter_write_indicator(emitter *yaml_emitter_t, indicator []byte, need_whitespace, is_whitespace, is_indention bool) bool { + if need_whitespace && !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + if !write_all(emitter, indicator) { + return false + } + emitter.whitespace = is_whitespace + emitter.indention = (emitter.indention && is_indention) + emitter.open_ended = false + return true +} + +func yaml_emitter_write_anchor(emitter *yaml_emitter_t, value []byte) bool { + if !write_all(emitter, value) { + return false + } + emitter.whitespace = false + emitter.indention = false + return true +} + +func yaml_emitter_write_tag_handle(emitter *yaml_emitter_t, value []byte) bool { + if !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + if !write_all(emitter, value) { + return false + } + emitter.whitespace = false + emitter.indention = false + return true +} + +func yaml_emitter_write_tag_content(emitter *yaml_emitter_t, value []byte, need_whitespace bool) bool { + if need_whitespace && !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + for i := 0; i < len(value); { + var must_write bool + switch value[i] { + case ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '_', '.', '~', '*', '\'', '(', ')', '[', ']': + must_write = true + default: + must_write = is_alpha(value, i) + } + if must_write { + if !write(emitter, value, &i) { + return false + } + } else { + w := width(value[i]) + for k := 0; k < w; k++ { + octet := value[i] + i++ + if !put(emitter, '%') { + return false + } + + c := octet >> 4 + if c < 10 { + c += '0' + } else { + c += 'A' - 10 + } + if !put(emitter, c) { + return false + } + + c = octet & 0x0f + if c < 10 { + c += '0' + } else { + c += 'A' - 10 + } + if !put(emitter, c) { + return false + } + } + } + } + emitter.whitespace = false + emitter.indention = false + return true +} + +func yaml_emitter_write_plain_scalar(emitter *yaml_emitter_t, value []byte, allow_breaks bool) bool { + if len(value) > 0 && !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + + spaces := false + breaks := false + for i := 0; i < len(value); { + if is_space(value, i) { + if allow_breaks && !spaces && emitter.column > emitter.best_width && !is_space(value, i+1) { + if !yaml_emitter_write_indent(emitter) { + return false + } + i += width(value[i]) + } else { + if !write(emitter, value, &i) { + return false + } + } + spaces = true + } else if is_break(value, i) { + if !breaks && value[i] == '\n' { + if !put_break(emitter) { + return false + } + } + if !write_break(emitter, value, &i) { + return false + } + //emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !write(emitter, value, &i) { + return false + } + emitter.indention = false + spaces = false + breaks = false + } + } + + if len(value) > 0 { + emitter.whitespace = false + } + emitter.indention = false + if emitter.root_context { + emitter.open_ended = true + } + + return true +} + +func yaml_emitter_write_single_quoted_scalar(emitter *yaml_emitter_t, value []byte, allow_breaks bool) bool { + + if !yaml_emitter_write_indicator(emitter, []byte{'\''}, true, false, false) { + return false + } + + spaces := false + breaks := false + for i := 0; i < len(value); { + if is_space(value, i) { + if allow_breaks && !spaces && emitter.column > emitter.best_width && i > 0 && i < len(value)-1 && !is_space(value, i+1) { + if !yaml_emitter_write_indent(emitter) { + return false + } + i += width(value[i]) + } else { + if !write(emitter, value, &i) { + return false + } + } + spaces = true + } else if is_break(value, i) { + if !breaks && value[i] == '\n' { + if !put_break(emitter) { + return false + } + } + if !write_break(emitter, value, &i) { + return false + } + //emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if value[i] == '\'' { + if !put(emitter, '\'') { + return false + } + } + if !write(emitter, value, &i) { + return false + } + emitter.indention = false + spaces = false + breaks = false + } + } + if !yaml_emitter_write_indicator(emitter, []byte{'\''}, false, false, false) { + return false + } + emitter.whitespace = false + emitter.indention = false + return true +} + +func yaml_emitter_write_double_quoted_scalar(emitter *yaml_emitter_t, value []byte, allow_breaks bool) bool { + spaces := false + if !yaml_emitter_write_indicator(emitter, []byte{'"'}, true, false, false) { + return false + } + + for i := 0; i < len(value); { + if !is_printable(value, i) || (!emitter.unicode && !is_ascii(value, i)) || + is_bom(value, i) || is_break(value, i) || + value[i] == '"' || value[i] == '\\' { + + octet := value[i] + + var w int + var v rune + switch { + case octet&0x80 == 0x00: + w, v = 1, rune(octet&0x7F) + case octet&0xE0 == 0xC0: + w, v = 2, rune(octet&0x1F) + case octet&0xF0 == 0xE0: + w, v = 3, rune(octet&0x0F) + case octet&0xF8 == 0xF0: + w, v = 4, rune(octet&0x07) + } + for k := 1; k < w; k++ { + octet = value[i+k] + v = (v << 6) + (rune(octet) & 0x3F) + } + i += w + + if !put(emitter, '\\') { + return false + } + + var ok bool + switch v { + case 0x00: + ok = put(emitter, '0') + case 0x07: + ok = put(emitter, 'a') + case 0x08: + ok = put(emitter, 'b') + case 0x09: + ok = put(emitter, 't') + case 0x0A: + ok = put(emitter, 'n') + case 0x0b: + ok = put(emitter, 'v') + case 0x0c: + ok = put(emitter, 'f') + case 0x0d: + ok = put(emitter, 'r') + case 0x1b: + ok = put(emitter, 'e') + case 0x22: + ok = put(emitter, '"') + case 0x5c: + ok = put(emitter, '\\') + case 0x85: + ok = put(emitter, 'N') + case 0xA0: + ok = put(emitter, '_') + case 0x2028: + ok = put(emitter, 'L') + case 0x2029: + ok = put(emitter, 'P') + default: + if v <= 0xFF { + ok = put(emitter, 'x') + w = 2 + } else if v <= 0xFFFF { + ok = put(emitter, 'u') + w = 4 + } else { + ok = put(emitter, 'U') + w = 8 + } + for k := (w - 1) * 4; ok && k >= 0; k -= 4 { + digit := byte((v >> uint(k)) & 0x0F) + if digit < 10 { + ok = put(emitter, digit+'0') + } else { + ok = put(emitter, digit+'A'-10) + } + } + } + if !ok { + return false + } + spaces = false + } else if is_space(value, i) { + if allow_breaks && !spaces && emitter.column > emitter.best_width && i > 0 && i < len(value)-1 { + if !yaml_emitter_write_indent(emitter) { + return false + } + if is_space(value, i+1) { + if !put(emitter, '\\') { + return false + } + } + i += width(value[i]) + } else if !write(emitter, value, &i) { + return false + } + spaces = true + } else { + if !write(emitter, value, &i) { + return false + } + spaces = false + } + } + if !yaml_emitter_write_indicator(emitter, []byte{'"'}, false, false, false) { + return false + } + emitter.whitespace = false + emitter.indention = false + return true +} + +func yaml_emitter_write_block_scalar_hints(emitter *yaml_emitter_t, value []byte) bool { + if is_space(value, 0) || is_break(value, 0) { + indent_hint := []byte{'0' + byte(emitter.best_indent)} + if !yaml_emitter_write_indicator(emitter, indent_hint, false, false, false) { + return false + } + } + + emitter.open_ended = false + + var chomp_hint [1]byte + if len(value) == 0 { + chomp_hint[0] = '-' + } else { + i := len(value) - 1 + for value[i]&0xC0 == 0x80 { + i-- + } + if !is_break(value, i) { + chomp_hint[0] = '-' + } else if i == 0 { + chomp_hint[0] = '+' + emitter.open_ended = true + } else { + i-- + for value[i]&0xC0 == 0x80 { + i-- + } + if is_break(value, i) { + chomp_hint[0] = '+' + emitter.open_ended = true + } + } + } + if chomp_hint[0] != 0 { + if !yaml_emitter_write_indicator(emitter, chomp_hint[:], false, false, false) { + return false + } + } + return true +} + +func yaml_emitter_write_literal_scalar(emitter *yaml_emitter_t, value []byte) bool { + if !yaml_emitter_write_indicator(emitter, []byte{'|'}, true, false, false) { + return false + } + if !yaml_emitter_write_block_scalar_hints(emitter, value) { + return false + } + if !yaml_emitter_process_line_comment_linebreak(emitter, true) { + return false + } + //emitter.indention = true + emitter.whitespace = true + breaks := true + for i := 0; i < len(value); { + if is_break(value, i) { + if !write_break(emitter, value, &i) { + return false + } + //emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !write(emitter, value, &i) { + return false + } + emitter.indention = false + breaks = false + } + } + + return true +} + +func yaml_emitter_write_folded_scalar(emitter *yaml_emitter_t, value []byte) bool { + if !yaml_emitter_write_indicator(emitter, []byte{'>'}, true, false, false) { + return false + } + if !yaml_emitter_write_block_scalar_hints(emitter, value) { + return false + } + if !yaml_emitter_process_line_comment_linebreak(emitter, true) { + return false + } + + //emitter.indention = true + emitter.whitespace = true + + breaks := true + leading_spaces := true + for i := 0; i < len(value); { + if is_break(value, i) { + if !breaks && !leading_spaces && value[i] == '\n' { + k := 0 + for is_break(value, k) { + k += width(value[k]) + } + if !is_blankz(value, k) { + if !put_break(emitter) { + return false + } + } + } + if !write_break(emitter, value, &i) { + return false + } + //emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + leading_spaces = is_blank(value, i) + } + if !breaks && is_space(value, i) && !is_space(value, i+1) && emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + i += width(value[i]) + } else { + if !write(emitter, value, &i) { + return false + } + } + emitter.indention = false + breaks = false + } + } + return true +} + +func yaml_emitter_write_comment(emitter *yaml_emitter_t, comment []byte) bool { + breaks := false + pound := false + for i := 0; i < len(comment); { + if is_break(comment, i) { + if !write_break(emitter, comment, &i) { + return false + } + //emitter.indention = true + breaks = true + pound = false + } else { + if breaks && !yaml_emitter_write_indent(emitter) { + return false + } + if !pound { + if comment[i] != '#' && (!put(emitter, '#') || !put(emitter, ' ')) { + return false + } + pound = true + } + if !write(emitter, comment, &i) { + return false + } + emitter.indention = false + breaks = false + } + } + if !breaks && !put_break(emitter) { + return false + } + + emitter.whitespace = true + //emitter.indention = true + return true +} diff --git a/vendor/go.yaml.in/yaml/v3/encode.go b/vendor/go.yaml.in/yaml/v3/encode.go new file mode 100644 index 000000000..de9e72a3e --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/encode.go @@ -0,0 +1,577 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package yaml + +import ( + "encoding" + "fmt" + "io" + "reflect" + "regexp" + "sort" + "strconv" + "strings" + "time" + "unicode/utf8" +) + +type encoder struct { + emitter yaml_emitter_t + event yaml_event_t + out []byte + flow bool + indent int + doneInit bool +} + +func newEncoder() *encoder { + e := &encoder{} + yaml_emitter_initialize(&e.emitter) + yaml_emitter_set_output_string(&e.emitter, &e.out) + yaml_emitter_set_unicode(&e.emitter, true) + return e +} + +func newEncoderWithWriter(w io.Writer) *encoder { + e := &encoder{} + yaml_emitter_initialize(&e.emitter) + yaml_emitter_set_output_writer(&e.emitter, w) + yaml_emitter_set_unicode(&e.emitter, true) + return e +} + +func (e *encoder) init() { + if e.doneInit { + return + } + if e.indent == 0 { + e.indent = 4 + } + e.emitter.best_indent = e.indent + yaml_stream_start_event_initialize(&e.event, yaml_UTF8_ENCODING) + e.emit() + e.doneInit = true +} + +func (e *encoder) finish() { + e.emitter.open_ended = false + yaml_stream_end_event_initialize(&e.event) + e.emit() +} + +func (e *encoder) destroy() { + yaml_emitter_delete(&e.emitter) +} + +func (e *encoder) emit() { + // This will internally delete the e.event value. + e.must(yaml_emitter_emit(&e.emitter, &e.event)) +} + +func (e *encoder) must(ok bool) { + if !ok { + msg := e.emitter.problem + if msg == "" { + msg = "unknown problem generating YAML content" + } + failf("%s", msg) + } +} + +func (e *encoder) marshalDoc(tag string, in reflect.Value) { + e.init() + var node *Node + if in.IsValid() { + node, _ = in.Interface().(*Node) + } + if node != nil && node.Kind == DocumentNode { + e.nodev(in) + } else { + yaml_document_start_event_initialize(&e.event, nil, nil, true) + e.emit() + e.marshal(tag, in) + yaml_document_end_event_initialize(&e.event, true) + e.emit() + } +} + +func (e *encoder) marshal(tag string, in reflect.Value) { + tag = shortTag(tag) + if !in.IsValid() || in.Kind() == reflect.Ptr && in.IsNil() { + e.nilv() + return + } + iface := in.Interface() + switch value := iface.(type) { + case *Node: + e.nodev(in) + return + case Node: + if !in.CanAddr() { + var n = reflect.New(in.Type()).Elem() + n.Set(in) + in = n + } + e.nodev(in.Addr()) + return + case time.Time: + e.timev(tag, in) + return + case *time.Time: + e.timev(tag, in.Elem()) + return + case time.Duration: + e.stringv(tag, reflect.ValueOf(value.String())) + return + case Marshaler: + v, err := value.MarshalYAML() + if err != nil { + fail(err) + } + if v == nil { + e.nilv() + return + } + e.marshal(tag, reflect.ValueOf(v)) + return + case encoding.TextMarshaler: + text, err := value.MarshalText() + if err != nil { + fail(err) + } + in = reflect.ValueOf(string(text)) + case nil: + e.nilv() + return + } + switch in.Kind() { + case reflect.Interface: + e.marshal(tag, in.Elem()) + case reflect.Map: + e.mapv(tag, in) + case reflect.Ptr: + e.marshal(tag, in.Elem()) + case reflect.Struct: + e.structv(tag, in) + case reflect.Slice, reflect.Array: + e.slicev(tag, in) + case reflect.String: + e.stringv(tag, in) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + e.intv(tag, in) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + e.uintv(tag, in) + case reflect.Float32, reflect.Float64: + e.floatv(tag, in) + case reflect.Bool: + e.boolv(tag, in) + default: + panic("cannot marshal type: " + in.Type().String()) + } +} + +func (e *encoder) mapv(tag string, in reflect.Value) { + e.mappingv(tag, func() { + keys := keyList(in.MapKeys()) + sort.Sort(keys) + for _, k := range keys { + e.marshal("", k) + e.marshal("", in.MapIndex(k)) + } + }) +} + +func (e *encoder) fieldByIndex(v reflect.Value, index []int) (field reflect.Value) { + for _, num := range index { + for { + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + continue + } + break + } + v = v.Field(num) + } + return v +} + +func (e *encoder) structv(tag string, in reflect.Value) { + sinfo, err := getStructInfo(in.Type()) + if err != nil { + panic(err) + } + e.mappingv(tag, func() { + for _, info := range sinfo.FieldsList { + var value reflect.Value + if info.Inline == nil { + value = in.Field(info.Num) + } else { + value = e.fieldByIndex(in, info.Inline) + if !value.IsValid() { + continue + } + } + if info.OmitEmpty && isZero(value) { + continue + } + e.marshal("", reflect.ValueOf(info.Key)) + e.flow = info.Flow + e.marshal("", value) + } + if sinfo.InlineMap >= 0 { + m := in.Field(sinfo.InlineMap) + if m.Len() > 0 { + e.flow = false + keys := keyList(m.MapKeys()) + sort.Sort(keys) + for _, k := range keys { + if _, found := sinfo.FieldsMap[k.String()]; found { + panic(fmt.Sprintf("cannot have key %q in inlined map: conflicts with struct field", k.String())) + } + e.marshal("", k) + e.flow = false + e.marshal("", m.MapIndex(k)) + } + } + } + }) +} + +func (e *encoder) mappingv(tag string, f func()) { + implicit := tag == "" + style := yaml_BLOCK_MAPPING_STYLE + if e.flow { + e.flow = false + style = yaml_FLOW_MAPPING_STYLE + } + yaml_mapping_start_event_initialize(&e.event, nil, []byte(tag), implicit, style) + e.emit() + f() + yaml_mapping_end_event_initialize(&e.event) + e.emit() +} + +func (e *encoder) slicev(tag string, in reflect.Value) { + implicit := tag == "" + style := yaml_BLOCK_SEQUENCE_STYLE + if e.flow { + e.flow = false + style = yaml_FLOW_SEQUENCE_STYLE + } + e.must(yaml_sequence_start_event_initialize(&e.event, nil, []byte(tag), implicit, style)) + e.emit() + n := in.Len() + for i := 0; i < n; i++ { + e.marshal("", in.Index(i)) + } + e.must(yaml_sequence_end_event_initialize(&e.event)) + e.emit() +} + +// isBase60 returns whether s is in base 60 notation as defined in YAML 1.1. +// +// The base 60 float notation in YAML 1.1 is a terrible idea and is unsupported +// in YAML 1.2 and by this package, but these should be marshalled quoted for +// the time being for compatibility with other parsers. +func isBase60Float(s string) (result bool) { + // Fast path. + if s == "" { + return false + } + c := s[0] + if !(c == '+' || c == '-' || c >= '0' && c <= '9') || strings.IndexByte(s, ':') < 0 { + return false + } + // Do the full match. + return base60float.MatchString(s) +} + +// From http://yaml.org/type/float.html, except the regular expression there +// is bogus. In practice parsers do not enforce the "\.[0-9_]*" suffix. +var base60float = regexp.MustCompile(`^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+(?:\.[0-9_]*)?$`) + +// isOldBool returns whether s is bool notation as defined in YAML 1.1. +// +// We continue to force strings that YAML 1.1 would interpret as booleans to be +// rendered as quotes strings so that the marshalled output valid for YAML 1.1 +// parsing. +func isOldBool(s string) (result bool) { + switch s { + case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON", + "n", "N", "no", "No", "NO", "off", "Off", "OFF": + return true + default: + return false + } +} + +func (e *encoder) stringv(tag string, in reflect.Value) { + var style yaml_scalar_style_t + s := in.String() + canUsePlain := true + switch { + case !utf8.ValidString(s): + if tag == binaryTag { + failf("explicitly tagged !!binary data must be base64-encoded") + } + if tag != "" { + failf("cannot marshal invalid UTF-8 data as %s", shortTag(tag)) + } + // It can't be encoded directly as YAML so use a binary tag + // and encode it as base64. + tag = binaryTag + s = encodeBase64(s) + case tag == "": + // Check to see if it would resolve to a specific + // tag when encoded unquoted. If it doesn't, + // there's no need to quote it. + rtag, _ := resolve("", s) + canUsePlain = rtag == strTag && !(isBase60Float(s) || isOldBool(s)) + } + // Note: it's possible for user code to emit invalid YAML + // if they explicitly specify a tag and a string containing + // text that's incompatible with that tag. + switch { + case strings.Contains(s, "\n"): + if e.flow { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } else { + style = yaml_LITERAL_SCALAR_STYLE + } + case canUsePlain: + style = yaml_PLAIN_SCALAR_STYLE + default: + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + e.emitScalar(s, "", tag, style, nil, nil, nil, nil) +} + +func (e *encoder) boolv(tag string, in reflect.Value) { + var s string + if in.Bool() { + s = "true" + } else { + s = "false" + } + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) intv(tag string, in reflect.Value) { + s := strconv.FormatInt(in.Int(), 10) + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) uintv(tag string, in reflect.Value) { + s := strconv.FormatUint(in.Uint(), 10) + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) timev(tag string, in reflect.Value) { + t := in.Interface().(time.Time) + s := t.Format(time.RFC3339Nano) + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) floatv(tag string, in reflect.Value) { + // Issue #352: When formatting, use the precision of the underlying value + precision := 64 + if in.Kind() == reflect.Float32 { + precision = 32 + } + + s := strconv.FormatFloat(in.Float(), 'g', -1, precision) + switch s { + case "+Inf": + s = ".inf" + case "-Inf": + s = "-.inf" + case "NaN": + s = ".nan" + } + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) nilv() { + e.emitScalar("null", "", "", yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) emitScalar(value, anchor, tag string, style yaml_scalar_style_t, head, line, foot, tail []byte) { + // TODO Kill this function. Replace all initialize calls by their underlining Go literals. + implicit := tag == "" + if !implicit { + tag = longTag(tag) + } + e.must(yaml_scalar_event_initialize(&e.event, []byte(anchor), []byte(tag), []byte(value), implicit, implicit, style)) + e.event.head_comment = head + e.event.line_comment = line + e.event.foot_comment = foot + e.event.tail_comment = tail + e.emit() +} + +func (e *encoder) nodev(in reflect.Value) { + e.node(in.Interface().(*Node), "") +} + +func (e *encoder) node(node *Node, tail string) { + // Zero nodes behave as nil. + if node.Kind == 0 && node.IsZero() { + e.nilv() + return + } + + // If the tag was not explicitly requested, and dropping it won't change the + // implicit tag of the value, don't include it in the presentation. + var tag = node.Tag + var stag = shortTag(tag) + var forceQuoting bool + if tag != "" && node.Style&TaggedStyle == 0 { + if node.Kind == ScalarNode { + if stag == strTag && node.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0 { + tag = "" + } else { + rtag, _ := resolve("", node.Value) + if rtag == stag { + tag = "" + } else if stag == strTag { + tag = "" + forceQuoting = true + } + } + } else { + var rtag string + switch node.Kind { + case MappingNode: + rtag = mapTag + case SequenceNode: + rtag = seqTag + } + if rtag == stag { + tag = "" + } + } + } + + switch node.Kind { + case DocumentNode: + yaml_document_start_event_initialize(&e.event, nil, nil, true) + e.event.head_comment = []byte(node.HeadComment) + e.emit() + for _, node := range node.Content { + e.node(node, "") + } + yaml_document_end_event_initialize(&e.event, true) + e.event.foot_comment = []byte(node.FootComment) + e.emit() + + case SequenceNode: + style := yaml_BLOCK_SEQUENCE_STYLE + if node.Style&FlowStyle != 0 { + style = yaml_FLOW_SEQUENCE_STYLE + } + e.must(yaml_sequence_start_event_initialize(&e.event, []byte(node.Anchor), []byte(longTag(tag)), tag == "", style)) + e.event.head_comment = []byte(node.HeadComment) + e.emit() + for _, node := range node.Content { + e.node(node, "") + } + e.must(yaml_sequence_end_event_initialize(&e.event)) + e.event.line_comment = []byte(node.LineComment) + e.event.foot_comment = []byte(node.FootComment) + e.emit() + + case MappingNode: + style := yaml_BLOCK_MAPPING_STYLE + if node.Style&FlowStyle != 0 { + style = yaml_FLOW_MAPPING_STYLE + } + yaml_mapping_start_event_initialize(&e.event, []byte(node.Anchor), []byte(longTag(tag)), tag == "", style) + e.event.tail_comment = []byte(tail) + e.event.head_comment = []byte(node.HeadComment) + e.emit() + + // The tail logic below moves the foot comment of prior keys to the following key, + // since the value for each key may be a nested structure and the foot needs to be + // processed only the entirety of the value is streamed. The last tail is processed + // with the mapping end event. + var tail string + for i := 0; i+1 < len(node.Content); i += 2 { + k := node.Content[i] + foot := k.FootComment + if foot != "" { + kopy := *k + kopy.FootComment = "" + k = &kopy + } + e.node(k, tail) + tail = foot + + v := node.Content[i+1] + e.node(v, "") + } + + yaml_mapping_end_event_initialize(&e.event) + e.event.tail_comment = []byte(tail) + e.event.line_comment = []byte(node.LineComment) + e.event.foot_comment = []byte(node.FootComment) + e.emit() + + case AliasNode: + yaml_alias_event_initialize(&e.event, []byte(node.Value)) + e.event.head_comment = []byte(node.HeadComment) + e.event.line_comment = []byte(node.LineComment) + e.event.foot_comment = []byte(node.FootComment) + e.emit() + + case ScalarNode: + value := node.Value + if !utf8.ValidString(value) { + if stag == binaryTag { + failf("explicitly tagged !!binary data must be base64-encoded") + } + if stag != "" { + failf("cannot marshal invalid UTF-8 data as %s", stag) + } + // It can't be encoded directly as YAML so use a binary tag + // and encode it as base64. + tag = binaryTag + value = encodeBase64(value) + } + + style := yaml_PLAIN_SCALAR_STYLE + switch { + case node.Style&DoubleQuotedStyle != 0: + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + case node.Style&SingleQuotedStyle != 0: + style = yaml_SINGLE_QUOTED_SCALAR_STYLE + case node.Style&LiteralStyle != 0: + style = yaml_LITERAL_SCALAR_STYLE + case node.Style&FoldedStyle != 0: + style = yaml_FOLDED_SCALAR_STYLE + case strings.Contains(value, "\n"): + style = yaml_LITERAL_SCALAR_STYLE + case forceQuoting: + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + + e.emitScalar(value, node.Anchor, tag, style, []byte(node.HeadComment), []byte(node.LineComment), []byte(node.FootComment), []byte(tail)) + default: + failf("cannot encode node with unknown kind %d", node.Kind) + } +} diff --git a/vendor/go.yaml.in/yaml/v3/parserc.go b/vendor/go.yaml.in/yaml/v3/parserc.go new file mode 100644 index 000000000..f35829db4 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/parserc.go @@ -0,0 +1,1260 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// 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. + +package yaml + +import ( + "bytes" +) + +// The parser implements the following grammar: +// +// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END +// implicit_document ::= block_node DOCUMENT-END* +// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// block_node_or_indentless_sequence ::= +// ALIAS +// | properties (block_content | indentless_block_sequence)? +// | block_content +// | indentless_block_sequence +// block_node ::= ALIAS +// | properties block_content? +// | block_content +// flow_node ::= ALIAS +// | properties flow_content? +// | flow_content +// properties ::= TAG ANCHOR? | ANCHOR TAG? +// block_content ::= block_collection | flow_collection | SCALAR +// flow_content ::= flow_collection | SCALAR +// block_collection ::= block_sequence | block_mapping +// flow_collection ::= flow_sequence | flow_mapping +// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END +// indentless_sequence ::= (BLOCK-ENTRY block_node?)+ +// block_mapping ::= BLOCK-MAPPING_START +// ((KEY block_node_or_indentless_sequence?)? +// (VALUE block_node_or_indentless_sequence?)?)* +// BLOCK-END +// flow_sequence ::= FLOW-SEQUENCE-START +// (flow_sequence_entry FLOW-ENTRY)* +// flow_sequence_entry? +// FLOW-SEQUENCE-END +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// flow_mapping ::= FLOW-MAPPING-START +// (flow_mapping_entry FLOW-ENTRY)* +// flow_mapping_entry? +// FLOW-MAPPING-END +// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + +// Peek the next token in the token queue. +func peek_token(parser *yaml_parser_t) *yaml_token_t { + if parser.token_available || yaml_parser_fetch_more_tokens(parser) { + token := &parser.tokens[parser.tokens_head] + yaml_parser_unfold_comments(parser, token) + return token + } + return nil +} + +// yaml_parser_unfold_comments walks through the comments queue and joins all +// comments behind the position of the provided token into the respective +// top-level comment slices in the parser. +func yaml_parser_unfold_comments(parser *yaml_parser_t, token *yaml_token_t) { + for parser.comments_head < len(parser.comments) && token.start_mark.index >= parser.comments[parser.comments_head].token_mark.index { + comment := &parser.comments[parser.comments_head] + if len(comment.head) > 0 { + if token.typ == yaml_BLOCK_END_TOKEN { + // No heads on ends, so keep comment.head for a follow up token. + break + } + if len(parser.head_comment) > 0 { + parser.head_comment = append(parser.head_comment, '\n') + } + parser.head_comment = append(parser.head_comment, comment.head...) + } + if len(comment.foot) > 0 { + if len(parser.foot_comment) > 0 { + parser.foot_comment = append(parser.foot_comment, '\n') + } + parser.foot_comment = append(parser.foot_comment, comment.foot...) + } + if len(comment.line) > 0 { + if len(parser.line_comment) > 0 { + parser.line_comment = append(parser.line_comment, '\n') + } + parser.line_comment = append(parser.line_comment, comment.line...) + } + *comment = yaml_comment_t{} + parser.comments_head++ + } +} + +// Remove the next token from the queue (must be called after peek_token). +func skip_token(parser *yaml_parser_t) { + parser.token_available = false + parser.tokens_parsed++ + parser.stream_end_produced = parser.tokens[parser.tokens_head].typ == yaml_STREAM_END_TOKEN + parser.tokens_head++ +} + +// Get the next event. +func yaml_parser_parse(parser *yaml_parser_t, event *yaml_event_t) bool { + // Erase the event object. + *event = yaml_event_t{} + + // No events after the end of the stream or error. + if parser.stream_end_produced || parser.error != yaml_NO_ERROR || parser.state == yaml_PARSE_END_STATE { + return true + } + + // Generate the next event. + return yaml_parser_state_machine(parser, event) +} + +// Set parser error. +func yaml_parser_set_parser_error(parser *yaml_parser_t, problem string, problem_mark yaml_mark_t) bool { + parser.error = yaml_PARSER_ERROR + parser.problem = problem + parser.problem_mark = problem_mark + return false +} + +func yaml_parser_set_parser_error_context(parser *yaml_parser_t, context string, context_mark yaml_mark_t, problem string, problem_mark yaml_mark_t) bool { + parser.error = yaml_PARSER_ERROR + parser.context = context + parser.context_mark = context_mark + parser.problem = problem + parser.problem_mark = problem_mark + return false +} + +// State dispatcher. +func yaml_parser_state_machine(parser *yaml_parser_t, event *yaml_event_t) bool { + //trace("yaml_parser_state_machine", "state:", parser.state.String()) + + switch parser.state { + case yaml_PARSE_STREAM_START_STATE: + return yaml_parser_parse_stream_start(parser, event) + + case yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE: + return yaml_parser_parse_document_start(parser, event, true) + + case yaml_PARSE_DOCUMENT_START_STATE: + return yaml_parser_parse_document_start(parser, event, false) + + case yaml_PARSE_DOCUMENT_CONTENT_STATE: + return yaml_parser_parse_document_content(parser, event) + + case yaml_PARSE_DOCUMENT_END_STATE: + return yaml_parser_parse_document_end(parser, event) + + case yaml_PARSE_BLOCK_NODE_STATE: + return yaml_parser_parse_node(parser, event, true, false) + + case yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE: + return yaml_parser_parse_node(parser, event, true, true) + + case yaml_PARSE_FLOW_NODE_STATE: + return yaml_parser_parse_node(parser, event, false, false) + + case yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE: + return yaml_parser_parse_block_sequence_entry(parser, event, true) + + case yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE: + return yaml_parser_parse_block_sequence_entry(parser, event, false) + + case yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE: + return yaml_parser_parse_indentless_sequence_entry(parser, event) + + case yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE: + return yaml_parser_parse_block_mapping_key(parser, event, true) + + case yaml_PARSE_BLOCK_MAPPING_KEY_STATE: + return yaml_parser_parse_block_mapping_key(parser, event, false) + + case yaml_PARSE_BLOCK_MAPPING_VALUE_STATE: + return yaml_parser_parse_block_mapping_value(parser, event) + + case yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE: + return yaml_parser_parse_flow_sequence_entry(parser, event, true) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE: + return yaml_parser_parse_flow_sequence_entry(parser, event, false) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE: + return yaml_parser_parse_flow_sequence_entry_mapping_key(parser, event) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE: + return yaml_parser_parse_flow_sequence_entry_mapping_value(parser, event) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE: + return yaml_parser_parse_flow_sequence_entry_mapping_end(parser, event) + + case yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE: + return yaml_parser_parse_flow_mapping_key(parser, event, true) + + case yaml_PARSE_FLOW_MAPPING_KEY_STATE: + return yaml_parser_parse_flow_mapping_key(parser, event, false) + + case yaml_PARSE_FLOW_MAPPING_VALUE_STATE: + return yaml_parser_parse_flow_mapping_value(parser, event, false) + + case yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE: + return yaml_parser_parse_flow_mapping_value(parser, event, true) + + default: + panic("invalid parser state") + } +} + +// Parse the production: +// +// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END +// ************ +func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_STREAM_START_TOKEN { + return yaml_parser_set_parser_error(parser, "did not find expected ", token.start_mark) + } + parser.state = yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE + *event = yaml_event_t{ + typ: yaml_STREAM_START_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + encoding: token.encoding, + } + skip_token(parser) + return true +} + +// Parse the productions: +// +// implicit_document ::= block_node DOCUMENT-END* +// * +// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// ************************* +func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t, implicit bool) bool { + + token := peek_token(parser) + if token == nil { + return false + } + + // Parse extra document end indicators. + if !implicit { + for token.typ == yaml_DOCUMENT_END_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } + } + + if implicit && token.typ != yaml_VERSION_DIRECTIVE_TOKEN && + token.typ != yaml_TAG_DIRECTIVE_TOKEN && + token.typ != yaml_DOCUMENT_START_TOKEN && + token.typ != yaml_STREAM_END_TOKEN { + // Parse an implicit document. + if !yaml_parser_process_directives(parser, nil, nil) { + return false + } + parser.states = append(parser.states, yaml_PARSE_DOCUMENT_END_STATE) + parser.state = yaml_PARSE_BLOCK_NODE_STATE + + var head_comment []byte + if len(parser.head_comment) > 0 { + // [Go] Scan the header comment backwards, and if an empty line is found, break + // the header so the part before the last empty line goes into the + // document header, while the bottom of it goes into a follow up event. + for i := len(parser.head_comment) - 1; i > 0; i-- { + if parser.head_comment[i] == '\n' { + if i == len(parser.head_comment)-1 { + head_comment = parser.head_comment[:i] + parser.head_comment = parser.head_comment[i+1:] + break + } else if parser.head_comment[i-1] == '\n' { + head_comment = parser.head_comment[:i-1] + parser.head_comment = parser.head_comment[i+1:] + break + } + } + } + } + + *event = yaml_event_t{ + typ: yaml_DOCUMENT_START_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + + head_comment: head_comment, + } + + } else if token.typ != yaml_STREAM_END_TOKEN { + // Parse an explicit document. + var version_directive *yaml_version_directive_t + var tag_directives []yaml_tag_directive_t + start_mark := token.start_mark + if !yaml_parser_process_directives(parser, &version_directive, &tag_directives) { + return false + } + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_DOCUMENT_START_TOKEN { + yaml_parser_set_parser_error(parser, + "did not find expected ", token.start_mark) + return false + } + parser.states = append(parser.states, yaml_PARSE_DOCUMENT_END_STATE) + parser.state = yaml_PARSE_DOCUMENT_CONTENT_STATE + end_mark := token.end_mark + + *event = yaml_event_t{ + typ: yaml_DOCUMENT_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + version_directive: version_directive, + tag_directives: tag_directives, + implicit: false, + } + skip_token(parser) + + } else { + // Parse the stream end. + parser.state = yaml_PARSE_END_STATE + *event = yaml_event_t{ + typ: yaml_STREAM_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + skip_token(parser) + } + + return true +} + +// Parse the productions: +// +// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// *********** +func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + if token.typ == yaml_VERSION_DIRECTIVE_TOKEN || + token.typ == yaml_TAG_DIRECTIVE_TOKEN || + token.typ == yaml_DOCUMENT_START_TOKEN || + token.typ == yaml_DOCUMENT_END_TOKEN || + token.typ == yaml_STREAM_END_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + return yaml_parser_process_empty_scalar(parser, event, + token.start_mark) + } + return yaml_parser_parse_node(parser, event, true, false) +} + +// Parse the productions: +// +// implicit_document ::= block_node DOCUMENT-END* +// ************* +// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +func yaml_parser_parse_document_end(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + start_mark := token.start_mark + end_mark := token.start_mark + + implicit := true + if token.typ == yaml_DOCUMENT_END_TOKEN { + end_mark = token.end_mark + skip_token(parser) + implicit = false + } + + parser.tag_directives = parser.tag_directives[:0] + + parser.state = yaml_PARSE_DOCUMENT_START_STATE + *event = yaml_event_t{ + typ: yaml_DOCUMENT_END_EVENT, + start_mark: start_mark, + end_mark: end_mark, + implicit: implicit, + } + yaml_parser_set_event_comments(parser, event) + if len(event.head_comment) > 0 && len(event.foot_comment) == 0 { + event.foot_comment = event.head_comment + event.head_comment = nil + } + return true +} + +func yaml_parser_set_event_comments(parser *yaml_parser_t, event *yaml_event_t) { + event.head_comment = parser.head_comment + event.line_comment = parser.line_comment + event.foot_comment = parser.foot_comment + parser.head_comment = nil + parser.line_comment = nil + parser.foot_comment = nil + parser.tail_comment = nil + parser.stem_comment = nil +} + +// Parse the productions: +// +// block_node_or_indentless_sequence ::= +// ALIAS +// ***** +// | properties (block_content | indentless_block_sequence)? +// ********** * +// | block_content | indentless_block_sequence +// * +// block_node ::= ALIAS +// ***** +// | properties block_content? +// ********** * +// | block_content +// * +// flow_node ::= ALIAS +// ***** +// | properties flow_content? +// ********** * +// | flow_content +// * +// properties ::= TAG ANCHOR? | ANCHOR TAG? +// ************************* +// block_content ::= block_collection | flow_collection | SCALAR +// ****** +// flow_content ::= flow_collection | SCALAR +// ****** +func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, indentless_sequence bool) bool { + //defer trace("yaml_parser_parse_node", "block:", block, "indentless_sequence:", indentless_sequence)() + + token := peek_token(parser) + if token == nil { + return false + } + + if token.typ == yaml_ALIAS_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + *event = yaml_event_t{ + typ: yaml_ALIAS_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + anchor: token.value, + } + yaml_parser_set_event_comments(parser, event) + skip_token(parser) + return true + } + + start_mark := token.start_mark + end_mark := token.start_mark + + var tag_token bool + var tag_handle, tag_suffix, anchor []byte + var tag_mark yaml_mark_t + if token.typ == yaml_ANCHOR_TOKEN { + anchor = token.value + start_mark = token.start_mark + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ == yaml_TAG_TOKEN { + tag_token = true + tag_handle = token.value + tag_suffix = token.suffix + tag_mark = token.start_mark + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } + } else if token.typ == yaml_TAG_TOKEN { + tag_token = true + tag_handle = token.value + tag_suffix = token.suffix + start_mark = token.start_mark + tag_mark = token.start_mark + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ == yaml_ANCHOR_TOKEN { + anchor = token.value + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } + } + + var tag []byte + if tag_token { + if len(tag_handle) == 0 { + tag = tag_suffix + tag_suffix = nil + } else { + for i := range parser.tag_directives { + if bytes.Equal(parser.tag_directives[i].handle, tag_handle) { + tag = append([]byte(nil), parser.tag_directives[i].prefix...) + tag = append(tag, tag_suffix...) + break + } + } + if len(tag) == 0 { + yaml_parser_set_parser_error_context(parser, + "while parsing a node", start_mark, + "found undefined tag handle", tag_mark) + return false + } + } + } + + implicit := len(tag) == 0 + if indentless_sequence && token.typ == yaml_BLOCK_ENTRY_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE + *event = yaml_event_t{ + typ: yaml_SEQUENCE_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_BLOCK_SEQUENCE_STYLE), + } + return true + } + if token.typ == yaml_SCALAR_TOKEN { + var plain_implicit, quoted_implicit bool + end_mark = token.end_mark + if (len(tag) == 0 && token.style == yaml_PLAIN_SCALAR_STYLE) || (len(tag) == 1 && tag[0] == '!') { + plain_implicit = true + } else if len(tag) == 0 { + quoted_implicit = true + } + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + + *event = yaml_event_t{ + typ: yaml_SCALAR_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + value: token.value, + implicit: plain_implicit, + quoted_implicit: quoted_implicit, + style: yaml_style_t(token.style), + } + yaml_parser_set_event_comments(parser, event) + skip_token(parser) + return true + } + if token.typ == yaml_FLOW_SEQUENCE_START_TOKEN { + // [Go] Some of the events below can be merged as they differ only on style. + end_mark = token.end_mark + parser.state = yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE + *event = yaml_event_t{ + typ: yaml_SEQUENCE_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_FLOW_SEQUENCE_STYLE), + } + yaml_parser_set_event_comments(parser, event) + return true + } + if token.typ == yaml_FLOW_MAPPING_START_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE + *event = yaml_event_t{ + typ: yaml_MAPPING_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_FLOW_MAPPING_STYLE), + } + yaml_parser_set_event_comments(parser, event) + return true + } + if block && token.typ == yaml_BLOCK_SEQUENCE_START_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE + *event = yaml_event_t{ + typ: yaml_SEQUENCE_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_BLOCK_SEQUENCE_STYLE), + } + if parser.stem_comment != nil { + event.head_comment = parser.stem_comment + parser.stem_comment = nil + } + return true + } + if block && token.typ == yaml_BLOCK_MAPPING_START_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE + *event = yaml_event_t{ + typ: yaml_MAPPING_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_BLOCK_MAPPING_STYLE), + } + if parser.stem_comment != nil { + event.head_comment = parser.stem_comment + parser.stem_comment = nil + } + return true + } + if len(anchor) > 0 || len(tag) > 0 { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + + *event = yaml_event_t{ + typ: yaml_SCALAR_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + quoted_implicit: false, + style: yaml_style_t(yaml_PLAIN_SCALAR_STYLE), + } + return true + } + + context := "while parsing a flow node" + if block { + context = "while parsing a block node" + } + yaml_parser_set_parser_error_context(parser, context, start_mark, + "did not find expected node content", token.start_mark) + return false +} + +// Parse the productions: +// +// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END +// ******************** *********** * ********* +func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + if token == nil { + return false + } + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + + token := peek_token(parser) + if token == nil { + return false + } + + if token.typ == yaml_BLOCK_ENTRY_TOKEN { + mark := token.end_mark + prior_head_len := len(parser.head_comment) + skip_token(parser) + yaml_parser_split_stem_comment(parser, prior_head_len) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_BLOCK_ENTRY_TOKEN && token.typ != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE) + return yaml_parser_parse_node(parser, event, true, false) + } else { + parser.state = yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + } + if token.typ == yaml_BLOCK_END_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + + *event = yaml_event_t{ + typ: yaml_SEQUENCE_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + + skip_token(parser) + return true + } + + context_mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + return yaml_parser_set_parser_error_context(parser, + "while parsing a block collection", context_mark, + "did not find expected '-' indicator", token.start_mark) +} + +// Parse the productions: +// +// indentless_sequence ::= (BLOCK-ENTRY block_node?)+ +// *********** * +func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + if token.typ == yaml_BLOCK_ENTRY_TOKEN { + mark := token.end_mark + prior_head_len := len(parser.head_comment) + skip_token(parser) + yaml_parser_split_stem_comment(parser, prior_head_len) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_BLOCK_ENTRY_TOKEN && + token.typ != yaml_KEY_TOKEN && + token.typ != yaml_VALUE_TOKEN && + token.typ != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE) + return yaml_parser_parse_node(parser, event, true, false) + } + parser.state = yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + + *event = yaml_event_t{ + typ: yaml_SEQUENCE_END_EVENT, + start_mark: token.start_mark, + end_mark: token.start_mark, // [Go] Shouldn't this be token.end_mark? + } + return true +} + +// Split stem comment from head comment. +// +// When a sequence or map is found under a sequence entry, the former head comment +// is assigned to the underlying sequence or map as a whole, not the individual +// sequence or map entry as would be expected otherwise. To handle this case the +// previous head comment is moved aside as the stem comment. +func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) { + if stem_len == 0 { + return + } + + token := peek_token(parser) + if token == nil || token.typ != yaml_BLOCK_SEQUENCE_START_TOKEN && token.typ != yaml_BLOCK_MAPPING_START_TOKEN { + return + } + + parser.stem_comment = parser.head_comment[:stem_len] + if len(parser.head_comment) == stem_len { + parser.head_comment = nil + } else { + // Copy suffix to prevent very strange bugs if someone ever appends + // further bytes to the prefix in the stem_comment slice above. + parser.head_comment = append([]byte(nil), parser.head_comment[stem_len+1:]...) + } +} + +// Parse the productions: +// +// block_mapping ::= BLOCK-MAPPING_START +// ******************* +// ((KEY block_node_or_indentless_sequence?)? +// *** * +// (VALUE block_node_or_indentless_sequence?)?)* +// +// BLOCK-END +// ********* +func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + if token == nil { + return false + } + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + + token := peek_token(parser) + if token == nil { + return false + } + + // [Go] A tail comment was left from the prior mapping value processed. Emit an event + // as it needs to be processed with that value and not the following key. + if len(parser.tail_comment) > 0 { + *event = yaml_event_t{ + typ: yaml_TAIL_COMMENT_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + foot_comment: parser.tail_comment, + } + parser.tail_comment = nil + return true + } + + if token.typ == yaml_KEY_TOKEN { + mark := token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_KEY_TOKEN && + token.typ != yaml_VALUE_TOKEN && + token.typ != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_BLOCK_MAPPING_VALUE_STATE) + return yaml_parser_parse_node(parser, event, true, true) + } else { + parser.state = yaml_PARSE_BLOCK_MAPPING_VALUE_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + } else if token.typ == yaml_BLOCK_END_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + *event = yaml_event_t{ + typ: yaml_MAPPING_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + yaml_parser_set_event_comments(parser, event) + skip_token(parser) + return true + } + + context_mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + return yaml_parser_set_parser_error_context(parser, + "while parsing a block mapping", context_mark, + "did not find expected key", token.start_mark) +} + +// Parse the productions: +// +// block_mapping ::= BLOCK-MAPPING_START +// +// ((KEY block_node_or_indentless_sequence?)? +// +// (VALUE block_node_or_indentless_sequence?)?)* +// ***** * +// BLOCK-END +func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + if token.typ == yaml_VALUE_TOKEN { + mark := token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_KEY_TOKEN && + token.typ != yaml_VALUE_TOKEN && + token.typ != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_BLOCK_MAPPING_KEY_STATE) + return yaml_parser_parse_node(parser, event, true, true) + } + parser.state = yaml_PARSE_BLOCK_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + parser.state = yaml_PARSE_BLOCK_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) +} + +// Parse the productions: +// +// flow_sequence ::= FLOW-SEQUENCE-START +// ******************* +// (flow_sequence_entry FLOW-ENTRY)* +// * ********** +// flow_sequence_entry? +// * +// FLOW-SEQUENCE-END +// ***************** +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * +func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + if token == nil { + return false + } + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + token := peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_FLOW_SEQUENCE_END_TOKEN { + if !first { + if token.typ == yaml_FLOW_ENTRY_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } else { + context_mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + return yaml_parser_set_parser_error_context(parser, + "while parsing a flow sequence", context_mark, + "did not find expected ',' or ']'", token.start_mark) + } + } + + if token.typ == yaml_KEY_TOKEN { + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE + *event = yaml_event_t{ + typ: yaml_MAPPING_START_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + implicit: true, + style: yaml_style_t(yaml_FLOW_MAPPING_STYLE), + } + skip_token(parser) + return true + } else if token.typ != yaml_FLOW_SEQUENCE_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + + *event = yaml_event_t{ + typ: yaml_SEQUENCE_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + yaml_parser_set_event_comments(parser, event) + + skip_token(parser) + return true +} + +// Parse the productions: +// +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// *** * +func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_VALUE_TOKEN && + token.typ != yaml_FLOW_ENTRY_TOKEN && + token.typ != yaml_FLOW_SEQUENCE_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + mark := token.end_mark + skip_token(parser) + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) +} + +// Parse the productions: +// +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// ***** * +func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + if token.typ == yaml_VALUE_TOKEN { + skip_token(parser) + token := peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_FLOW_ENTRY_TOKEN && token.typ != yaml_FLOW_SEQUENCE_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) +} + +// Parse the productions: +// +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * +func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE + *event = yaml_event_t{ + typ: yaml_MAPPING_END_EVENT, + start_mark: token.start_mark, + end_mark: token.start_mark, // [Go] Shouldn't this be end_mark? + } + return true +} + +// Parse the productions: +// +// flow_mapping ::= FLOW-MAPPING-START +// ****************** +// (flow_mapping_entry FLOW-ENTRY)* +// * ********** +// flow_mapping_entry? +// ****************** +// FLOW-MAPPING-END +// **************** +// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * *** * +func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + + token := peek_token(parser) + if token == nil { + return false + } + + if token.typ != yaml_FLOW_MAPPING_END_TOKEN { + if !first { + if token.typ == yaml_FLOW_ENTRY_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } else { + context_mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + return yaml_parser_set_parser_error_context(parser, + "while parsing a flow mapping", context_mark, + "did not find expected ',' or '}'", token.start_mark) + } + } + + if token.typ == yaml_KEY_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_VALUE_TOKEN && + token.typ != yaml_FLOW_ENTRY_TOKEN && + token.typ != yaml_FLOW_MAPPING_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_VALUE_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } else { + parser.state = yaml_PARSE_FLOW_MAPPING_VALUE_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) + } + } else if token.typ != yaml_FLOW_MAPPING_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + *event = yaml_event_t{ + typ: yaml_MAPPING_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + yaml_parser_set_event_comments(parser, event) + skip_token(parser) + return true +} + +// Parse the productions: +// +// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * ***** * +func yaml_parser_parse_flow_mapping_value(parser *yaml_parser_t, event *yaml_event_t, empty bool) bool { + token := peek_token(parser) + if token == nil { + return false + } + if empty { + parser.state = yaml_PARSE_FLOW_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) + } + if token.typ == yaml_VALUE_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_FLOW_ENTRY_TOKEN && token.typ != yaml_FLOW_MAPPING_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_KEY_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + parser.state = yaml_PARSE_FLOW_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) +} + +// Generate an empty scalar event. +func yaml_parser_process_empty_scalar(parser *yaml_parser_t, event *yaml_event_t, mark yaml_mark_t) bool { + *event = yaml_event_t{ + typ: yaml_SCALAR_EVENT, + start_mark: mark, + end_mark: mark, + value: nil, // Empty + implicit: true, + style: yaml_style_t(yaml_PLAIN_SCALAR_STYLE), + } + return true +} + +var default_tag_directives = []yaml_tag_directive_t{ + {[]byte("!"), []byte("!")}, + {[]byte("!!"), []byte("tag:yaml.org,2002:")}, +} + +// Parse directives. +func yaml_parser_process_directives(parser *yaml_parser_t, + version_directive_ref **yaml_version_directive_t, + tag_directives_ref *[]yaml_tag_directive_t) bool { + + var version_directive *yaml_version_directive_t + var tag_directives []yaml_tag_directive_t + + token := peek_token(parser) + if token == nil { + return false + } + + for token.typ == yaml_VERSION_DIRECTIVE_TOKEN || token.typ == yaml_TAG_DIRECTIVE_TOKEN { + if token.typ == yaml_VERSION_DIRECTIVE_TOKEN { + if version_directive != nil { + yaml_parser_set_parser_error(parser, + "found duplicate %YAML directive", token.start_mark) + return false + } + if token.major != 1 || token.minor != 1 { + yaml_parser_set_parser_error(parser, + "found incompatible YAML document", token.start_mark) + return false + } + version_directive = &yaml_version_directive_t{ + major: token.major, + minor: token.minor, + } + } else if token.typ == yaml_TAG_DIRECTIVE_TOKEN { + value := yaml_tag_directive_t{ + handle: token.value, + prefix: token.prefix, + } + if !yaml_parser_append_tag_directive(parser, value, false, token.start_mark) { + return false + } + tag_directives = append(tag_directives, value) + } + + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } + + for i := range default_tag_directives { + if !yaml_parser_append_tag_directive(parser, default_tag_directives[i], true, token.start_mark) { + return false + } + } + + if version_directive_ref != nil { + *version_directive_ref = version_directive + } + if tag_directives_ref != nil { + *tag_directives_ref = tag_directives + } + return true +} + +// Append a tag directive to the directives stack. +func yaml_parser_append_tag_directive(parser *yaml_parser_t, value yaml_tag_directive_t, allow_duplicates bool, mark yaml_mark_t) bool { + for i := range parser.tag_directives { + if bytes.Equal(value.handle, parser.tag_directives[i].handle) { + if allow_duplicates { + return true + } + return yaml_parser_set_parser_error(parser, "found duplicate %TAG directive", mark) + } + } + + // [Go] I suspect the copy is unnecessary. This was likely done + // because there was no way to track ownership of the data. + value_copy := yaml_tag_directive_t{ + handle: make([]byte, len(value.handle)), + prefix: make([]byte, len(value.prefix)), + } + copy(value_copy.handle, value.handle) + copy(value_copy.prefix, value.prefix) + parser.tag_directives = append(parser.tag_directives, value_copy) + return true +} diff --git a/vendor/go.yaml.in/yaml/v3/readerc.go b/vendor/go.yaml.in/yaml/v3/readerc.go new file mode 100644 index 000000000..56af24536 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/readerc.go @@ -0,0 +1,434 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// 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. + +package yaml + +import ( + "io" +) + +// Set the reader error and return 0. +func yaml_parser_set_reader_error(parser *yaml_parser_t, problem string, offset int, value int) bool { + parser.error = yaml_READER_ERROR + parser.problem = problem + parser.problem_offset = offset + parser.problem_value = value + return false +} + +// Byte order marks. +const ( + bom_UTF8 = "\xef\xbb\xbf" + bom_UTF16LE = "\xff\xfe" + bom_UTF16BE = "\xfe\xff" +) + +// Determine the input stream encoding by checking the BOM symbol. If no BOM is +// found, the UTF-8 encoding is assumed. Return 1 on success, 0 on failure. +func yaml_parser_determine_encoding(parser *yaml_parser_t) bool { + // Ensure that we had enough bytes in the raw buffer. + for !parser.eof && len(parser.raw_buffer)-parser.raw_buffer_pos < 3 { + if !yaml_parser_update_raw_buffer(parser) { + return false + } + } + + // Determine the encoding. + buf := parser.raw_buffer + pos := parser.raw_buffer_pos + avail := len(buf) - pos + if avail >= 2 && buf[pos] == bom_UTF16LE[0] && buf[pos+1] == bom_UTF16LE[1] { + parser.encoding = yaml_UTF16LE_ENCODING + parser.raw_buffer_pos += 2 + parser.offset += 2 + } else if avail >= 2 && buf[pos] == bom_UTF16BE[0] && buf[pos+1] == bom_UTF16BE[1] { + parser.encoding = yaml_UTF16BE_ENCODING + parser.raw_buffer_pos += 2 + parser.offset += 2 + } else if avail >= 3 && buf[pos] == bom_UTF8[0] && buf[pos+1] == bom_UTF8[1] && buf[pos+2] == bom_UTF8[2] { + parser.encoding = yaml_UTF8_ENCODING + parser.raw_buffer_pos += 3 + parser.offset += 3 + } else { + parser.encoding = yaml_UTF8_ENCODING + } + return true +} + +// Update the raw buffer. +func yaml_parser_update_raw_buffer(parser *yaml_parser_t) bool { + size_read := 0 + + // Return if the raw buffer is full. + if parser.raw_buffer_pos == 0 && len(parser.raw_buffer) == cap(parser.raw_buffer) { + return true + } + + // Return on EOF. + if parser.eof { + return true + } + + // Move the remaining bytes in the raw buffer to the beginning. + if parser.raw_buffer_pos > 0 && parser.raw_buffer_pos < len(parser.raw_buffer) { + copy(parser.raw_buffer, parser.raw_buffer[parser.raw_buffer_pos:]) + } + parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)-parser.raw_buffer_pos] + parser.raw_buffer_pos = 0 + + // Call the read handler to fill the buffer. + size_read, err := parser.read_handler(parser, parser.raw_buffer[len(parser.raw_buffer):cap(parser.raw_buffer)]) + parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)+size_read] + if err == io.EOF { + parser.eof = true + } else if err != nil { + return yaml_parser_set_reader_error(parser, "input error: "+err.Error(), parser.offset, -1) + } + return true +} + +// Ensure that the buffer contains at least `length` characters. +// Return true on success, false on failure. +// +// The length is supposed to be significantly less that the buffer size. +func yaml_parser_update_buffer(parser *yaml_parser_t, length int) bool { + if parser.read_handler == nil { + panic("read handler must be set") + } + + // [Go] This function was changed to guarantee the requested length size at EOF. + // The fact we need to do this is pretty awful, but the description above implies + // for that to be the case, and there are tests + + // If the EOF flag is set and the raw buffer is empty, do nothing. + if parser.eof && parser.raw_buffer_pos == len(parser.raw_buffer) { + // [Go] ACTUALLY! Read the documentation of this function above. + // This is just broken. To return true, we need to have the + // given length in the buffer. Not doing that means every single + // check that calls this function to make sure the buffer has a + // given length is Go) panicking; or C) accessing invalid memory. + //return true + } + + // Return if the buffer contains enough characters. + if parser.unread >= length { + return true + } + + // Determine the input encoding if it is not known yet. + if parser.encoding == yaml_ANY_ENCODING { + if !yaml_parser_determine_encoding(parser) { + return false + } + } + + // Move the unread characters to the beginning of the buffer. + buffer_len := len(parser.buffer) + if parser.buffer_pos > 0 && parser.buffer_pos < buffer_len { + copy(parser.buffer, parser.buffer[parser.buffer_pos:]) + buffer_len -= parser.buffer_pos + parser.buffer_pos = 0 + } else if parser.buffer_pos == buffer_len { + buffer_len = 0 + parser.buffer_pos = 0 + } + + // Open the whole buffer for writing, and cut it before returning. + parser.buffer = parser.buffer[:cap(parser.buffer)] + + // Fill the buffer until it has enough characters. + first := true + for parser.unread < length { + + // Fill the raw buffer if necessary. + if !first || parser.raw_buffer_pos == len(parser.raw_buffer) { + if !yaml_parser_update_raw_buffer(parser) { + parser.buffer = parser.buffer[:buffer_len] + return false + } + } + first = false + + // Decode the raw buffer. + inner: + for parser.raw_buffer_pos != len(parser.raw_buffer) { + var value rune + var width int + + raw_unread := len(parser.raw_buffer) - parser.raw_buffer_pos + + // Decode the next character. + switch parser.encoding { + case yaml_UTF8_ENCODING: + // Decode a UTF-8 character. Check RFC 3629 + // (http://www.ietf.org/rfc/rfc3629.txt) for more details. + // + // The following table (taken from the RFC) is used for + // decoding. + // + // Char. number range | UTF-8 octet sequence + // (hexadecimal) | (binary) + // --------------------+------------------------------------ + // 0000 0000-0000 007F | 0xxxxxxx + // 0000 0080-0000 07FF | 110xxxxx 10xxxxxx + // 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx + // 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + // + // Additionally, the characters in the range 0xD800-0xDFFF + // are prohibited as they are reserved for use with UTF-16 + // surrogate pairs. + + // Determine the length of the UTF-8 sequence. + octet := parser.raw_buffer[parser.raw_buffer_pos] + switch { + case octet&0x80 == 0x00: + width = 1 + case octet&0xE0 == 0xC0: + width = 2 + case octet&0xF0 == 0xE0: + width = 3 + case octet&0xF8 == 0xF0: + width = 4 + default: + // The leading octet is invalid. + return yaml_parser_set_reader_error(parser, + "invalid leading UTF-8 octet", + parser.offset, int(octet)) + } + + // Check if the raw buffer contains an incomplete character. + if width > raw_unread { + if parser.eof { + return yaml_parser_set_reader_error(parser, + "incomplete UTF-8 octet sequence", + parser.offset, -1) + } + break inner + } + + // Decode the leading octet. + switch { + case octet&0x80 == 0x00: + value = rune(octet & 0x7F) + case octet&0xE0 == 0xC0: + value = rune(octet & 0x1F) + case octet&0xF0 == 0xE0: + value = rune(octet & 0x0F) + case octet&0xF8 == 0xF0: + value = rune(octet & 0x07) + default: + value = 0 + } + + // Check and decode the trailing octets. + for k := 1; k < width; k++ { + octet = parser.raw_buffer[parser.raw_buffer_pos+k] + + // Check if the octet is valid. + if (octet & 0xC0) != 0x80 { + return yaml_parser_set_reader_error(parser, + "invalid trailing UTF-8 octet", + parser.offset+k, int(octet)) + } + + // Decode the octet. + value = (value << 6) + rune(octet&0x3F) + } + + // Check the length of the sequence against the value. + switch { + case width == 1: + case width == 2 && value >= 0x80: + case width == 3 && value >= 0x800: + case width == 4 && value >= 0x10000: + default: + return yaml_parser_set_reader_error(parser, + "invalid length of a UTF-8 sequence", + parser.offset, -1) + } + + // Check the range of the value. + if value >= 0xD800 && value <= 0xDFFF || value > 0x10FFFF { + return yaml_parser_set_reader_error(parser, + "invalid Unicode character", + parser.offset, int(value)) + } + + case yaml_UTF16LE_ENCODING, yaml_UTF16BE_ENCODING: + var low, high int + if parser.encoding == yaml_UTF16LE_ENCODING { + low, high = 0, 1 + } else { + low, high = 1, 0 + } + + // The UTF-16 encoding is not as simple as one might + // naively think. Check RFC 2781 + // (http://www.ietf.org/rfc/rfc2781.txt). + // + // Normally, two subsequent bytes describe a Unicode + // character. However a special technique (called a + // surrogate pair) is used for specifying character + // values larger than 0xFFFF. + // + // A surrogate pair consists of two pseudo-characters: + // high surrogate area (0xD800-0xDBFF) + // low surrogate area (0xDC00-0xDFFF) + // + // The following formulas are used for decoding + // and encoding characters using surrogate pairs: + // + // U = U' + 0x10000 (0x01 00 00 <= U <= 0x10 FF FF) + // U' = yyyyyyyyyyxxxxxxxxxx (0 <= U' <= 0x0F FF FF) + // W1 = 110110yyyyyyyyyy + // W2 = 110111xxxxxxxxxx + // + // where U is the character value, W1 is the high surrogate + // area, W2 is the low surrogate area. + + // Check for incomplete UTF-16 character. + if raw_unread < 2 { + if parser.eof { + return yaml_parser_set_reader_error(parser, + "incomplete UTF-16 character", + parser.offset, -1) + } + break inner + } + + // Get the character. + value = rune(parser.raw_buffer[parser.raw_buffer_pos+low]) + + (rune(parser.raw_buffer[parser.raw_buffer_pos+high]) << 8) + + // Check for unexpected low surrogate area. + if value&0xFC00 == 0xDC00 { + return yaml_parser_set_reader_error(parser, + "unexpected low surrogate area", + parser.offset, int(value)) + } + + // Check for a high surrogate area. + if value&0xFC00 == 0xD800 { + width = 4 + + // Check for incomplete surrogate pair. + if raw_unread < 4 { + if parser.eof { + return yaml_parser_set_reader_error(parser, + "incomplete UTF-16 surrogate pair", + parser.offset, -1) + } + break inner + } + + // Get the next character. + value2 := rune(parser.raw_buffer[parser.raw_buffer_pos+low+2]) + + (rune(parser.raw_buffer[parser.raw_buffer_pos+high+2]) << 8) + + // Check for a low surrogate area. + if value2&0xFC00 != 0xDC00 { + return yaml_parser_set_reader_error(parser, + "expected low surrogate area", + parser.offset+2, int(value2)) + } + + // Generate the value of the surrogate pair. + value = 0x10000 + ((value & 0x3FF) << 10) + (value2 & 0x3FF) + } else { + width = 2 + } + + default: + panic("impossible") + } + + // Check if the character is in the allowed range: + // #x9 | #xA | #xD | [#x20-#x7E] (8 bit) + // | #x85 | [#xA0-#xD7FF] | [#xE000-#xFFFD] (16 bit) + // | [#x10000-#x10FFFF] (32 bit) + switch { + case value == 0x09: + case value == 0x0A: + case value == 0x0D: + case value >= 0x20 && value <= 0x7E: + case value == 0x85: + case value >= 0xA0 && value <= 0xD7FF: + case value >= 0xE000 && value <= 0xFFFD: + case value >= 0x10000 && value <= 0x10FFFF: + default: + return yaml_parser_set_reader_error(parser, + "control characters are not allowed", + parser.offset, int(value)) + } + + // Move the raw pointers. + parser.raw_buffer_pos += width + parser.offset += width + + // Finally put the character into the buffer. + if value <= 0x7F { + // 0000 0000-0000 007F . 0xxxxxxx + parser.buffer[buffer_len+0] = byte(value) + buffer_len += 1 + } else if value <= 0x7FF { + // 0000 0080-0000 07FF . 110xxxxx 10xxxxxx + parser.buffer[buffer_len+0] = byte(0xC0 + (value >> 6)) + parser.buffer[buffer_len+1] = byte(0x80 + (value & 0x3F)) + buffer_len += 2 + } else if value <= 0xFFFF { + // 0000 0800-0000 FFFF . 1110xxxx 10xxxxxx 10xxxxxx + parser.buffer[buffer_len+0] = byte(0xE0 + (value >> 12)) + parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 6) & 0x3F)) + parser.buffer[buffer_len+2] = byte(0x80 + (value & 0x3F)) + buffer_len += 3 + } else { + // 0001 0000-0010 FFFF . 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + parser.buffer[buffer_len+0] = byte(0xF0 + (value >> 18)) + parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 12) & 0x3F)) + parser.buffer[buffer_len+2] = byte(0x80 + ((value >> 6) & 0x3F)) + parser.buffer[buffer_len+3] = byte(0x80 + (value & 0x3F)) + buffer_len += 4 + } + + parser.unread++ + } + + // On EOF, put NUL into the buffer and return. + if parser.eof { + parser.buffer[buffer_len] = 0 + buffer_len++ + parser.unread++ + break + } + } + // [Go] Read the documentation of this function above. To return true, + // we need to have the given length in the buffer. Not doing that means + // every single check that calls this function to make sure the buffer + // has a given length is Go) panicking; or C) accessing invalid memory. + // This happens here due to the EOF above breaking early. + for buffer_len < length { + parser.buffer[buffer_len] = 0 + buffer_len++ + } + parser.buffer = parser.buffer[:buffer_len] + return true +} diff --git a/vendor/go.yaml.in/yaml/v3/resolve.go b/vendor/go.yaml.in/yaml/v3/resolve.go new file mode 100644 index 000000000..64ae88805 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/resolve.go @@ -0,0 +1,326 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package yaml + +import ( + "encoding/base64" + "math" + "regexp" + "strconv" + "strings" + "time" +) + +type resolveMapItem struct { + value interface{} + tag string +} + +var resolveTable = make([]byte, 256) +var resolveMap = make(map[string]resolveMapItem) + +func init() { + t := resolveTable + t[int('+')] = 'S' // Sign + t[int('-')] = 'S' + for _, c := range "0123456789" { + t[int(c)] = 'D' // Digit + } + for _, c := range "yYnNtTfFoO~" { + t[int(c)] = 'M' // In map + } + t[int('.')] = '.' // Float (potentially in map) + + var resolveMapList = []struct { + v interface{} + tag string + l []string + }{ + {true, boolTag, []string{"true", "True", "TRUE"}}, + {false, boolTag, []string{"false", "False", "FALSE"}}, + {nil, nullTag, []string{"", "~", "null", "Null", "NULL"}}, + {math.NaN(), floatTag, []string{".nan", ".NaN", ".NAN"}}, + {math.Inf(+1), floatTag, []string{".inf", ".Inf", ".INF"}}, + {math.Inf(+1), floatTag, []string{"+.inf", "+.Inf", "+.INF"}}, + {math.Inf(-1), floatTag, []string{"-.inf", "-.Inf", "-.INF"}}, + {"<<", mergeTag, []string{"<<"}}, + } + + m := resolveMap + for _, item := range resolveMapList { + for _, s := range item.l { + m[s] = resolveMapItem{item.v, item.tag} + } + } +} + +const ( + nullTag = "!!null" + boolTag = "!!bool" + strTag = "!!str" + intTag = "!!int" + floatTag = "!!float" + timestampTag = "!!timestamp" + seqTag = "!!seq" + mapTag = "!!map" + binaryTag = "!!binary" + mergeTag = "!!merge" +) + +var longTags = make(map[string]string) +var shortTags = make(map[string]string) + +func init() { + for _, stag := range []string{nullTag, boolTag, strTag, intTag, floatTag, timestampTag, seqTag, mapTag, binaryTag, mergeTag} { + ltag := longTag(stag) + longTags[stag] = ltag + shortTags[ltag] = stag + } +} + +const longTagPrefix = "tag:yaml.org,2002:" + +func shortTag(tag string) string { + if strings.HasPrefix(tag, longTagPrefix) { + if stag, ok := shortTags[tag]; ok { + return stag + } + return "!!" + tag[len(longTagPrefix):] + } + return tag +} + +func longTag(tag string) string { + if strings.HasPrefix(tag, "!!") { + if ltag, ok := longTags[tag]; ok { + return ltag + } + return longTagPrefix + tag[2:] + } + return tag +} + +func resolvableTag(tag string) bool { + switch tag { + case "", strTag, boolTag, intTag, floatTag, nullTag, timestampTag: + return true + } + return false +} + +var yamlStyleFloat = regexp.MustCompile(`^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$`) + +func resolve(tag string, in string) (rtag string, out interface{}) { + tag = shortTag(tag) + if !resolvableTag(tag) { + return tag, in + } + + defer func() { + switch tag { + case "", rtag, strTag, binaryTag: + return + case floatTag: + if rtag == intTag { + switch v := out.(type) { + case int64: + rtag = floatTag + out = float64(v) + return + case int: + rtag = floatTag + out = float64(v) + return + } + } + } + failf("cannot decode %s `%s` as a %s", shortTag(rtag), in, shortTag(tag)) + }() + + // Any data is accepted as a !!str or !!binary. + // Otherwise, the prefix is enough of a hint about what it might be. + hint := byte('N') + if in != "" { + hint = resolveTable[in[0]] + } + if hint != 0 && tag != strTag && tag != binaryTag { + // Handle things we can lookup in a map. + if item, ok := resolveMap[in]; ok { + return item.tag, item.value + } + + // Base 60 floats are a bad idea, were dropped in YAML 1.2, and + // are purposefully unsupported here. They're still quoted on + // the way out for compatibility with other parser, though. + + switch hint { + case 'M': + // We've already checked the map above. + + case '.': + // Not in the map, so maybe a normal float. + floatv, err := strconv.ParseFloat(in, 64) + if err == nil { + return floatTag, floatv + } + + case 'D', 'S': + // Int, float, or timestamp. + // Only try values as a timestamp if the value is unquoted or there's an explicit + // !!timestamp tag. + if tag == "" || tag == timestampTag { + t, ok := parseTimestamp(in) + if ok { + return timestampTag, t + } + } + + plain := strings.Replace(in, "_", "", -1) + intv, err := strconv.ParseInt(plain, 0, 64) + if err == nil { + if intv == int64(int(intv)) { + return intTag, int(intv) + } else { + return intTag, intv + } + } + uintv, err := strconv.ParseUint(plain, 0, 64) + if err == nil { + return intTag, uintv + } + if yamlStyleFloat.MatchString(plain) { + floatv, err := strconv.ParseFloat(plain, 64) + if err == nil { + return floatTag, floatv + } + } + if strings.HasPrefix(plain, "0b") { + intv, err := strconv.ParseInt(plain[2:], 2, 64) + if err == nil { + if intv == int64(int(intv)) { + return intTag, int(intv) + } else { + return intTag, intv + } + } + uintv, err := strconv.ParseUint(plain[2:], 2, 64) + if err == nil { + return intTag, uintv + } + } else if strings.HasPrefix(plain, "-0b") { + intv, err := strconv.ParseInt("-"+plain[3:], 2, 64) + if err == nil { + if true || intv == int64(int(intv)) { + return intTag, int(intv) + } else { + return intTag, intv + } + } + } + // Octals as introduced in version 1.2 of the spec. + // Octals from the 1.1 spec, spelled as 0777, are still + // decoded by default in v3 as well for compatibility. + // May be dropped in v4 depending on how usage evolves. + if strings.HasPrefix(plain, "0o") { + intv, err := strconv.ParseInt(plain[2:], 8, 64) + if err == nil { + if intv == int64(int(intv)) { + return intTag, int(intv) + } else { + return intTag, intv + } + } + uintv, err := strconv.ParseUint(plain[2:], 8, 64) + if err == nil { + return intTag, uintv + } + } else if strings.HasPrefix(plain, "-0o") { + intv, err := strconv.ParseInt("-"+plain[3:], 8, 64) + if err == nil { + if true || intv == int64(int(intv)) { + return intTag, int(intv) + } else { + return intTag, intv + } + } + } + default: + panic("internal error: missing handler for resolver table: " + string(rune(hint)) + " (with " + in + ")") + } + } + return strTag, in +} + +// encodeBase64 encodes s as base64 that is broken up into multiple lines +// as appropriate for the resulting length. +func encodeBase64(s string) string { + const lineLen = 70 + encLen := base64.StdEncoding.EncodedLen(len(s)) + lines := encLen/lineLen + 1 + buf := make([]byte, encLen*2+lines) + in := buf[0:encLen] + out := buf[encLen:] + base64.StdEncoding.Encode(in, []byte(s)) + k := 0 + for i := 0; i < len(in); i += lineLen { + j := i + lineLen + if j > len(in) { + j = len(in) + } + k += copy(out[k:], in[i:j]) + if lines > 1 { + out[k] = '\n' + k++ + } + } + return string(out[:k]) +} + +// This is a subset of the formats allowed by the regular expression +// defined at http://yaml.org/type/timestamp.html. +var allowedTimestampFormats = []string{ + "2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields. + "2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t". + "2006-1-2 15:4:5.999999999", // space separated with no time zone + "2006-1-2", // date only + // Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5" + // from the set of examples. +} + +// parseTimestamp parses s as a timestamp string and +// returns the timestamp and reports whether it succeeded. +// Timestamp formats are defined at http://yaml.org/type/timestamp.html +func parseTimestamp(s string) (time.Time, bool) { + // TODO write code to check all the formats supported by + // http://yaml.org/type/timestamp.html instead of using time.Parse. + + // Quick check: all date formats start with YYYY-. + i := 0 + for ; i < len(s); i++ { + if c := s[i]; c < '0' || c > '9' { + break + } + } + if i != 4 || i == len(s) || s[i] != '-' { + return time.Time{}, false + } + for _, format := range allowedTimestampFormats { + if t, err := time.Parse(format, s); err == nil { + return t, true + } + } + return time.Time{}, false +} diff --git a/vendor/go.yaml.in/yaml/v3/scannerc.go b/vendor/go.yaml.in/yaml/v3/scannerc.go new file mode 100644 index 000000000..30b1f0892 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/scannerc.go @@ -0,0 +1,3040 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// 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. + +package yaml + +import ( + "bytes" + "fmt" +) + +// Introduction +// ************ +// +// The following notes assume that you are familiar with the YAML specification +// (http://yaml.org/spec/1.2/spec.html). We mostly follow it, although in +// some cases we are less restrictive that it requires. +// +// The process of transforming a YAML stream into a sequence of events is +// divided on two steps: Scanning and Parsing. +// +// The Scanner transforms the input stream into a sequence of tokens, while the +// parser transform the sequence of tokens produced by the Scanner into a +// sequence of parsing events. +// +// The Scanner is rather clever and complicated. The Parser, on the contrary, +// is a straightforward implementation of a recursive-descendant parser (or, +// LL(1) parser, as it is usually called). +// +// Actually there are two issues of Scanning that might be called "clever", the +// rest is quite straightforward. The issues are "block collection start" and +// "simple keys". Both issues are explained below in details. +// +// Here the Scanning step is explained and implemented. We start with the list +// of all the tokens produced by the Scanner together with short descriptions. +// +// Now, tokens: +// +// STREAM-START(encoding) # The stream start. +// STREAM-END # The stream end. +// VERSION-DIRECTIVE(major,minor) # The '%YAML' directive. +// TAG-DIRECTIVE(handle,prefix) # The '%TAG' directive. +// DOCUMENT-START # '---' +// DOCUMENT-END # '...' +// BLOCK-SEQUENCE-START # Indentation increase denoting a block +// BLOCK-MAPPING-START # sequence or a block mapping. +// BLOCK-END # Indentation decrease. +// FLOW-SEQUENCE-START # '[' +// FLOW-SEQUENCE-END # ']' +// BLOCK-SEQUENCE-START # '{' +// BLOCK-SEQUENCE-END # '}' +// BLOCK-ENTRY # '-' +// FLOW-ENTRY # ',' +// KEY # '?' or nothing (simple keys). +// VALUE # ':' +// ALIAS(anchor) # '*anchor' +// ANCHOR(anchor) # '&anchor' +// TAG(handle,suffix) # '!handle!suffix' +// SCALAR(value,style) # A scalar. +// +// The following two tokens are "virtual" tokens denoting the beginning and the +// end of the stream: +// +// STREAM-START(encoding) +// STREAM-END +// +// We pass the information about the input stream encoding with the +// STREAM-START token. +// +// The next two tokens are responsible for tags: +// +// VERSION-DIRECTIVE(major,minor) +// TAG-DIRECTIVE(handle,prefix) +// +// Example: +// +// %YAML 1.1 +// %TAG ! !foo +// %TAG !yaml! tag:yaml.org,2002: +// --- +// +// The correspoding sequence of tokens: +// +// STREAM-START(utf-8) +// VERSION-DIRECTIVE(1,1) +// TAG-DIRECTIVE("!","!foo") +// TAG-DIRECTIVE("!yaml","tag:yaml.org,2002:") +// DOCUMENT-START +// STREAM-END +// +// Note that the VERSION-DIRECTIVE and TAG-DIRECTIVE tokens occupy a whole +// line. +// +// The document start and end indicators are represented by: +// +// DOCUMENT-START +// DOCUMENT-END +// +// Note that if a YAML stream contains an implicit document (without '---' +// and '...' indicators), no DOCUMENT-START and DOCUMENT-END tokens will be +// produced. +// +// In the following examples, we present whole documents together with the +// produced tokens. +// +// 1. An implicit document: +// +// 'a scalar' +// +// Tokens: +// +// STREAM-START(utf-8) +// SCALAR("a scalar",single-quoted) +// STREAM-END +// +// 2. An explicit document: +// +// --- +// 'a scalar' +// ... +// +// Tokens: +// +// STREAM-START(utf-8) +// DOCUMENT-START +// SCALAR("a scalar",single-quoted) +// DOCUMENT-END +// STREAM-END +// +// 3. Several documents in a stream: +// +// 'a scalar' +// --- +// 'another scalar' +// --- +// 'yet another scalar' +// +// Tokens: +// +// STREAM-START(utf-8) +// SCALAR("a scalar",single-quoted) +// DOCUMENT-START +// SCALAR("another scalar",single-quoted) +// DOCUMENT-START +// SCALAR("yet another scalar",single-quoted) +// STREAM-END +// +// We have already introduced the SCALAR token above. The following tokens are +// used to describe aliases, anchors, tag, and scalars: +// +// ALIAS(anchor) +// ANCHOR(anchor) +// TAG(handle,suffix) +// SCALAR(value,style) +// +// The following series of examples illustrate the usage of these tokens: +// +// 1. A recursive sequence: +// +// &A [ *A ] +// +// Tokens: +// +// STREAM-START(utf-8) +// ANCHOR("A") +// FLOW-SEQUENCE-START +// ALIAS("A") +// FLOW-SEQUENCE-END +// STREAM-END +// +// 2. A tagged scalar: +// +// !!float "3.14" # A good approximation. +// +// Tokens: +// +// STREAM-START(utf-8) +// TAG("!!","float") +// SCALAR("3.14",double-quoted) +// STREAM-END +// +// 3. Various scalar styles: +// +// --- # Implicit empty plain scalars do not produce tokens. +// --- a plain scalar +// --- 'a single-quoted scalar' +// --- "a double-quoted scalar" +// --- |- +// a literal scalar +// --- >- +// a folded +// scalar +// +// Tokens: +// +// STREAM-START(utf-8) +// DOCUMENT-START +// DOCUMENT-START +// SCALAR("a plain scalar",plain) +// DOCUMENT-START +// SCALAR("a single-quoted scalar",single-quoted) +// DOCUMENT-START +// SCALAR("a double-quoted scalar",double-quoted) +// DOCUMENT-START +// SCALAR("a literal scalar",literal) +// DOCUMENT-START +// SCALAR("a folded scalar",folded) +// STREAM-END +// +// Now it's time to review collection-related tokens. We will start with +// flow collections: +// +// FLOW-SEQUENCE-START +// FLOW-SEQUENCE-END +// FLOW-MAPPING-START +// FLOW-MAPPING-END +// FLOW-ENTRY +// KEY +// VALUE +// +// The tokens FLOW-SEQUENCE-START, FLOW-SEQUENCE-END, FLOW-MAPPING-START, and +// FLOW-MAPPING-END represent the indicators '[', ']', '{', and '}' +// correspondingly. FLOW-ENTRY represent the ',' indicator. Finally the +// indicators '?' and ':', which are used for denoting mapping keys and values, +// are represented by the KEY and VALUE tokens. +// +// The following examples show flow collections: +// +// 1. A flow sequence: +// +// [item 1, item 2, item 3] +// +// Tokens: +// +// STREAM-START(utf-8) +// FLOW-SEQUENCE-START +// SCALAR("item 1",plain) +// FLOW-ENTRY +// SCALAR("item 2",plain) +// FLOW-ENTRY +// SCALAR("item 3",plain) +// FLOW-SEQUENCE-END +// STREAM-END +// +// 2. A flow mapping: +// +// { +// a simple key: a value, # Note that the KEY token is produced. +// ? a complex key: another value, +// } +// +// Tokens: +// +// STREAM-START(utf-8) +// FLOW-MAPPING-START +// KEY +// SCALAR("a simple key",plain) +// VALUE +// SCALAR("a value",plain) +// FLOW-ENTRY +// KEY +// SCALAR("a complex key",plain) +// VALUE +// SCALAR("another value",plain) +// FLOW-ENTRY +// FLOW-MAPPING-END +// STREAM-END +// +// A simple key is a key which is not denoted by the '?' indicator. Note that +// the Scanner still produce the KEY token whenever it encounters a simple key. +// +// For scanning block collections, the following tokens are used (note that we +// repeat KEY and VALUE here): +// +// BLOCK-SEQUENCE-START +// BLOCK-MAPPING-START +// BLOCK-END +// BLOCK-ENTRY +// KEY +// VALUE +// +// The tokens BLOCK-SEQUENCE-START and BLOCK-MAPPING-START denote indentation +// increase that precedes a block collection (cf. the INDENT token in Python). +// The token BLOCK-END denote indentation decrease that ends a block collection +// (cf. the DEDENT token in Python). However YAML has some syntax pecularities +// that makes detections of these tokens more complex. +// +// The tokens BLOCK-ENTRY, KEY, and VALUE are used to represent the indicators +// '-', '?', and ':' correspondingly. +// +// The following examples show how the tokens BLOCK-SEQUENCE-START, +// BLOCK-MAPPING-START, and BLOCK-END are emitted by the Scanner: +// +// 1. Block sequences: +// +// - item 1 +// - item 2 +// - +// - item 3.1 +// - item 3.2 +// - +// key 1: value 1 +// key 2: value 2 +// +// Tokens: +// +// STREAM-START(utf-8) +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// SCALAR("item 1",plain) +// BLOCK-ENTRY +// SCALAR("item 2",plain) +// BLOCK-ENTRY +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// SCALAR("item 3.1",plain) +// BLOCK-ENTRY +// SCALAR("item 3.2",plain) +// BLOCK-END +// BLOCK-ENTRY +// BLOCK-MAPPING-START +// KEY +// SCALAR("key 1",plain) +// VALUE +// SCALAR("value 1",plain) +// KEY +// SCALAR("key 2",plain) +// VALUE +// SCALAR("value 2",plain) +// BLOCK-END +// BLOCK-END +// STREAM-END +// +// 2. Block mappings: +// +// a simple key: a value # The KEY token is produced here. +// ? a complex key +// : another value +// a mapping: +// key 1: value 1 +// key 2: value 2 +// a sequence: +// - item 1 +// - item 2 +// +// Tokens: +// +// STREAM-START(utf-8) +// BLOCK-MAPPING-START +// KEY +// SCALAR("a simple key",plain) +// VALUE +// SCALAR("a value",plain) +// KEY +// SCALAR("a complex key",plain) +// VALUE +// SCALAR("another value",plain) +// KEY +// SCALAR("a mapping",plain) +// BLOCK-MAPPING-START +// KEY +// SCALAR("key 1",plain) +// VALUE +// SCALAR("value 1",plain) +// KEY +// SCALAR("key 2",plain) +// VALUE +// SCALAR("value 2",plain) +// BLOCK-END +// KEY +// SCALAR("a sequence",plain) +// VALUE +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// SCALAR("item 1",plain) +// BLOCK-ENTRY +// SCALAR("item 2",plain) +// BLOCK-END +// BLOCK-END +// STREAM-END +// +// YAML does not always require to start a new block collection from a new +// line. If the current line contains only '-', '?', and ':' indicators, a new +// block collection may start at the current line. The following examples +// illustrate this case: +// +// 1. Collections in a sequence: +// +// - - item 1 +// - item 2 +// - key 1: value 1 +// key 2: value 2 +// - ? complex key +// : complex value +// +// Tokens: +// +// STREAM-START(utf-8) +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// SCALAR("item 1",plain) +// BLOCK-ENTRY +// SCALAR("item 2",plain) +// BLOCK-END +// BLOCK-ENTRY +// BLOCK-MAPPING-START +// KEY +// SCALAR("key 1",plain) +// VALUE +// SCALAR("value 1",plain) +// KEY +// SCALAR("key 2",plain) +// VALUE +// SCALAR("value 2",plain) +// BLOCK-END +// BLOCK-ENTRY +// BLOCK-MAPPING-START +// KEY +// SCALAR("complex key") +// VALUE +// SCALAR("complex value") +// BLOCK-END +// BLOCK-END +// STREAM-END +// +// 2. Collections in a mapping: +// +// ? a sequence +// : - item 1 +// - item 2 +// ? a mapping +// : key 1: value 1 +// key 2: value 2 +// +// Tokens: +// +// STREAM-START(utf-8) +// BLOCK-MAPPING-START +// KEY +// SCALAR("a sequence",plain) +// VALUE +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// SCALAR("item 1",plain) +// BLOCK-ENTRY +// SCALAR("item 2",plain) +// BLOCK-END +// KEY +// SCALAR("a mapping",plain) +// VALUE +// BLOCK-MAPPING-START +// KEY +// SCALAR("key 1",plain) +// VALUE +// SCALAR("value 1",plain) +// KEY +// SCALAR("key 2",plain) +// VALUE +// SCALAR("value 2",plain) +// BLOCK-END +// BLOCK-END +// STREAM-END +// +// YAML also permits non-indented sequences if they are included into a block +// mapping. In this case, the token BLOCK-SEQUENCE-START is not produced: +// +// key: +// - item 1 # BLOCK-SEQUENCE-START is NOT produced here. +// - item 2 +// +// Tokens: +// +// STREAM-START(utf-8) +// BLOCK-MAPPING-START +// KEY +// SCALAR("key",plain) +// VALUE +// BLOCK-ENTRY +// SCALAR("item 1",plain) +// BLOCK-ENTRY +// SCALAR("item 2",plain) +// BLOCK-END +// + +// Ensure that the buffer contains the required number of characters. +// Return true on success, false on failure (reader error or memory error). +func cache(parser *yaml_parser_t, length int) bool { + // [Go] This was inlined: !cache(A, B) -> unread < B && !update(A, B) + return parser.unread >= length || yaml_parser_update_buffer(parser, length) +} + +// Advance the buffer pointer. +func skip(parser *yaml_parser_t) { + if !is_blank(parser.buffer, parser.buffer_pos) { + parser.newlines = 0 + } + parser.mark.index++ + parser.mark.column++ + parser.unread-- + parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) +} + +func skip_line(parser *yaml_parser_t) { + if is_crlf(parser.buffer, parser.buffer_pos) { + parser.mark.index += 2 + parser.mark.column = 0 + parser.mark.line++ + parser.unread -= 2 + parser.buffer_pos += 2 + parser.newlines++ + } else if is_break(parser.buffer, parser.buffer_pos) { + parser.mark.index++ + parser.mark.column = 0 + parser.mark.line++ + parser.unread-- + parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) + parser.newlines++ + } +} + +// Copy a character to a string buffer and advance pointers. +func read(parser *yaml_parser_t, s []byte) []byte { + if !is_blank(parser.buffer, parser.buffer_pos) { + parser.newlines = 0 + } + w := width(parser.buffer[parser.buffer_pos]) + if w == 0 { + panic("invalid character sequence") + } + if len(s) == 0 { + s = make([]byte, 0, 32) + } + if w == 1 && len(s)+w <= cap(s) { + s = s[:len(s)+1] + s[len(s)-1] = parser.buffer[parser.buffer_pos] + parser.buffer_pos++ + } else { + s = append(s, parser.buffer[parser.buffer_pos:parser.buffer_pos+w]...) + parser.buffer_pos += w + } + parser.mark.index++ + parser.mark.column++ + parser.unread-- + return s +} + +// Copy a line break character to a string buffer and advance pointers. +func read_line(parser *yaml_parser_t, s []byte) []byte { + buf := parser.buffer + pos := parser.buffer_pos + switch { + case buf[pos] == '\r' && buf[pos+1] == '\n': + // CR LF . LF + s = append(s, '\n') + parser.buffer_pos += 2 + parser.mark.index++ + parser.unread-- + case buf[pos] == '\r' || buf[pos] == '\n': + // CR|LF . LF + s = append(s, '\n') + parser.buffer_pos += 1 + case buf[pos] == '\xC2' && buf[pos+1] == '\x85': + // NEL . LF + s = append(s, '\n') + parser.buffer_pos += 2 + case buf[pos] == '\xE2' && buf[pos+1] == '\x80' && (buf[pos+2] == '\xA8' || buf[pos+2] == '\xA9'): + // LS|PS . LS|PS + s = append(s, buf[parser.buffer_pos:pos+3]...) + parser.buffer_pos += 3 + default: + return s + } + parser.mark.index++ + parser.mark.column = 0 + parser.mark.line++ + parser.unread-- + parser.newlines++ + return s +} + +// Get the next token. +func yaml_parser_scan(parser *yaml_parser_t, token *yaml_token_t) bool { + // Erase the token object. + *token = yaml_token_t{} // [Go] Is this necessary? + + // No tokens after STREAM-END or error. + if parser.stream_end_produced || parser.error != yaml_NO_ERROR { + return true + } + + // Ensure that the tokens queue contains enough tokens. + if !parser.token_available { + if !yaml_parser_fetch_more_tokens(parser) { + return false + } + } + + // Fetch the next token from the queue. + *token = parser.tokens[parser.tokens_head] + parser.tokens_head++ + parser.tokens_parsed++ + parser.token_available = false + + if token.typ == yaml_STREAM_END_TOKEN { + parser.stream_end_produced = true + } + return true +} + +// Set the scanner error and return false. +func yaml_parser_set_scanner_error(parser *yaml_parser_t, context string, context_mark yaml_mark_t, problem string) bool { + parser.error = yaml_SCANNER_ERROR + parser.context = context + parser.context_mark = context_mark + parser.problem = problem + parser.problem_mark = parser.mark + return false +} + +func yaml_parser_set_scanner_tag_error(parser *yaml_parser_t, directive bool, context_mark yaml_mark_t, problem string) bool { + context := "while parsing a tag" + if directive { + context = "while parsing a %TAG directive" + } + return yaml_parser_set_scanner_error(parser, context, context_mark, problem) +} + +func trace(args ...interface{}) func() { + pargs := append([]interface{}{"+++"}, args...) + fmt.Println(pargs...) + pargs = append([]interface{}{"---"}, args...) + return func() { fmt.Println(pargs...) } +} + +// Ensure that the tokens queue contains at least one token which can be +// returned to the Parser. +func yaml_parser_fetch_more_tokens(parser *yaml_parser_t) bool { + // While we need more tokens to fetch, do it. + for { + // [Go] The comment parsing logic requires a lookahead of two tokens + // so that foot comments may be parsed in time of associating them + // with the tokens that are parsed before them, and also for line + // comments to be transformed into head comments in some edge cases. + if parser.tokens_head < len(parser.tokens)-2 { + // If a potential simple key is at the head position, we need to fetch + // the next token to disambiguate it. + head_tok_idx, ok := parser.simple_keys_by_tok[parser.tokens_parsed] + if !ok { + break + } else if valid, ok := yaml_simple_key_is_valid(parser, &parser.simple_keys[head_tok_idx]); !ok { + return false + } else if !valid { + break + } + } + // Fetch the next token. + if !yaml_parser_fetch_next_token(parser) { + return false + } + } + + parser.token_available = true + return true +} + +// The dispatcher for token fetchers. +func yaml_parser_fetch_next_token(parser *yaml_parser_t) (ok bool) { + // Ensure that the buffer is initialized. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + // Check if we just started scanning. Fetch STREAM-START then. + if !parser.stream_start_produced { + return yaml_parser_fetch_stream_start(parser) + } + + scan_mark := parser.mark + + // Eat whitespaces and comments until we reach the next token. + if !yaml_parser_scan_to_next_token(parser) { + return false + } + + // [Go] While unrolling indents, transform the head comments of prior + // indentation levels observed after scan_start into foot comments at + // the respective indexes. + + // Check the indentation level against the current column. + if !yaml_parser_unroll_indent(parser, parser.mark.column, scan_mark) { + return false + } + + // Ensure that the buffer contains at least 4 characters. 4 is the length + // of the longest indicators ('--- ' and '... '). + if parser.unread < 4 && !yaml_parser_update_buffer(parser, 4) { + return false + } + + // Is it the end of the stream? + if is_z(parser.buffer, parser.buffer_pos) { + return yaml_parser_fetch_stream_end(parser) + } + + // Is it a directive? + if parser.mark.column == 0 && parser.buffer[parser.buffer_pos] == '%' { + return yaml_parser_fetch_directive(parser) + } + + buf := parser.buffer + pos := parser.buffer_pos + + // Is it the document start indicator? + if parser.mark.column == 0 && buf[pos] == '-' && buf[pos+1] == '-' && buf[pos+2] == '-' && is_blankz(buf, pos+3) { + return yaml_parser_fetch_document_indicator(parser, yaml_DOCUMENT_START_TOKEN) + } + + // Is it the document end indicator? + if parser.mark.column == 0 && buf[pos] == '.' && buf[pos+1] == '.' && buf[pos+2] == '.' && is_blankz(buf, pos+3) { + return yaml_parser_fetch_document_indicator(parser, yaml_DOCUMENT_END_TOKEN) + } + + comment_mark := parser.mark + if len(parser.tokens) > 0 && (parser.flow_level == 0 && buf[pos] == ':' || parser.flow_level > 0 && buf[pos] == ',') { + // Associate any following comments with the prior token. + comment_mark = parser.tokens[len(parser.tokens)-1].start_mark + } + defer func() { + if !ok { + return + } + if len(parser.tokens) > 0 && parser.tokens[len(parser.tokens)-1].typ == yaml_BLOCK_ENTRY_TOKEN { + // Sequence indicators alone have no line comments. It becomes + // a head comment for whatever follows. + return + } + if !yaml_parser_scan_line_comment(parser, comment_mark) { + ok = false + return + } + }() + + // Is it the flow sequence start indicator? + if buf[pos] == '[' { + return yaml_parser_fetch_flow_collection_start(parser, yaml_FLOW_SEQUENCE_START_TOKEN) + } + + // Is it the flow mapping start indicator? + if parser.buffer[parser.buffer_pos] == '{' { + return yaml_parser_fetch_flow_collection_start(parser, yaml_FLOW_MAPPING_START_TOKEN) + } + + // Is it the flow sequence end indicator? + if parser.buffer[parser.buffer_pos] == ']' { + return yaml_parser_fetch_flow_collection_end(parser, + yaml_FLOW_SEQUENCE_END_TOKEN) + } + + // Is it the flow mapping end indicator? + if parser.buffer[parser.buffer_pos] == '}' { + return yaml_parser_fetch_flow_collection_end(parser, + yaml_FLOW_MAPPING_END_TOKEN) + } + + // Is it the flow entry indicator? + if parser.buffer[parser.buffer_pos] == ',' { + return yaml_parser_fetch_flow_entry(parser) + } + + // Is it the block entry indicator? + if parser.buffer[parser.buffer_pos] == '-' && is_blankz(parser.buffer, parser.buffer_pos+1) { + return yaml_parser_fetch_block_entry(parser) + } + + // Is it the key indicator? + if parser.buffer[parser.buffer_pos] == '?' && (parser.flow_level > 0 || is_blankz(parser.buffer, parser.buffer_pos+1)) { + return yaml_parser_fetch_key(parser) + } + + // Is it the value indicator? + if parser.buffer[parser.buffer_pos] == ':' && (parser.flow_level > 0 || is_blankz(parser.buffer, parser.buffer_pos+1)) { + return yaml_parser_fetch_value(parser) + } + + // Is it an alias? + if parser.buffer[parser.buffer_pos] == '*' { + return yaml_parser_fetch_anchor(parser, yaml_ALIAS_TOKEN) + } + + // Is it an anchor? + if parser.buffer[parser.buffer_pos] == '&' { + return yaml_parser_fetch_anchor(parser, yaml_ANCHOR_TOKEN) + } + + // Is it a tag? + if parser.buffer[parser.buffer_pos] == '!' { + return yaml_parser_fetch_tag(parser) + } + + // Is it a literal scalar? + if parser.buffer[parser.buffer_pos] == '|' && parser.flow_level == 0 { + return yaml_parser_fetch_block_scalar(parser, true) + } + + // Is it a folded scalar? + if parser.buffer[parser.buffer_pos] == '>' && parser.flow_level == 0 { + return yaml_parser_fetch_block_scalar(parser, false) + } + + // Is it a single-quoted scalar? + if parser.buffer[parser.buffer_pos] == '\'' { + return yaml_parser_fetch_flow_scalar(parser, true) + } + + // Is it a double-quoted scalar? + if parser.buffer[parser.buffer_pos] == '"' { + return yaml_parser_fetch_flow_scalar(parser, false) + } + + // Is it a plain scalar? + // + // A plain scalar may start with any non-blank characters except + // + // '-', '?', ':', ',', '[', ']', '{', '}', + // '#', '&', '*', '!', '|', '>', '\'', '\"', + // '%', '@', '`'. + // + // In the block context (and, for the '-' indicator, in the flow context + // too), it may also start with the characters + // + // '-', '?', ':' + // + // if it is followed by a non-space character. + // + // The last rule is more restrictive than the specification requires. + // [Go] TODO Make this logic more reasonable. + //switch parser.buffer[parser.buffer_pos] { + //case '-', '?', ':', ',', '?', '-', ',', ':', ']', '[', '}', '{', '&', '#', '!', '*', '>', '|', '"', '\'', '@', '%', '-', '`': + //} + if !(is_blankz(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == '-' || + parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == ':' || + parser.buffer[parser.buffer_pos] == ',' || parser.buffer[parser.buffer_pos] == '[' || + parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '{' || + parser.buffer[parser.buffer_pos] == '}' || parser.buffer[parser.buffer_pos] == '#' || + parser.buffer[parser.buffer_pos] == '&' || parser.buffer[parser.buffer_pos] == '*' || + parser.buffer[parser.buffer_pos] == '!' || parser.buffer[parser.buffer_pos] == '|' || + parser.buffer[parser.buffer_pos] == '>' || parser.buffer[parser.buffer_pos] == '\'' || + parser.buffer[parser.buffer_pos] == '"' || parser.buffer[parser.buffer_pos] == '%' || + parser.buffer[parser.buffer_pos] == '@' || parser.buffer[parser.buffer_pos] == '`') || + (parser.buffer[parser.buffer_pos] == '-' && !is_blank(parser.buffer, parser.buffer_pos+1)) || + (parser.flow_level == 0 && + (parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == ':') && + !is_blankz(parser.buffer, parser.buffer_pos+1)) { + return yaml_parser_fetch_plain_scalar(parser) + } + + // If we don't determine the token type so far, it is an error. + return yaml_parser_set_scanner_error(parser, + "while scanning for the next token", parser.mark, + "found character that cannot start any token") +} + +func yaml_simple_key_is_valid(parser *yaml_parser_t, simple_key *yaml_simple_key_t) (valid, ok bool) { + if !simple_key.possible { + return false, true + } + + // The 1.2 specification says: + // + // "If the ? indicator is omitted, parsing needs to see past the + // implicit key to recognize it as such. To limit the amount of + // lookahead required, the “:” indicator must appear at most 1024 + // Unicode characters beyond the start of the key. In addition, the key + // is restricted to a single line." + // + if simple_key.mark.line < parser.mark.line || simple_key.mark.index+1024 < parser.mark.index { + // Check if the potential simple key to be removed is required. + if simple_key.required { + return false, yaml_parser_set_scanner_error(parser, + "while scanning a simple key", simple_key.mark, + "could not find expected ':'") + } + simple_key.possible = false + return false, true + } + return true, true +} + +// Check if a simple key may start at the current position and add it if +// needed. +func yaml_parser_save_simple_key(parser *yaml_parser_t) bool { + // A simple key is required at the current position if the scanner is in + // the block context and the current column coincides with the indentation + // level. + + required := parser.flow_level == 0 && parser.indent == parser.mark.column + + // + // If the current position may start a simple key, save it. + // + if parser.simple_key_allowed { + simple_key := yaml_simple_key_t{ + possible: true, + required: required, + token_number: parser.tokens_parsed + (len(parser.tokens) - parser.tokens_head), + mark: parser.mark, + } + + if !yaml_parser_remove_simple_key(parser) { + return false + } + parser.simple_keys[len(parser.simple_keys)-1] = simple_key + parser.simple_keys_by_tok[simple_key.token_number] = len(parser.simple_keys) - 1 + } + return true +} + +// Remove a potential simple key at the current flow level. +func yaml_parser_remove_simple_key(parser *yaml_parser_t) bool { + i := len(parser.simple_keys) - 1 + if parser.simple_keys[i].possible { + // If the key is required, it is an error. + if parser.simple_keys[i].required { + return yaml_parser_set_scanner_error(parser, + "while scanning a simple key", parser.simple_keys[i].mark, + "could not find expected ':'") + } + // Remove the key from the stack. + parser.simple_keys[i].possible = false + delete(parser.simple_keys_by_tok, parser.simple_keys[i].token_number) + } + return true +} + +// max_flow_level limits the flow_level +const max_flow_level = 10000 + +// Increase the flow level and resize the simple key list if needed. +func yaml_parser_increase_flow_level(parser *yaml_parser_t) bool { + // Reset the simple key on the next level. + parser.simple_keys = append(parser.simple_keys, yaml_simple_key_t{ + possible: false, + required: false, + token_number: parser.tokens_parsed + (len(parser.tokens) - parser.tokens_head), + mark: parser.mark, + }) + + // Increase the flow level. + parser.flow_level++ + if parser.flow_level > max_flow_level { + return yaml_parser_set_scanner_error(parser, + "while increasing flow level", parser.simple_keys[len(parser.simple_keys)-1].mark, + fmt.Sprintf("exceeded max depth of %d", max_flow_level)) + } + return true +} + +// Decrease the flow level. +func yaml_parser_decrease_flow_level(parser *yaml_parser_t) bool { + if parser.flow_level > 0 { + parser.flow_level-- + last := len(parser.simple_keys) - 1 + delete(parser.simple_keys_by_tok, parser.simple_keys[last].token_number) + parser.simple_keys = parser.simple_keys[:last] + } + return true +} + +// max_indents limits the indents stack size +const max_indents = 10000 + +// Push the current indentation level to the stack and set the new level +// the current column is greater than the indentation level. In this case, +// append or insert the specified token into the token queue. +func yaml_parser_roll_indent(parser *yaml_parser_t, column, number int, typ yaml_token_type_t, mark yaml_mark_t) bool { + // In the flow context, do nothing. + if parser.flow_level > 0 { + return true + } + + if parser.indent < column { + // Push the current indentation level to the stack and set the new + // indentation level. + parser.indents = append(parser.indents, parser.indent) + parser.indent = column + if len(parser.indents) > max_indents { + return yaml_parser_set_scanner_error(parser, + "while increasing indent level", parser.simple_keys[len(parser.simple_keys)-1].mark, + fmt.Sprintf("exceeded max depth of %d", max_indents)) + } + + // Create a token and insert it into the queue. + token := yaml_token_t{ + typ: typ, + start_mark: mark, + end_mark: mark, + } + if number > -1 { + number -= parser.tokens_parsed + } + yaml_insert_token(parser, number, &token) + } + return true +} + +// Pop indentation levels from the indents stack until the current level +// becomes less or equal to the column. For each indentation level, append +// the BLOCK-END token. +func yaml_parser_unroll_indent(parser *yaml_parser_t, column int, scan_mark yaml_mark_t) bool { + // In the flow context, do nothing. + if parser.flow_level > 0 { + return true + } + + block_mark := scan_mark + block_mark.index-- + + // Loop through the indentation levels in the stack. + for parser.indent > column { + + // [Go] Reposition the end token before potential following + // foot comments of parent blocks. For that, search + // backwards for recent comments that were at the same + // indent as the block that is ending now. + stop_index := block_mark.index + for i := len(parser.comments) - 1; i >= 0; i-- { + comment := &parser.comments[i] + + if comment.end_mark.index < stop_index { + // Don't go back beyond the start of the comment/whitespace scan, unless column < 0. + // If requested indent column is < 0, then the document is over and everything else + // is a foot anyway. + break + } + if comment.start_mark.column == parser.indent+1 { + // This is a good match. But maybe there's a former comment + // at that same indent level, so keep searching. + block_mark = comment.start_mark + } + + // While the end of the former comment matches with + // the start of the following one, we know there's + // nothing in between and scanning is still safe. + stop_index = comment.scan_mark.index + } + + // Create a token and append it to the queue. + token := yaml_token_t{ + typ: yaml_BLOCK_END_TOKEN, + start_mark: block_mark, + end_mark: block_mark, + } + yaml_insert_token(parser, -1, &token) + + // Pop the indentation level. + parser.indent = parser.indents[len(parser.indents)-1] + parser.indents = parser.indents[:len(parser.indents)-1] + } + return true +} + +// Initialize the scanner and produce the STREAM-START token. +func yaml_parser_fetch_stream_start(parser *yaml_parser_t) bool { + + // Set the initial indentation. + parser.indent = -1 + + // Initialize the simple key stack. + parser.simple_keys = append(parser.simple_keys, yaml_simple_key_t{}) + + parser.simple_keys_by_tok = make(map[int]int) + + // A simple key is allowed at the beginning of the stream. + parser.simple_key_allowed = true + + // We have started. + parser.stream_start_produced = true + + // Create the STREAM-START token and append it to the queue. + token := yaml_token_t{ + typ: yaml_STREAM_START_TOKEN, + start_mark: parser.mark, + end_mark: parser.mark, + encoding: parser.encoding, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the STREAM-END token and shut down the scanner. +func yaml_parser_fetch_stream_end(parser *yaml_parser_t) bool { + + // Force new line. + if parser.mark.column != 0 { + parser.mark.column = 0 + parser.mark.line++ + } + + // Reset the indentation level. + if !yaml_parser_unroll_indent(parser, -1, parser.mark) { + return false + } + + // Reset simple keys. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + parser.simple_key_allowed = false + + // Create the STREAM-END token and append it to the queue. + token := yaml_token_t{ + typ: yaml_STREAM_END_TOKEN, + start_mark: parser.mark, + end_mark: parser.mark, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce a VERSION-DIRECTIVE or TAG-DIRECTIVE token. +func yaml_parser_fetch_directive(parser *yaml_parser_t) bool { + // Reset the indentation level. + if !yaml_parser_unroll_indent(parser, -1, parser.mark) { + return false + } + + // Reset simple keys. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + parser.simple_key_allowed = false + + // Create the YAML-DIRECTIVE or TAG-DIRECTIVE token. + token := yaml_token_t{} + if !yaml_parser_scan_directive(parser, &token) { + return false + } + // Append the token to the queue. + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the DOCUMENT-START or DOCUMENT-END token. +func yaml_parser_fetch_document_indicator(parser *yaml_parser_t, typ yaml_token_type_t) bool { + // Reset the indentation level. + if !yaml_parser_unroll_indent(parser, -1, parser.mark) { + return false + } + + // Reset simple keys. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + parser.simple_key_allowed = false + + // Consume the token. + start_mark := parser.mark + + skip(parser) + skip(parser) + skip(parser) + + end_mark := parser.mark + + // Create the DOCUMENT-START or DOCUMENT-END token. + token := yaml_token_t{ + typ: typ, + start_mark: start_mark, + end_mark: end_mark, + } + // Append the token to the queue. + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the FLOW-SEQUENCE-START or FLOW-MAPPING-START token. +func yaml_parser_fetch_flow_collection_start(parser *yaml_parser_t, typ yaml_token_type_t) bool { + + // The indicators '[' and '{' may start a simple key. + if !yaml_parser_save_simple_key(parser) { + return false + } + + // Increase the flow level. + if !yaml_parser_increase_flow_level(parser) { + return false + } + + // A simple key may follow the indicators '[' and '{'. + parser.simple_key_allowed = true + + // Consume the token. + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the FLOW-SEQUENCE-START of FLOW-MAPPING-START token. + token := yaml_token_t{ + typ: typ, + start_mark: start_mark, + end_mark: end_mark, + } + // Append the token to the queue. + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the FLOW-SEQUENCE-END or FLOW-MAPPING-END token. +func yaml_parser_fetch_flow_collection_end(parser *yaml_parser_t, typ yaml_token_type_t) bool { + // Reset any potential simple key on the current flow level. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + // Decrease the flow level. + if !yaml_parser_decrease_flow_level(parser) { + return false + } + + // No simple keys after the indicators ']' and '}'. + parser.simple_key_allowed = false + + // Consume the token. + + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the FLOW-SEQUENCE-END of FLOW-MAPPING-END token. + token := yaml_token_t{ + typ: typ, + start_mark: start_mark, + end_mark: end_mark, + } + // Append the token to the queue. + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the FLOW-ENTRY token. +func yaml_parser_fetch_flow_entry(parser *yaml_parser_t) bool { + // Reset any potential simple keys on the current flow level. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + // Simple keys are allowed after ','. + parser.simple_key_allowed = true + + // Consume the token. + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the FLOW-ENTRY token and append it to the queue. + token := yaml_token_t{ + typ: yaml_FLOW_ENTRY_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the BLOCK-ENTRY token. +func yaml_parser_fetch_block_entry(parser *yaml_parser_t) bool { + // Check if the scanner is in the block context. + if parser.flow_level == 0 { + // Check if we are allowed to start a new entry. + if !parser.simple_key_allowed { + return yaml_parser_set_scanner_error(parser, "", parser.mark, + "block sequence entries are not allowed in this context") + } + // Add the BLOCK-SEQUENCE-START token if needed. + if !yaml_parser_roll_indent(parser, parser.mark.column, -1, yaml_BLOCK_SEQUENCE_START_TOKEN, parser.mark) { + return false + } + } else { + // It is an error for the '-' indicator to occur in the flow context, + // but we let the Parser detect and report about it because the Parser + // is able to point to the context. + } + + // Reset any potential simple keys on the current flow level. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + // Simple keys are allowed after '-'. + parser.simple_key_allowed = true + + // Consume the token. + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the BLOCK-ENTRY token and append it to the queue. + token := yaml_token_t{ + typ: yaml_BLOCK_ENTRY_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the KEY token. +func yaml_parser_fetch_key(parser *yaml_parser_t) bool { + + // In the block context, additional checks are required. + if parser.flow_level == 0 { + // Check if we are allowed to start a new key (not nessesary simple). + if !parser.simple_key_allowed { + return yaml_parser_set_scanner_error(parser, "", parser.mark, + "mapping keys are not allowed in this context") + } + // Add the BLOCK-MAPPING-START token if needed. + if !yaml_parser_roll_indent(parser, parser.mark.column, -1, yaml_BLOCK_MAPPING_START_TOKEN, parser.mark) { + return false + } + } + + // Reset any potential simple keys on the current flow level. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + // Simple keys are allowed after '?' in the block context. + parser.simple_key_allowed = parser.flow_level == 0 + + // Consume the token. + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the KEY token and append it to the queue. + token := yaml_token_t{ + typ: yaml_KEY_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the VALUE token. +func yaml_parser_fetch_value(parser *yaml_parser_t) bool { + + simple_key := &parser.simple_keys[len(parser.simple_keys)-1] + + // Have we found a simple key? + if valid, ok := yaml_simple_key_is_valid(parser, simple_key); !ok { + return false + + } else if valid { + + // Create the KEY token and insert it into the queue. + token := yaml_token_t{ + typ: yaml_KEY_TOKEN, + start_mark: simple_key.mark, + end_mark: simple_key.mark, + } + yaml_insert_token(parser, simple_key.token_number-parser.tokens_parsed, &token) + + // In the block context, we may need to add the BLOCK-MAPPING-START token. + if !yaml_parser_roll_indent(parser, simple_key.mark.column, + simple_key.token_number, + yaml_BLOCK_MAPPING_START_TOKEN, simple_key.mark) { + return false + } + + // Remove the simple key. + simple_key.possible = false + delete(parser.simple_keys_by_tok, simple_key.token_number) + + // A simple key cannot follow another simple key. + parser.simple_key_allowed = false + + } else { + // The ':' indicator follows a complex key. + + // In the block context, extra checks are required. + if parser.flow_level == 0 { + + // Check if we are allowed to start a complex value. + if !parser.simple_key_allowed { + return yaml_parser_set_scanner_error(parser, "", parser.mark, + "mapping values are not allowed in this context") + } + + // Add the BLOCK-MAPPING-START token if needed. + if !yaml_parser_roll_indent(parser, parser.mark.column, -1, yaml_BLOCK_MAPPING_START_TOKEN, parser.mark) { + return false + } + } + + // Simple keys after ':' are allowed in the block context. + parser.simple_key_allowed = parser.flow_level == 0 + } + + // Consume the token. + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the VALUE token and append it to the queue. + token := yaml_token_t{ + typ: yaml_VALUE_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the ALIAS or ANCHOR token. +func yaml_parser_fetch_anchor(parser *yaml_parser_t, typ yaml_token_type_t) bool { + // An anchor or an alias could be a simple key. + if !yaml_parser_save_simple_key(parser) { + return false + } + + // A simple key cannot follow an anchor or an alias. + parser.simple_key_allowed = false + + // Create the ALIAS or ANCHOR token and append it to the queue. + var token yaml_token_t + if !yaml_parser_scan_anchor(parser, &token, typ) { + return false + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the TAG token. +func yaml_parser_fetch_tag(parser *yaml_parser_t) bool { + // A tag could be a simple key. + if !yaml_parser_save_simple_key(parser) { + return false + } + + // A simple key cannot follow a tag. + parser.simple_key_allowed = false + + // Create the TAG token and append it to the queue. + var token yaml_token_t + if !yaml_parser_scan_tag(parser, &token) { + return false + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the SCALAR(...,literal) or SCALAR(...,folded) tokens. +func yaml_parser_fetch_block_scalar(parser *yaml_parser_t, literal bool) bool { + // Remove any potential simple keys. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + // A simple key may follow a block scalar. + parser.simple_key_allowed = true + + // Create the SCALAR token and append it to the queue. + var token yaml_token_t + if !yaml_parser_scan_block_scalar(parser, &token, literal) { + return false + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the SCALAR(...,single-quoted) or SCALAR(...,double-quoted) tokens. +func yaml_parser_fetch_flow_scalar(parser *yaml_parser_t, single bool) bool { + // A plain scalar could be a simple key. + if !yaml_parser_save_simple_key(parser) { + return false + } + + // A simple key cannot follow a flow scalar. + parser.simple_key_allowed = false + + // Create the SCALAR token and append it to the queue. + var token yaml_token_t + if !yaml_parser_scan_flow_scalar(parser, &token, single) { + return false + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the SCALAR(...,plain) token. +func yaml_parser_fetch_plain_scalar(parser *yaml_parser_t) bool { + // A plain scalar could be a simple key. + if !yaml_parser_save_simple_key(parser) { + return false + } + + // A simple key cannot follow a flow scalar. + parser.simple_key_allowed = false + + // Create the SCALAR token and append it to the queue. + var token yaml_token_t + if !yaml_parser_scan_plain_scalar(parser, &token) { + return false + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Eat whitespaces and comments until the next token is found. +func yaml_parser_scan_to_next_token(parser *yaml_parser_t) bool { + + scan_mark := parser.mark + + // Until the next token is not found. + for { + // Allow the BOM mark to start a line. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if parser.mark.column == 0 && is_bom(parser.buffer, parser.buffer_pos) { + skip(parser) + } + + // Eat whitespaces. + // Tabs are allowed: + // - in the flow context + // - in the block context, but not at the beginning of the line or + // after '-', '?', or ':' (complex value). + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + for parser.buffer[parser.buffer_pos] == ' ' || ((parser.flow_level > 0 || !parser.simple_key_allowed) && parser.buffer[parser.buffer_pos] == '\t') { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Check if we just had a line comment under a sequence entry that + // looks more like a header to the following content. Similar to this: + // + // - # The comment + // - Some data + // + // If so, transform the line comment to a head comment and reposition. + if len(parser.comments) > 0 && len(parser.tokens) > 1 { + tokenA := parser.tokens[len(parser.tokens)-2] + tokenB := parser.tokens[len(parser.tokens)-1] + comment := &parser.comments[len(parser.comments)-1] + if tokenA.typ == yaml_BLOCK_SEQUENCE_START_TOKEN && tokenB.typ == yaml_BLOCK_ENTRY_TOKEN && len(comment.line) > 0 && !is_break(parser.buffer, parser.buffer_pos) { + // If it was in the prior line, reposition so it becomes a + // header of the follow up token. Otherwise, keep it in place + // so it becomes a header of the former. + comment.head = comment.line + comment.line = nil + if comment.start_mark.line == parser.mark.line-1 { + comment.token_mark = parser.mark + } + } + } + + // Eat a comment until a line break. + if parser.buffer[parser.buffer_pos] == '#' { + if !yaml_parser_scan_comments(parser, scan_mark) { + return false + } + } + + // If it is a line break, eat it. + if is_break(parser.buffer, parser.buffer_pos) { + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + skip_line(parser) + + // In the block context, a new line may start a simple key. + if parser.flow_level == 0 { + parser.simple_key_allowed = true + } + } else { + break // We have found a token. + } + } + + return true +} + +// Scan a YAML-DIRECTIVE or TAG-DIRECTIVE token. +// +// Scope: +// +// %YAML 1.1 # a comment \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +func yaml_parser_scan_directive(parser *yaml_parser_t, token *yaml_token_t) bool { + // Eat '%'. + start_mark := parser.mark + skip(parser) + + // Scan the directive name. + var name []byte + if !yaml_parser_scan_directive_name(parser, start_mark, &name) { + return false + } + + // Is it a YAML directive? + if bytes.Equal(name, []byte("YAML")) { + // Scan the VERSION directive value. + var major, minor int8 + if !yaml_parser_scan_version_directive_value(parser, start_mark, &major, &minor) { + return false + } + end_mark := parser.mark + + // Create a VERSION-DIRECTIVE token. + *token = yaml_token_t{ + typ: yaml_VERSION_DIRECTIVE_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + major: major, + minor: minor, + } + + // Is it a TAG directive? + } else if bytes.Equal(name, []byte("TAG")) { + // Scan the TAG directive value. + var handle, prefix []byte + if !yaml_parser_scan_tag_directive_value(parser, start_mark, &handle, &prefix) { + return false + } + end_mark := parser.mark + + // Create a TAG-DIRECTIVE token. + *token = yaml_token_t{ + typ: yaml_TAG_DIRECTIVE_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: handle, + prefix: prefix, + } + + // Unknown directive. + } else { + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "found unknown directive name") + return false + } + + // Eat the rest of the line including any comments. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + for is_blank(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + if parser.buffer[parser.buffer_pos] == '#' { + // [Go] Discard this inline comment for the time being. + //if !yaml_parser_scan_line_comment(parser, start_mark) { + // return false + //} + for !is_breakz(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + } + + // Check if we are at the end of the line. + if !is_breakz(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "did not find expected comment or line break") + return false + } + + // Eat a line break. + if is_break(parser.buffer, parser.buffer_pos) { + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + skip_line(parser) + } + + return true +} + +// Scan the directive name. +// +// Scope: +// +// %YAML 1.1 # a comment \n +// ^^^^ +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^ +func yaml_parser_scan_directive_name(parser *yaml_parser_t, start_mark yaml_mark_t, name *[]byte) bool { + // Consume the directive name. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + var s []byte + for is_alpha(parser.buffer, parser.buffer_pos) { + s = read(parser, s) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Check if the name is empty. + if len(s) == 0 { + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "could not find expected directive name") + return false + } + + // Check for an blank character after the name. + if !is_blankz(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "found unexpected non-alphabetical character") + return false + } + *name = s + return true +} + +// Scan the value of VERSION-DIRECTIVE. +// +// Scope: +// +// %YAML 1.1 # a comment \n +// ^^^^^^ +func yaml_parser_scan_version_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, major, minor *int8) bool { + // Eat whitespaces. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + for is_blank(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Consume the major version number. + if !yaml_parser_scan_version_directive_number(parser, start_mark, major) { + return false + } + + // Eat '.'. + if parser.buffer[parser.buffer_pos] != '.' { + return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive", + start_mark, "did not find expected digit or '.' character") + } + + skip(parser) + + // Consume the minor version number. + if !yaml_parser_scan_version_directive_number(parser, start_mark, minor) { + return false + } + return true +} + +const max_number_length = 2 + +// Scan the version number of VERSION-DIRECTIVE. +// +// Scope: +// +// %YAML 1.1 # a comment \n +// ^ +// %YAML 1.1 # a comment \n +// ^ +func yaml_parser_scan_version_directive_number(parser *yaml_parser_t, start_mark yaml_mark_t, number *int8) bool { + + // Repeat while the next character is digit. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + var value, length int8 + for is_digit(parser.buffer, parser.buffer_pos) { + // Check if the number is too long. + length++ + if length > max_number_length { + return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive", + start_mark, "found extremely long version number") + } + value = value*10 + int8(as_digit(parser.buffer, parser.buffer_pos)) + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Check if the number was present. + if length == 0 { + return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive", + start_mark, "did not find expected version number") + } + *number = value + return true +} + +// Scan the value of a TAG-DIRECTIVE token. +// +// Scope: +// +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +func yaml_parser_scan_tag_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, handle, prefix *[]byte) bool { + var handle_value, prefix_value []byte + + // Eat whitespaces. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + for is_blank(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Scan a handle. + if !yaml_parser_scan_tag_handle(parser, true, start_mark, &handle_value) { + return false + } + + // Expect a whitespace. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if !is_blank(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a %TAG directive", + start_mark, "did not find expected whitespace") + return false + } + + // Eat whitespaces. + for is_blank(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Scan a prefix. + if !yaml_parser_scan_tag_uri(parser, true, nil, start_mark, &prefix_value) { + return false + } + + // Expect a whitespace or line break. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if !is_blankz(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a %TAG directive", + start_mark, "did not find expected whitespace or line break") + return false + } + + *handle = handle_value + *prefix = prefix_value + return true +} + +func yaml_parser_scan_anchor(parser *yaml_parser_t, token *yaml_token_t, typ yaml_token_type_t) bool { + var s []byte + + // Eat the indicator character. + start_mark := parser.mark + skip(parser) + + // Consume the value. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + for is_alpha(parser.buffer, parser.buffer_pos) { + s = read(parser, s) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + end_mark := parser.mark + + /* + * Check if length of the anchor is greater than 0 and it is followed by + * a whitespace character or one of the indicators: + * + * '?', ':', ',', ']', '}', '%', '@', '`'. + */ + + if len(s) == 0 || + !(is_blankz(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == '?' || + parser.buffer[parser.buffer_pos] == ':' || parser.buffer[parser.buffer_pos] == ',' || + parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '}' || + parser.buffer[parser.buffer_pos] == '%' || parser.buffer[parser.buffer_pos] == '@' || + parser.buffer[parser.buffer_pos] == '`') { + context := "while scanning an alias" + if typ == yaml_ANCHOR_TOKEN { + context = "while scanning an anchor" + } + yaml_parser_set_scanner_error(parser, context, start_mark, + "did not find expected alphabetic or numeric character") + return false + } + + // Create a token. + *token = yaml_token_t{ + typ: typ, + start_mark: start_mark, + end_mark: end_mark, + value: s, + } + + return true +} + +/* + * Scan a TAG token. + */ + +func yaml_parser_scan_tag(parser *yaml_parser_t, token *yaml_token_t) bool { + var handle, suffix []byte + + start_mark := parser.mark + + // Check if the tag is in the canonical form. + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + + if parser.buffer[parser.buffer_pos+1] == '<' { + // Keep the handle as '' + + // Eat '!<' + skip(parser) + skip(parser) + + // Consume the tag value. + if !yaml_parser_scan_tag_uri(parser, false, nil, start_mark, &suffix) { + return false + } + + // Check for '>' and eat it. + if parser.buffer[parser.buffer_pos] != '>' { + yaml_parser_set_scanner_error(parser, "while scanning a tag", + start_mark, "did not find the expected '>'") + return false + } + + skip(parser) + } else { + // The tag has either the '!suffix' or the '!handle!suffix' form. + + // First, try to scan a handle. + if !yaml_parser_scan_tag_handle(parser, false, start_mark, &handle) { + return false + } + + // Check if it is, indeed, handle. + if handle[0] == '!' && len(handle) > 1 && handle[len(handle)-1] == '!' { + // Scan the suffix now. + if !yaml_parser_scan_tag_uri(parser, false, nil, start_mark, &suffix) { + return false + } + } else { + // It wasn't a handle after all. Scan the rest of the tag. + if !yaml_parser_scan_tag_uri(parser, false, handle, start_mark, &suffix) { + return false + } + + // Set the handle to '!'. + handle = []byte{'!'} + + // A special case: the '!' tag. Set the handle to '' and the + // suffix to '!'. + if len(suffix) == 0 { + handle, suffix = suffix, handle + } + } + } + + // Check the character which ends the tag. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if !is_blankz(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a tag", + start_mark, "did not find expected whitespace or line break") + return false + } + + end_mark := parser.mark + + // Create a token. + *token = yaml_token_t{ + typ: yaml_TAG_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: handle, + suffix: suffix, + } + return true +} + +// Scan a tag handle. +func yaml_parser_scan_tag_handle(parser *yaml_parser_t, directive bool, start_mark yaml_mark_t, handle *[]byte) bool { + // Check the initial '!' character. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if parser.buffer[parser.buffer_pos] != '!' { + yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find expected '!'") + return false + } + + var s []byte + + // Copy the '!' character. + s = read(parser, s) + + // Copy all subsequent alphabetical and numerical characters. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + for is_alpha(parser.buffer, parser.buffer_pos) { + s = read(parser, s) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Check if the trailing character is '!' and copy it. + if parser.buffer[parser.buffer_pos] == '!' { + s = read(parser, s) + } else { + // It's either the '!' tag or not really a tag handle. If it's a %TAG + // directive, it's an error. If it's a tag token, it must be a part of URI. + if directive && string(s) != "!" { + yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find expected '!'") + return false + } + } + + *handle = s + return true +} + +// Scan a tag. +func yaml_parser_scan_tag_uri(parser *yaml_parser_t, directive bool, head []byte, start_mark yaml_mark_t, uri *[]byte) bool { + //size_t length = head ? strlen((char *)head) : 0 + var s []byte + hasTag := len(head) > 0 + + // Copy the head if needed. + // + // Note that we don't copy the leading '!' character. + if len(head) > 1 { + s = append(s, head[1:]...) + } + + // Scan the tag. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + // The set of characters that may appear in URI is as follows: + // + // '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&', + // '=', '+', '$', ',', '.', '!', '~', '*', '\'', '(', ')', '[', ']', + // '%'. + // [Go] TODO Convert this into more reasonable logic. + for is_alpha(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == ';' || + parser.buffer[parser.buffer_pos] == '/' || parser.buffer[parser.buffer_pos] == '?' || + parser.buffer[parser.buffer_pos] == ':' || parser.buffer[parser.buffer_pos] == '@' || + parser.buffer[parser.buffer_pos] == '&' || parser.buffer[parser.buffer_pos] == '=' || + parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '$' || + parser.buffer[parser.buffer_pos] == ',' || parser.buffer[parser.buffer_pos] == '.' || + parser.buffer[parser.buffer_pos] == '!' || parser.buffer[parser.buffer_pos] == '~' || + parser.buffer[parser.buffer_pos] == '*' || parser.buffer[parser.buffer_pos] == '\'' || + parser.buffer[parser.buffer_pos] == '(' || parser.buffer[parser.buffer_pos] == ')' || + parser.buffer[parser.buffer_pos] == '[' || parser.buffer[parser.buffer_pos] == ']' || + parser.buffer[parser.buffer_pos] == '%' { + // Check if it is a URI-escape sequence. + if parser.buffer[parser.buffer_pos] == '%' { + if !yaml_parser_scan_uri_escapes(parser, directive, start_mark, &s) { + return false + } + } else { + s = read(parser, s) + } + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + hasTag = true + } + + if !hasTag { + yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find expected tag URI") + return false + } + *uri = s + return true +} + +// Decode an URI-escape sequence corresponding to a single UTF-8 character. +func yaml_parser_scan_uri_escapes(parser *yaml_parser_t, directive bool, start_mark yaml_mark_t, s *[]byte) bool { + + // Decode the required number of characters. + w := 1024 + for w > 0 { + // Check for a URI-escaped octet. + if parser.unread < 3 && !yaml_parser_update_buffer(parser, 3) { + return false + } + + if !(parser.buffer[parser.buffer_pos] == '%' && + is_hex(parser.buffer, parser.buffer_pos+1) && + is_hex(parser.buffer, parser.buffer_pos+2)) { + return yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find URI escaped octet") + } + + // Get the octet. + octet := byte((as_hex(parser.buffer, parser.buffer_pos+1) << 4) + as_hex(parser.buffer, parser.buffer_pos+2)) + + // If it is the leading octet, determine the length of the UTF-8 sequence. + if w == 1024 { + w = width(octet) + if w == 0 { + return yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "found an incorrect leading UTF-8 octet") + } + } else { + // Check if the trailing octet is correct. + if octet&0xC0 != 0x80 { + return yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "found an incorrect trailing UTF-8 octet") + } + } + + // Copy the octet and move the pointers. + *s = append(*s, octet) + skip(parser) + skip(parser) + skip(parser) + w-- + } + return true +} + +// Scan a block scalar. +func yaml_parser_scan_block_scalar(parser *yaml_parser_t, token *yaml_token_t, literal bool) bool { + // Eat the indicator '|' or '>'. + start_mark := parser.mark + skip(parser) + + // Scan the additional block scalar indicators. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + // Check for a chomping indicator. + var chomping, increment int + if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { + // Set the chomping method and eat the indicator. + if parser.buffer[parser.buffer_pos] == '+' { + chomping = +1 + } else { + chomping = -1 + } + skip(parser) + + // Check for an indentation indicator. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if is_digit(parser.buffer, parser.buffer_pos) { + // Check that the indentation is greater than 0. + if parser.buffer[parser.buffer_pos] == '0' { + yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "found an indentation indicator equal to 0") + return false + } + + // Get the indentation level and eat the indicator. + increment = as_digit(parser.buffer, parser.buffer_pos) + skip(parser) + } + + } else if is_digit(parser.buffer, parser.buffer_pos) { + // Do the same as above, but in the opposite order. + + if parser.buffer[parser.buffer_pos] == '0' { + yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "found an indentation indicator equal to 0") + return false + } + increment = as_digit(parser.buffer, parser.buffer_pos) + skip(parser) + + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { + if parser.buffer[parser.buffer_pos] == '+' { + chomping = +1 + } else { + chomping = -1 + } + skip(parser) + } + } + + // Eat whitespaces and comments to the end of the line. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + for is_blank(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + if parser.buffer[parser.buffer_pos] == '#' { + if !yaml_parser_scan_line_comment(parser, start_mark) { + return false + } + for !is_breakz(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + } + + // Check if we are at the end of the line. + if !is_breakz(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "did not find expected comment or line break") + return false + } + + // Eat a line break. + if is_break(parser.buffer, parser.buffer_pos) { + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + skip_line(parser) + } + + end_mark := parser.mark + + // Set the indentation level if it was specified. + var indent int + if increment > 0 { + if parser.indent >= 0 { + indent = parser.indent + increment + } else { + indent = increment + } + } + + // Scan the leading line breaks and determine the indentation level if needed. + var s, leading_break, trailing_breaks []byte + if !yaml_parser_scan_block_scalar_breaks(parser, &indent, &trailing_breaks, start_mark, &end_mark) { + return false + } + + // Scan the block scalar content. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + var leading_blank, trailing_blank bool + for parser.mark.column == indent && !is_z(parser.buffer, parser.buffer_pos) { + // We are at the beginning of a non-empty line. + + // Is it a trailing whitespace? + trailing_blank = is_blank(parser.buffer, parser.buffer_pos) + + // Check if we need to fold the leading line break. + if !literal && !leading_blank && !trailing_blank && len(leading_break) > 0 && leading_break[0] == '\n' { + // Do we need to join the lines by space? + if len(trailing_breaks) == 0 { + s = append(s, ' ') + } + } else { + s = append(s, leading_break...) + } + leading_break = leading_break[:0] + + // Append the remaining line breaks. + s = append(s, trailing_breaks...) + trailing_breaks = trailing_breaks[:0] + + // Is it a leading whitespace? + leading_blank = is_blank(parser.buffer, parser.buffer_pos) + + // Consume the current line. + for !is_breakz(parser.buffer, parser.buffer_pos) { + s = read(parser, s) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Consume the line break. + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + + leading_break = read_line(parser, leading_break) + + // Eat the following indentation spaces and line breaks. + if !yaml_parser_scan_block_scalar_breaks(parser, &indent, &trailing_breaks, start_mark, &end_mark) { + return false + } + } + + // Chomp the tail. + if chomping != -1 { + s = append(s, leading_break...) + } + if chomping == 1 { + s = append(s, trailing_breaks...) + } + + // Create a token. + *token = yaml_token_t{ + typ: yaml_SCALAR_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: s, + style: yaml_LITERAL_SCALAR_STYLE, + } + if !literal { + token.style = yaml_FOLDED_SCALAR_STYLE + } + return true +} + +// Scan indentation spaces and line breaks for a block scalar. Determine the +// indentation level if needed. +func yaml_parser_scan_block_scalar_breaks(parser *yaml_parser_t, indent *int, breaks *[]byte, start_mark yaml_mark_t, end_mark *yaml_mark_t) bool { + *end_mark = parser.mark + + // Eat the indentation spaces and line breaks. + max_indent := 0 + for { + // Eat the indentation spaces. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + for (*indent == 0 || parser.mark.column < *indent) && is_space(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + if parser.mark.column > max_indent { + max_indent = parser.mark.column + } + + // Check for a tab character messing the indentation. + if (*indent == 0 || parser.mark.column < *indent) && is_tab(parser.buffer, parser.buffer_pos) { + return yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "found a tab character where an indentation space is expected") + } + + // Have we found a non-empty line? + if !is_break(parser.buffer, parser.buffer_pos) { + break + } + + // Consume the line break. + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + // [Go] Should really be returning breaks instead. + *breaks = read_line(parser, *breaks) + *end_mark = parser.mark + } + + // Determine the indentation level if needed. + if *indent == 0 { + *indent = max_indent + if *indent < parser.indent+1 { + *indent = parser.indent + 1 + } + if *indent < 1 { + *indent = 1 + } + } + return true +} + +// Scan a quoted scalar. +func yaml_parser_scan_flow_scalar(parser *yaml_parser_t, token *yaml_token_t, single bool) bool { + // Eat the left quote. + start_mark := parser.mark + skip(parser) + + // Consume the content of the quoted scalar. + var s, leading_break, trailing_breaks, whitespaces []byte + for { + // Check that there are no document indicators at the beginning of the line. + if parser.unread < 4 && !yaml_parser_update_buffer(parser, 4) { + return false + } + + if parser.mark.column == 0 && + ((parser.buffer[parser.buffer_pos+0] == '-' && + parser.buffer[parser.buffer_pos+1] == '-' && + parser.buffer[parser.buffer_pos+2] == '-') || + (parser.buffer[parser.buffer_pos+0] == '.' && + parser.buffer[parser.buffer_pos+1] == '.' && + parser.buffer[parser.buffer_pos+2] == '.')) && + is_blankz(parser.buffer, parser.buffer_pos+3) { + yaml_parser_set_scanner_error(parser, "while scanning a quoted scalar", + start_mark, "found unexpected document indicator") + return false + } + + // Check for EOF. + if is_z(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a quoted scalar", + start_mark, "found unexpected end of stream") + return false + } + + // Consume non-blank characters. + leading_blanks := false + for !is_blankz(parser.buffer, parser.buffer_pos) { + if single && parser.buffer[parser.buffer_pos] == '\'' && parser.buffer[parser.buffer_pos+1] == '\'' { + // Is is an escaped single quote. + s = append(s, '\'') + skip(parser) + skip(parser) + + } else if single && parser.buffer[parser.buffer_pos] == '\'' { + // It is a right single quote. + break + } else if !single && parser.buffer[parser.buffer_pos] == '"' { + // It is a right double quote. + break + + } else if !single && parser.buffer[parser.buffer_pos] == '\\' && is_break(parser.buffer, parser.buffer_pos+1) { + // It is an escaped line break. + if parser.unread < 3 && !yaml_parser_update_buffer(parser, 3) { + return false + } + skip(parser) + skip_line(parser) + leading_blanks = true + break + + } else if !single && parser.buffer[parser.buffer_pos] == '\\' { + // It is an escape sequence. + code_length := 0 + + // Check the escape character. + switch parser.buffer[parser.buffer_pos+1] { + case '0': + s = append(s, 0) + case 'a': + s = append(s, '\x07') + case 'b': + s = append(s, '\x08') + case 't', '\t': + s = append(s, '\x09') + case 'n': + s = append(s, '\x0A') + case 'v': + s = append(s, '\x0B') + case 'f': + s = append(s, '\x0C') + case 'r': + s = append(s, '\x0D') + case 'e': + s = append(s, '\x1B') + case ' ': + s = append(s, '\x20') + case '"': + s = append(s, '"') + case '\'': + s = append(s, '\'') + case '\\': + s = append(s, '\\') + case 'N': // NEL (#x85) + s = append(s, '\xC2') + s = append(s, '\x85') + case '_': // #xA0 + s = append(s, '\xC2') + s = append(s, '\xA0') + case 'L': // LS (#x2028) + s = append(s, '\xE2') + s = append(s, '\x80') + s = append(s, '\xA8') + case 'P': // PS (#x2029) + s = append(s, '\xE2') + s = append(s, '\x80') + s = append(s, '\xA9') + case 'x': + code_length = 2 + case 'u': + code_length = 4 + case 'U': + code_length = 8 + default: + yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar", + start_mark, "found unknown escape character") + return false + } + + skip(parser) + skip(parser) + + // Consume an arbitrary escape code. + if code_length > 0 { + var value int + + // Scan the character value. + if parser.unread < code_length && !yaml_parser_update_buffer(parser, code_length) { + return false + } + for k := 0; k < code_length; k++ { + if !is_hex(parser.buffer, parser.buffer_pos+k) { + yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar", + start_mark, "did not find expected hexdecimal number") + return false + } + value = (value << 4) + as_hex(parser.buffer, parser.buffer_pos+k) + } + + // Check the value and write the character. + if (value >= 0xD800 && value <= 0xDFFF) || value > 0x10FFFF { + yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar", + start_mark, "found invalid Unicode character escape code") + return false + } + if value <= 0x7F { + s = append(s, byte(value)) + } else if value <= 0x7FF { + s = append(s, byte(0xC0+(value>>6))) + s = append(s, byte(0x80+(value&0x3F))) + } else if value <= 0xFFFF { + s = append(s, byte(0xE0+(value>>12))) + s = append(s, byte(0x80+((value>>6)&0x3F))) + s = append(s, byte(0x80+(value&0x3F))) + } else { + s = append(s, byte(0xF0+(value>>18))) + s = append(s, byte(0x80+((value>>12)&0x3F))) + s = append(s, byte(0x80+((value>>6)&0x3F))) + s = append(s, byte(0x80+(value&0x3F))) + } + + // Advance the pointer. + for k := 0; k < code_length; k++ { + skip(parser) + } + } + } else { + // It is a non-escaped non-blank character. + s = read(parser, s) + } + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + } + + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + // Check if we are at the end of the scalar. + if single { + if parser.buffer[parser.buffer_pos] == '\'' { + break + } + } else { + if parser.buffer[parser.buffer_pos] == '"' { + break + } + } + + // Consume blank characters. + for is_blank(parser.buffer, parser.buffer_pos) || is_break(parser.buffer, parser.buffer_pos) { + if is_blank(parser.buffer, parser.buffer_pos) { + // Consume a space or a tab character. + if !leading_blanks { + whitespaces = read(parser, whitespaces) + } else { + skip(parser) + } + } else { + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + + // Check if it is a first line break. + if !leading_blanks { + whitespaces = whitespaces[:0] + leading_break = read_line(parser, leading_break) + leading_blanks = true + } else { + trailing_breaks = read_line(parser, trailing_breaks) + } + } + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Join the whitespaces or fold line breaks. + if leading_blanks { + // Do we need to fold line breaks? + if len(leading_break) > 0 && leading_break[0] == '\n' { + if len(trailing_breaks) == 0 { + s = append(s, ' ') + } else { + s = append(s, trailing_breaks...) + } + } else { + s = append(s, leading_break...) + s = append(s, trailing_breaks...) + } + trailing_breaks = trailing_breaks[:0] + leading_break = leading_break[:0] + } else { + s = append(s, whitespaces...) + whitespaces = whitespaces[:0] + } + } + + // Eat the right quote. + skip(parser) + end_mark := parser.mark + + // Create a token. + *token = yaml_token_t{ + typ: yaml_SCALAR_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: s, + style: yaml_SINGLE_QUOTED_SCALAR_STYLE, + } + if !single { + token.style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + return true +} + +// Scan a plain scalar. +func yaml_parser_scan_plain_scalar(parser *yaml_parser_t, token *yaml_token_t) bool { + + var s, leading_break, trailing_breaks, whitespaces []byte + var leading_blanks bool + var indent = parser.indent + 1 + + start_mark := parser.mark + end_mark := parser.mark + + // Consume the content of the plain scalar. + for { + // Check for a document indicator. + if parser.unread < 4 && !yaml_parser_update_buffer(parser, 4) { + return false + } + if parser.mark.column == 0 && + ((parser.buffer[parser.buffer_pos+0] == '-' && + parser.buffer[parser.buffer_pos+1] == '-' && + parser.buffer[parser.buffer_pos+2] == '-') || + (parser.buffer[parser.buffer_pos+0] == '.' && + parser.buffer[parser.buffer_pos+1] == '.' && + parser.buffer[parser.buffer_pos+2] == '.')) && + is_blankz(parser.buffer, parser.buffer_pos+3) { + break + } + + // Check for a comment. + if parser.buffer[parser.buffer_pos] == '#' { + break + } + + // Consume non-blank characters. + for !is_blankz(parser.buffer, parser.buffer_pos) { + + // Check for indicators that may end a plain scalar. + if (parser.buffer[parser.buffer_pos] == ':' && is_blankz(parser.buffer, parser.buffer_pos+1)) || + (parser.flow_level > 0 && + (parser.buffer[parser.buffer_pos] == ',' || + parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == '[' || + parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '{' || + parser.buffer[parser.buffer_pos] == '}')) { + break + } + + // Check if we need to join whitespaces and breaks. + if leading_blanks || len(whitespaces) > 0 { + if leading_blanks { + // Do we need to fold line breaks? + if leading_break[0] == '\n' { + if len(trailing_breaks) == 0 { + s = append(s, ' ') + } else { + s = append(s, trailing_breaks...) + } + } else { + s = append(s, leading_break...) + s = append(s, trailing_breaks...) + } + trailing_breaks = trailing_breaks[:0] + leading_break = leading_break[:0] + leading_blanks = false + } else { + s = append(s, whitespaces...) + whitespaces = whitespaces[:0] + } + } + + // Copy the character. + s = read(parser, s) + + end_mark = parser.mark + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + } + + // Is it the end? + if !(is_blank(parser.buffer, parser.buffer_pos) || is_break(parser.buffer, parser.buffer_pos)) { + break + } + + // Consume blank characters. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + for is_blank(parser.buffer, parser.buffer_pos) || is_break(parser.buffer, parser.buffer_pos) { + if is_blank(parser.buffer, parser.buffer_pos) { + + // Check for tab characters that abuse indentation. + if leading_blanks && parser.mark.column < indent && is_tab(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a plain scalar", + start_mark, "found a tab character that violates indentation") + return false + } + + // Consume a space or a tab character. + if !leading_blanks { + whitespaces = read(parser, whitespaces) + } else { + skip(parser) + } + } else { + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + + // Check if it is a first line break. + if !leading_blanks { + whitespaces = whitespaces[:0] + leading_break = read_line(parser, leading_break) + leading_blanks = true + } else { + trailing_breaks = read_line(parser, trailing_breaks) + } + } + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Check indentation level. + if parser.flow_level == 0 && parser.mark.column < indent { + break + } + } + + // Create a token. + *token = yaml_token_t{ + typ: yaml_SCALAR_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: s, + style: yaml_PLAIN_SCALAR_STYLE, + } + + // Note that we change the 'simple_key_allowed' flag. + if leading_blanks { + parser.simple_key_allowed = true + } + return true +} + +func yaml_parser_scan_line_comment(parser *yaml_parser_t, token_mark yaml_mark_t) bool { + if parser.newlines > 0 { + return true + } + + var start_mark yaml_mark_t + var text []byte + + for peek := 0; peek < 512; peek++ { + if parser.unread < peek+1 && !yaml_parser_update_buffer(parser, peek+1) { + break + } + if is_blank(parser.buffer, parser.buffer_pos+peek) { + continue + } + if parser.buffer[parser.buffer_pos+peek] == '#' { + seen := parser.mark.index + peek + for { + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if is_breakz(parser.buffer, parser.buffer_pos) { + if parser.mark.index >= seen { + break + } + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + skip_line(parser) + } else if parser.mark.index >= seen { + if len(text) == 0 { + start_mark = parser.mark + } + text = read(parser, text) + } else { + skip(parser) + } + } + } + break + } + if len(text) > 0 { + parser.comments = append(parser.comments, yaml_comment_t{ + token_mark: token_mark, + start_mark: start_mark, + line: text, + }) + } + return true +} + +func yaml_parser_scan_comments(parser *yaml_parser_t, scan_mark yaml_mark_t) bool { + token := parser.tokens[len(parser.tokens)-1] + + if token.typ == yaml_FLOW_ENTRY_TOKEN && len(parser.tokens) > 1 { + token = parser.tokens[len(parser.tokens)-2] + } + + var token_mark = token.start_mark + var start_mark yaml_mark_t + var next_indent = parser.indent + if next_indent < 0 { + next_indent = 0 + } + + var recent_empty = false + var first_empty = parser.newlines <= 1 + + var line = parser.mark.line + var column = parser.mark.column + + var text []byte + + // The foot line is the place where a comment must start to + // still be considered as a foot of the prior content. + // If there's some content in the currently parsed line, then + // the foot is the line below it. + var foot_line = -1 + if scan_mark.line > 0 { + foot_line = parser.mark.line - parser.newlines + 1 + if parser.newlines == 0 && parser.mark.column > 1 { + foot_line++ + } + } + + var peek = 0 + for ; peek < 512; peek++ { + if parser.unread < peek+1 && !yaml_parser_update_buffer(parser, peek+1) { + break + } + column++ + if is_blank(parser.buffer, parser.buffer_pos+peek) { + continue + } + c := parser.buffer[parser.buffer_pos+peek] + var close_flow = parser.flow_level > 0 && (c == ']' || c == '}') + if close_flow || is_breakz(parser.buffer, parser.buffer_pos+peek) { + // Got line break or terminator. + if close_flow || !recent_empty { + if close_flow || first_empty && (start_mark.line == foot_line && token.typ != yaml_VALUE_TOKEN || start_mark.column-1 < next_indent) { + // This is the first empty line and there were no empty lines before, + // so this initial part of the comment is a foot of the prior token + // instead of being a head for the following one. Split it up. + // Alternatively, this might also be the last comment inside a flow + // scope, so it must be a footer. + if len(text) > 0 { + if start_mark.column-1 < next_indent { + // If dedented it's unrelated to the prior token. + token_mark = start_mark + } + parser.comments = append(parser.comments, yaml_comment_t{ + scan_mark: scan_mark, + token_mark: token_mark, + start_mark: start_mark, + end_mark: yaml_mark_t{parser.mark.index + peek, line, column}, + foot: text, + }) + scan_mark = yaml_mark_t{parser.mark.index + peek, line, column} + token_mark = scan_mark + text = nil + } + } else { + if len(text) > 0 && parser.buffer[parser.buffer_pos+peek] != 0 { + text = append(text, '\n') + } + } + } + if !is_break(parser.buffer, parser.buffer_pos+peek) { + break + } + first_empty = false + recent_empty = true + column = 0 + line++ + continue + } + + if len(text) > 0 && (close_flow || column-1 < next_indent && column != start_mark.column) { + // The comment at the different indentation is a foot of the + // preceding data rather than a head of the upcoming one. + parser.comments = append(parser.comments, yaml_comment_t{ + scan_mark: scan_mark, + token_mark: token_mark, + start_mark: start_mark, + end_mark: yaml_mark_t{parser.mark.index + peek, line, column}, + foot: text, + }) + scan_mark = yaml_mark_t{parser.mark.index + peek, line, column} + token_mark = scan_mark + text = nil + } + + if parser.buffer[parser.buffer_pos+peek] != '#' { + break + } + + if len(text) == 0 { + start_mark = yaml_mark_t{parser.mark.index + peek, line, column} + } else { + text = append(text, '\n') + } + + recent_empty = false + + // Consume until after the consumed comment line. + seen := parser.mark.index + peek + for { + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if is_breakz(parser.buffer, parser.buffer_pos) { + if parser.mark.index >= seen { + break + } + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + skip_line(parser) + } else if parser.mark.index >= seen { + text = read(parser, text) + } else { + skip(parser) + } + } + + peek = 0 + column = 0 + line = parser.mark.line + next_indent = parser.indent + if next_indent < 0 { + next_indent = 0 + } + } + + if len(text) > 0 { + parser.comments = append(parser.comments, yaml_comment_t{ + scan_mark: scan_mark, + token_mark: start_mark, + start_mark: start_mark, + end_mark: yaml_mark_t{parser.mark.index + peek - 1, line, column}, + head: text, + }) + } + return true +} diff --git a/vendor/go.yaml.in/yaml/v3/sorter.go b/vendor/go.yaml.in/yaml/v3/sorter.go new file mode 100644 index 000000000..9210ece7e --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/sorter.go @@ -0,0 +1,134 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package yaml + +import ( + "reflect" + "unicode" +) + +type keyList []reflect.Value + +func (l keyList) Len() int { return len(l) } +func (l keyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } +func (l keyList) Less(i, j int) bool { + a := l[i] + b := l[j] + ak := a.Kind() + bk := b.Kind() + for (ak == reflect.Interface || ak == reflect.Ptr) && !a.IsNil() { + a = a.Elem() + ak = a.Kind() + } + for (bk == reflect.Interface || bk == reflect.Ptr) && !b.IsNil() { + b = b.Elem() + bk = b.Kind() + } + af, aok := keyFloat(a) + bf, bok := keyFloat(b) + if aok && bok { + if af != bf { + return af < bf + } + if ak != bk { + return ak < bk + } + return numLess(a, b) + } + if ak != reflect.String || bk != reflect.String { + return ak < bk + } + ar, br := []rune(a.String()), []rune(b.String()) + digits := false + for i := 0; i < len(ar) && i < len(br); i++ { + if ar[i] == br[i] { + digits = unicode.IsDigit(ar[i]) + continue + } + al := unicode.IsLetter(ar[i]) + bl := unicode.IsLetter(br[i]) + if al && bl { + return ar[i] < br[i] + } + if al || bl { + if digits { + return al + } else { + return bl + } + } + var ai, bi int + var an, bn int64 + if ar[i] == '0' || br[i] == '0' { + for j := i - 1; j >= 0 && unicode.IsDigit(ar[j]); j-- { + if ar[j] != '0' { + an = 1 + bn = 1 + break + } + } + } + for ai = i; ai < len(ar) && unicode.IsDigit(ar[ai]); ai++ { + an = an*10 + int64(ar[ai]-'0') + } + for bi = i; bi < len(br) && unicode.IsDigit(br[bi]); bi++ { + bn = bn*10 + int64(br[bi]-'0') + } + if an != bn { + return an < bn + } + if ai != bi { + return ai < bi + } + return ar[i] < br[i] + } + return len(ar) < len(br) +} + +// keyFloat returns a float value for v if it is a number/bool +// and whether it is a number/bool or not. +func keyFloat(v reflect.Value) (f float64, ok bool) { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return float64(v.Int()), true + case reflect.Float32, reflect.Float64: + return v.Float(), true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return float64(v.Uint()), true + case reflect.Bool: + if v.Bool() { + return 1, true + } + return 0, true + } + return 0, false +} + +// numLess returns whether a < b. +// a and b must necessarily have the same kind. +func numLess(a, b reflect.Value) bool { + switch a.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return a.Int() < b.Int() + case reflect.Float32, reflect.Float64: + return a.Float() < b.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return a.Uint() < b.Uint() + case reflect.Bool: + return !a.Bool() && b.Bool() + } + panic("not a number") +} diff --git a/vendor/go.yaml.in/yaml/v3/writerc.go b/vendor/go.yaml.in/yaml/v3/writerc.go new file mode 100644 index 000000000..266d0b092 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/writerc.go @@ -0,0 +1,48 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// 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. + +package yaml + +// Set the writer error and return false. +func yaml_emitter_set_writer_error(emitter *yaml_emitter_t, problem string) bool { + emitter.error = yaml_WRITER_ERROR + emitter.problem = problem + return false +} + +// Flush the output buffer. +func yaml_emitter_flush(emitter *yaml_emitter_t) bool { + if emitter.write_handler == nil { + panic("write handler not set") + } + + // Check if the buffer is empty. + if emitter.buffer_pos == 0 { + return true + } + + if err := emitter.write_handler(emitter, emitter.buffer[:emitter.buffer_pos]); err != nil { + return yaml_emitter_set_writer_error(emitter, "write error: "+err.Error()) + } + emitter.buffer_pos = 0 + return true +} diff --git a/vendor/go.yaml.in/yaml/v3/yaml.go b/vendor/go.yaml.in/yaml/v3/yaml.go new file mode 100644 index 000000000..0b101cd20 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/yaml.go @@ -0,0 +1,703 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package yaml implements YAML support for the Go language. +// +// Source code and other details for the project are available at GitHub: +// +// https://github.com/yaml/go-yaml +package yaml + +import ( + "errors" + "fmt" + "io" + "reflect" + "strings" + "sync" + "unicode/utf8" +) + +// The Unmarshaler interface may be implemented by types to customize their +// behavior when being unmarshaled from a YAML document. +type Unmarshaler interface { + UnmarshalYAML(value *Node) error +} + +type obsoleteUnmarshaler interface { + UnmarshalYAML(unmarshal func(interface{}) error) error +} + +// The Marshaler interface may be implemented by types to customize their +// behavior when being marshaled into a YAML document. The returned value +// is marshaled in place of the original value implementing Marshaler. +// +// If an error is returned by MarshalYAML, the marshaling procedure stops +// and returns with the provided error. +type Marshaler interface { + MarshalYAML() (interface{}, error) +} + +// Unmarshal decodes the first document found within the in byte slice +// and assigns decoded values into the out value. +// +// Maps and pointers (to a struct, string, int, etc) are accepted as out +// values. If an internal pointer within a struct is not initialized, +// the yaml package will initialize it if necessary for unmarshalling +// the provided data. The out parameter must not be nil. +// +// The type of the decoded values should be compatible with the respective +// values in out. If one or more values cannot be decoded due to a type +// mismatches, decoding continues partially until the end of the YAML +// content, and a *yaml.TypeError is returned with details for all +// missed values. +// +// Struct fields are only unmarshalled if they are exported (have an +// upper case first letter), and are unmarshalled using the field name +// lowercased as the default key. Custom keys may be defined via the +// "yaml" name in the field tag: the content preceding the first comma +// is used as the key, and the following comma-separated options are +// used to tweak the marshalling process (see Marshal). +// Conflicting names result in a runtime error. +// +// For example: +// +// type T struct { +// F int `yaml:"a,omitempty"` +// B int +// } +// var t T +// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t) +// +// See the documentation of Marshal for the format of tags and a list of +// supported tag options. +func Unmarshal(in []byte, out interface{}) (err error) { + return unmarshal(in, out, false) +} + +// A Decoder reads and decodes YAML values from an input stream. +type Decoder struct { + parser *parser + knownFields bool +} + +// NewDecoder returns a new decoder that reads from r. +// +// The decoder introduces its own buffering and may read +// data from r beyond the YAML values requested. +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{ + parser: newParserFromReader(r), + } +} + +// KnownFields ensures that the keys in decoded mappings to +// exist as fields in the struct being decoded into. +func (dec *Decoder) KnownFields(enable bool) { + dec.knownFields = enable +} + +// Decode reads the next YAML-encoded value from its input +// and stores it in the value pointed to by v. +// +// See the documentation for Unmarshal for details about the +// conversion of YAML into a Go value. +func (dec *Decoder) Decode(v interface{}) (err error) { + d := newDecoder() + d.knownFields = dec.knownFields + defer handleErr(&err) + node := dec.parser.parse() + if node == nil { + return io.EOF + } + out := reflect.ValueOf(v) + if out.Kind() == reflect.Ptr && !out.IsNil() { + out = out.Elem() + } + d.unmarshal(node, out) + if len(d.terrors) > 0 { + return &TypeError{d.terrors} + } + return nil +} + +// Decode decodes the node and stores its data into the value pointed to by v. +// +// See the documentation for Unmarshal for details about the +// conversion of YAML into a Go value. +func (n *Node) Decode(v interface{}) (err error) { + d := newDecoder() + defer handleErr(&err) + out := reflect.ValueOf(v) + if out.Kind() == reflect.Ptr && !out.IsNil() { + out = out.Elem() + } + d.unmarshal(n, out) + if len(d.terrors) > 0 { + return &TypeError{d.terrors} + } + return nil +} + +func unmarshal(in []byte, out interface{}, strict bool) (err error) { + defer handleErr(&err) + d := newDecoder() + p := newParser(in) + defer p.destroy() + node := p.parse() + if node != nil { + v := reflect.ValueOf(out) + if v.Kind() == reflect.Ptr && !v.IsNil() { + v = v.Elem() + } + d.unmarshal(node, v) + } + if len(d.terrors) > 0 { + return &TypeError{d.terrors} + } + return nil +} + +// Marshal serializes the value provided into a YAML document. The structure +// of the generated document will reflect the structure of the value itself. +// Maps and pointers (to struct, string, int, etc) are accepted as the in value. +// +// Struct fields are only marshalled if they are exported (have an upper case +// first letter), and are marshalled using the field name lowercased as the +// default key. Custom keys may be defined via the "yaml" name in the field +// tag: the content preceding the first comma is used as the key, and the +// following comma-separated options are used to tweak the marshalling process. +// Conflicting names result in a runtime error. +// +// The field tag format accepted is: +// +// `(...) yaml:"[][,[,]]" (...)` +// +// The following flags are currently supported: +// +// omitempty Only include the field if it's not set to the zero +// value for the type or to empty slices or maps. +// Zero valued structs will be omitted if all their public +// fields are zero, unless they implement an IsZero +// method (see the IsZeroer interface type), in which +// case the field will be excluded if IsZero returns true. +// +// flow Marshal using a flow style (useful for structs, +// sequences and maps). +// +// inline Inline the field, which must be a struct or a map, +// causing all of its fields or keys to be processed as if +// they were part of the outer struct. For maps, keys must +// not conflict with the yaml keys of other struct fields. +// +// In addition, if the key is "-", the field is ignored. +// +// For example: +// +// type T struct { +// F int `yaml:"a,omitempty"` +// B int +// } +// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n" +// yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n" +func Marshal(in interface{}) (out []byte, err error) { + defer handleErr(&err) + e := newEncoder() + defer e.destroy() + e.marshalDoc("", reflect.ValueOf(in)) + e.finish() + out = e.out + return +} + +// An Encoder writes YAML values to an output stream. +type Encoder struct { + encoder *encoder +} + +// NewEncoder returns a new encoder that writes to w. +// The Encoder should be closed after use to flush all data +// to w. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{ + encoder: newEncoderWithWriter(w), + } +} + +// Encode writes the YAML encoding of v to the stream. +// If multiple items are encoded to the stream, the +// second and subsequent document will be preceded +// with a "---" document separator, but the first will not. +// +// See the documentation for Marshal for details about the conversion of Go +// values to YAML. +func (e *Encoder) Encode(v interface{}) (err error) { + defer handleErr(&err) + e.encoder.marshalDoc("", reflect.ValueOf(v)) + return nil +} + +// Encode encodes value v and stores its representation in n. +// +// See the documentation for Marshal for details about the +// conversion of Go values into YAML. +func (n *Node) Encode(v interface{}) (err error) { + defer handleErr(&err) + e := newEncoder() + defer e.destroy() + e.marshalDoc("", reflect.ValueOf(v)) + e.finish() + p := newParser(e.out) + p.textless = true + defer p.destroy() + doc := p.parse() + *n = *doc.Content[0] + return nil +} + +// SetIndent changes the used indentation used when encoding. +func (e *Encoder) SetIndent(spaces int) { + if spaces < 0 { + panic("yaml: cannot indent to a negative number of spaces") + } + e.encoder.indent = spaces +} + +// CompactSeqIndent makes it so that '- ' is considered part of the indentation. +func (e *Encoder) CompactSeqIndent() { + e.encoder.emitter.compact_sequence_indent = true +} + +// DefaultSeqIndent makes it so that '- ' is not considered part of the indentation. +func (e *Encoder) DefaultSeqIndent() { + e.encoder.emitter.compact_sequence_indent = false +} + +// Close closes the encoder by writing any remaining data. +// It does not write a stream terminating string "...". +func (e *Encoder) Close() (err error) { + defer handleErr(&err) + e.encoder.finish() + return nil +} + +func handleErr(err *error) { + if v := recover(); v != nil { + if e, ok := v.(yamlError); ok { + *err = e.err + } else { + panic(v) + } + } +} + +type yamlError struct { + err error +} + +func fail(err error) { + panic(yamlError{err}) +} + +func failf(format string, args ...interface{}) { + panic(yamlError{fmt.Errorf("yaml: "+format, args...)}) +} + +// A TypeError is returned by Unmarshal when one or more fields in +// the YAML document cannot be properly decoded into the requested +// types. When this error is returned, the value is still +// unmarshaled partially. +type TypeError struct { + Errors []string +} + +func (e *TypeError) Error() string { + return fmt.Sprintf("yaml: unmarshal errors:\n %s", strings.Join(e.Errors, "\n ")) +} + +type Kind uint32 + +const ( + DocumentNode Kind = 1 << iota + SequenceNode + MappingNode + ScalarNode + AliasNode +) + +type Style uint32 + +const ( + TaggedStyle Style = 1 << iota + DoubleQuotedStyle + SingleQuotedStyle + LiteralStyle + FoldedStyle + FlowStyle +) + +// Node represents an element in the YAML document hierarchy. While documents +// are typically encoded and decoded into higher level types, such as structs +// and maps, Node is an intermediate representation that allows detailed +// control over the content being decoded or encoded. +// +// It's worth noting that although Node offers access into details such as +// line numbers, colums, and comments, the content when re-encoded will not +// have its original textual representation preserved. An effort is made to +// render the data plesantly, and to preserve comments near the data they +// describe, though. +// +// Values that make use of the Node type interact with the yaml package in the +// same way any other type would do, by encoding and decoding yaml data +// directly or indirectly into them. +// +// For example: +// +// var person struct { +// Name string +// Address yaml.Node +// } +// err := yaml.Unmarshal(data, &person) +// +// Or by itself: +// +// var person Node +// err := yaml.Unmarshal(data, &person) +type Node struct { + // Kind defines whether the node is a document, a mapping, a sequence, + // a scalar value, or an alias to another node. The specific data type of + // scalar nodes may be obtained via the ShortTag and LongTag methods. + Kind Kind + + // Style allows customizing the apperance of the node in the tree. + Style Style + + // Tag holds the YAML tag defining the data type for the value. + // When decoding, this field will always be set to the resolved tag, + // even when it wasn't explicitly provided in the YAML content. + // When encoding, if this field is unset the value type will be + // implied from the node properties, and if it is set, it will only + // be serialized into the representation if TaggedStyle is used or + // the implicit tag diverges from the provided one. + Tag string + + // Value holds the unescaped and unquoted represenation of the value. + Value string + + // Anchor holds the anchor name for this node, which allows aliases to point to it. + Anchor string + + // Alias holds the node that this alias points to. Only valid when Kind is AliasNode. + Alias *Node + + // Content holds contained nodes for documents, mappings, and sequences. + Content []*Node + + // HeadComment holds any comments in the lines preceding the node and + // not separated by an empty line. + HeadComment string + + // LineComment holds any comments at the end of the line where the node is in. + LineComment string + + // FootComment holds any comments following the node and before empty lines. + FootComment string + + // Line and Column hold the node position in the decoded YAML text. + // These fields are not respected when encoding the node. + Line int + Column int +} + +// IsZero returns whether the node has all of its fields unset. +func (n *Node) IsZero() bool { + return n.Kind == 0 && n.Style == 0 && n.Tag == "" && n.Value == "" && n.Anchor == "" && n.Alias == nil && n.Content == nil && + n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" && n.Line == 0 && n.Column == 0 +} + +// LongTag returns the long form of the tag that indicates the data type for +// the node. If the Tag field isn't explicitly defined, one will be computed +// based on the node properties. +func (n *Node) LongTag() string { + return longTag(n.ShortTag()) +} + +// ShortTag returns the short form of the YAML tag that indicates data type for +// the node. If the Tag field isn't explicitly defined, one will be computed +// based on the node properties. +func (n *Node) ShortTag() string { + if n.indicatedString() { + return strTag + } + if n.Tag == "" || n.Tag == "!" { + switch n.Kind { + case MappingNode: + return mapTag + case SequenceNode: + return seqTag + case AliasNode: + if n.Alias != nil { + return n.Alias.ShortTag() + } + case ScalarNode: + tag, _ := resolve("", n.Value) + return tag + case 0: + // Special case to make the zero value convenient. + if n.IsZero() { + return nullTag + } + } + return "" + } + return shortTag(n.Tag) +} + +func (n *Node) indicatedString() bool { + return n.Kind == ScalarNode && + (shortTag(n.Tag) == strTag || + (n.Tag == "" || n.Tag == "!") && n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0) +} + +// SetString is a convenience function that sets the node to a string value +// and defines its style in a pleasant way depending on its content. +func (n *Node) SetString(s string) { + n.Kind = ScalarNode + if utf8.ValidString(s) { + n.Value = s + n.Tag = strTag + } else { + n.Value = encodeBase64(s) + n.Tag = binaryTag + } + if strings.Contains(n.Value, "\n") { + n.Style = LiteralStyle + } +} + +// -------------------------------------------------------------------------- +// Maintain a mapping of keys to structure field indexes + +// The code in this section was copied from mgo/bson. + +// structInfo holds details for the serialization of fields of +// a given struct. +type structInfo struct { + FieldsMap map[string]fieldInfo + FieldsList []fieldInfo + + // InlineMap is the number of the field in the struct that + // contains an ,inline map, or -1 if there's none. + InlineMap int + + // InlineUnmarshalers holds indexes to inlined fields that + // contain unmarshaler values. + InlineUnmarshalers [][]int +} + +type fieldInfo struct { + Key string + Num int + OmitEmpty bool + Flow bool + // Id holds the unique field identifier, so we can cheaply + // check for field duplicates without maintaining an extra map. + Id int + + // Inline holds the field index if the field is part of an inlined struct. + Inline []int +} + +var structMap = make(map[reflect.Type]*structInfo) +var fieldMapMutex sync.RWMutex +var unmarshalerType reflect.Type + +func init() { + var v Unmarshaler + unmarshalerType = reflect.ValueOf(&v).Elem().Type() +} + +func getStructInfo(st reflect.Type) (*structInfo, error) { + fieldMapMutex.RLock() + sinfo, found := structMap[st] + fieldMapMutex.RUnlock() + if found { + return sinfo, nil + } + + n := st.NumField() + fieldsMap := make(map[string]fieldInfo) + fieldsList := make([]fieldInfo, 0, n) + inlineMap := -1 + inlineUnmarshalers := [][]int(nil) + for i := 0; i != n; i++ { + field := st.Field(i) + if field.PkgPath != "" && !field.Anonymous { + continue // Private field + } + + info := fieldInfo{Num: i} + + tag := field.Tag.Get("yaml") + if tag == "" && strings.Index(string(field.Tag), ":") < 0 { + tag = string(field.Tag) + } + if tag == "-" { + continue + } + + inline := false + fields := strings.Split(tag, ",") + if len(fields) > 1 { + for _, flag := range fields[1:] { + switch flag { + case "omitempty": + info.OmitEmpty = true + case "flow": + info.Flow = true + case "inline": + inline = true + default: + return nil, errors.New(fmt.Sprintf("unsupported flag %q in tag %q of type %s", flag, tag, st)) + } + } + tag = fields[0] + } + + if inline { + switch field.Type.Kind() { + case reflect.Map: + if inlineMap >= 0 { + return nil, errors.New("multiple ,inline maps in struct " + st.String()) + } + if field.Type.Key() != reflect.TypeOf("") { + return nil, errors.New("option ,inline needs a map with string keys in struct " + st.String()) + } + inlineMap = info.Num + case reflect.Struct, reflect.Ptr: + ftype := field.Type + for ftype.Kind() == reflect.Ptr { + ftype = ftype.Elem() + } + if ftype.Kind() != reflect.Struct { + return nil, errors.New("option ,inline may only be used on a struct or map field") + } + if reflect.PtrTo(ftype).Implements(unmarshalerType) { + inlineUnmarshalers = append(inlineUnmarshalers, []int{i}) + } else { + sinfo, err := getStructInfo(ftype) + if err != nil { + return nil, err + } + for _, index := range sinfo.InlineUnmarshalers { + inlineUnmarshalers = append(inlineUnmarshalers, append([]int{i}, index...)) + } + for _, finfo := range sinfo.FieldsList { + if _, found := fieldsMap[finfo.Key]; found { + msg := "duplicated key '" + finfo.Key + "' in struct " + st.String() + return nil, errors.New(msg) + } + if finfo.Inline == nil { + finfo.Inline = []int{i, finfo.Num} + } else { + finfo.Inline = append([]int{i}, finfo.Inline...) + } + finfo.Id = len(fieldsList) + fieldsMap[finfo.Key] = finfo + fieldsList = append(fieldsList, finfo) + } + } + default: + return nil, errors.New("option ,inline may only be used on a struct or map field") + } + continue + } + + if tag != "" { + info.Key = tag + } else { + info.Key = strings.ToLower(field.Name) + } + + if _, found = fieldsMap[info.Key]; found { + msg := "duplicated key '" + info.Key + "' in struct " + st.String() + return nil, errors.New(msg) + } + + info.Id = len(fieldsList) + fieldsList = append(fieldsList, info) + fieldsMap[info.Key] = info + } + + sinfo = &structInfo{ + FieldsMap: fieldsMap, + FieldsList: fieldsList, + InlineMap: inlineMap, + InlineUnmarshalers: inlineUnmarshalers, + } + + fieldMapMutex.Lock() + structMap[st] = sinfo + fieldMapMutex.Unlock() + return sinfo, nil +} + +// IsZeroer is used to check whether an object is zero to +// determine whether it should be omitted when marshaling +// with the omitempty flag. One notable implementation +// is time.Time. +type IsZeroer interface { + IsZero() bool +} + +func isZero(v reflect.Value) bool { + kind := v.Kind() + if z, ok := v.Interface().(IsZeroer); ok { + if (kind == reflect.Ptr || kind == reflect.Interface) && v.IsNil() { + return true + } + return z.IsZero() + } + switch kind { + case reflect.String: + return len(v.String()) == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + case reflect.Slice: + return v.Len() == 0 + case reflect.Map: + return v.Len() == 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Struct: + vt := v.Type() + for i := v.NumField() - 1; i >= 0; i-- { + if vt.Field(i).PkgPath != "" { + continue // Private field + } + if !isZero(v.Field(i)) { + return false + } + } + return true + } + return false +} diff --git a/vendor/go.yaml.in/yaml/v3/yamlh.go b/vendor/go.yaml.in/yaml/v3/yamlh.go new file mode 100644 index 000000000..07c442361 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/yamlh.go @@ -0,0 +1,807 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// 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. + +package yaml + +import ( + "fmt" + "io" +) + +// The version directive data. +type yaml_version_directive_t struct { + major int8 // The major version number. + minor int8 // The minor version number. +} + +// The tag directive data. +type yaml_tag_directive_t struct { + handle []byte // The tag handle. + prefix []byte // The tag prefix. +} + +type yaml_encoding_t int + +// The stream encoding. +const ( + // Let the parser choose the encoding. + yaml_ANY_ENCODING yaml_encoding_t = iota + + yaml_UTF8_ENCODING // The default UTF-8 encoding. + yaml_UTF16LE_ENCODING // The UTF-16-LE encoding with BOM. + yaml_UTF16BE_ENCODING // The UTF-16-BE encoding with BOM. +) + +type yaml_break_t int + +// Line break types. +const ( + // Let the parser choose the break type. + yaml_ANY_BREAK yaml_break_t = iota + + yaml_CR_BREAK // Use CR for line breaks (Mac style). + yaml_LN_BREAK // Use LN for line breaks (Unix style). + yaml_CRLN_BREAK // Use CR LN for line breaks (DOS style). +) + +type yaml_error_type_t int + +// Many bad things could happen with the parser and emitter. +const ( + // No error is produced. + yaml_NO_ERROR yaml_error_type_t = iota + + yaml_MEMORY_ERROR // Cannot allocate or reallocate a block of memory. + yaml_READER_ERROR // Cannot read or decode the input stream. + yaml_SCANNER_ERROR // Cannot scan the input stream. + yaml_PARSER_ERROR // Cannot parse the input stream. + yaml_COMPOSER_ERROR // Cannot compose a YAML document. + yaml_WRITER_ERROR // Cannot write to the output stream. + yaml_EMITTER_ERROR // Cannot emit a YAML stream. +) + +// The pointer position. +type yaml_mark_t struct { + index int // The position index. + line int // The position line. + column int // The position column. +} + +// Node Styles + +type yaml_style_t int8 + +type yaml_scalar_style_t yaml_style_t + +// Scalar styles. +const ( + // Let the emitter choose the style. + yaml_ANY_SCALAR_STYLE yaml_scalar_style_t = 0 + + yaml_PLAIN_SCALAR_STYLE yaml_scalar_style_t = 1 << iota // The plain scalar style. + yaml_SINGLE_QUOTED_SCALAR_STYLE // The single-quoted scalar style. + yaml_DOUBLE_QUOTED_SCALAR_STYLE // The double-quoted scalar style. + yaml_LITERAL_SCALAR_STYLE // The literal scalar style. + yaml_FOLDED_SCALAR_STYLE // The folded scalar style. +) + +type yaml_sequence_style_t yaml_style_t + +// Sequence styles. +const ( + // Let the emitter choose the style. + yaml_ANY_SEQUENCE_STYLE yaml_sequence_style_t = iota + + yaml_BLOCK_SEQUENCE_STYLE // The block sequence style. + yaml_FLOW_SEQUENCE_STYLE // The flow sequence style. +) + +type yaml_mapping_style_t yaml_style_t + +// Mapping styles. +const ( + // Let the emitter choose the style. + yaml_ANY_MAPPING_STYLE yaml_mapping_style_t = iota + + yaml_BLOCK_MAPPING_STYLE // The block mapping style. + yaml_FLOW_MAPPING_STYLE // The flow mapping style. +) + +// Tokens + +type yaml_token_type_t int + +// Token types. +const ( + // An empty token. + yaml_NO_TOKEN yaml_token_type_t = iota + + yaml_STREAM_START_TOKEN // A STREAM-START token. + yaml_STREAM_END_TOKEN // A STREAM-END token. + + yaml_VERSION_DIRECTIVE_TOKEN // A VERSION-DIRECTIVE token. + yaml_TAG_DIRECTIVE_TOKEN // A TAG-DIRECTIVE token. + yaml_DOCUMENT_START_TOKEN // A DOCUMENT-START token. + yaml_DOCUMENT_END_TOKEN // A DOCUMENT-END token. + + yaml_BLOCK_SEQUENCE_START_TOKEN // A BLOCK-SEQUENCE-START token. + yaml_BLOCK_MAPPING_START_TOKEN // A BLOCK-SEQUENCE-END token. + yaml_BLOCK_END_TOKEN // A BLOCK-END token. + + yaml_FLOW_SEQUENCE_START_TOKEN // A FLOW-SEQUENCE-START token. + yaml_FLOW_SEQUENCE_END_TOKEN // A FLOW-SEQUENCE-END token. + yaml_FLOW_MAPPING_START_TOKEN // A FLOW-MAPPING-START token. + yaml_FLOW_MAPPING_END_TOKEN // A FLOW-MAPPING-END token. + + yaml_BLOCK_ENTRY_TOKEN // A BLOCK-ENTRY token. + yaml_FLOW_ENTRY_TOKEN // A FLOW-ENTRY token. + yaml_KEY_TOKEN // A KEY token. + yaml_VALUE_TOKEN // A VALUE token. + + yaml_ALIAS_TOKEN // An ALIAS token. + yaml_ANCHOR_TOKEN // An ANCHOR token. + yaml_TAG_TOKEN // A TAG token. + yaml_SCALAR_TOKEN // A SCALAR token. +) + +func (tt yaml_token_type_t) String() string { + switch tt { + case yaml_NO_TOKEN: + return "yaml_NO_TOKEN" + case yaml_STREAM_START_TOKEN: + return "yaml_STREAM_START_TOKEN" + case yaml_STREAM_END_TOKEN: + return "yaml_STREAM_END_TOKEN" + case yaml_VERSION_DIRECTIVE_TOKEN: + return "yaml_VERSION_DIRECTIVE_TOKEN" + case yaml_TAG_DIRECTIVE_TOKEN: + return "yaml_TAG_DIRECTIVE_TOKEN" + case yaml_DOCUMENT_START_TOKEN: + return "yaml_DOCUMENT_START_TOKEN" + case yaml_DOCUMENT_END_TOKEN: + return "yaml_DOCUMENT_END_TOKEN" + case yaml_BLOCK_SEQUENCE_START_TOKEN: + return "yaml_BLOCK_SEQUENCE_START_TOKEN" + case yaml_BLOCK_MAPPING_START_TOKEN: + return "yaml_BLOCK_MAPPING_START_TOKEN" + case yaml_BLOCK_END_TOKEN: + return "yaml_BLOCK_END_TOKEN" + case yaml_FLOW_SEQUENCE_START_TOKEN: + return "yaml_FLOW_SEQUENCE_START_TOKEN" + case yaml_FLOW_SEQUENCE_END_TOKEN: + return "yaml_FLOW_SEQUENCE_END_TOKEN" + case yaml_FLOW_MAPPING_START_TOKEN: + return "yaml_FLOW_MAPPING_START_TOKEN" + case yaml_FLOW_MAPPING_END_TOKEN: + return "yaml_FLOW_MAPPING_END_TOKEN" + case yaml_BLOCK_ENTRY_TOKEN: + return "yaml_BLOCK_ENTRY_TOKEN" + case yaml_FLOW_ENTRY_TOKEN: + return "yaml_FLOW_ENTRY_TOKEN" + case yaml_KEY_TOKEN: + return "yaml_KEY_TOKEN" + case yaml_VALUE_TOKEN: + return "yaml_VALUE_TOKEN" + case yaml_ALIAS_TOKEN: + return "yaml_ALIAS_TOKEN" + case yaml_ANCHOR_TOKEN: + return "yaml_ANCHOR_TOKEN" + case yaml_TAG_TOKEN: + return "yaml_TAG_TOKEN" + case yaml_SCALAR_TOKEN: + return "yaml_SCALAR_TOKEN" + } + return "" +} + +// The token structure. +type yaml_token_t struct { + // The token type. + typ yaml_token_type_t + + // The start/end of the token. + start_mark, end_mark yaml_mark_t + + // The stream encoding (for yaml_STREAM_START_TOKEN). + encoding yaml_encoding_t + + // The alias/anchor/scalar value or tag/tag directive handle + // (for yaml_ALIAS_TOKEN, yaml_ANCHOR_TOKEN, yaml_SCALAR_TOKEN, yaml_TAG_TOKEN, yaml_TAG_DIRECTIVE_TOKEN). + value []byte + + // The tag suffix (for yaml_TAG_TOKEN). + suffix []byte + + // The tag directive prefix (for yaml_TAG_DIRECTIVE_TOKEN). + prefix []byte + + // The scalar style (for yaml_SCALAR_TOKEN). + style yaml_scalar_style_t + + // The version directive major/minor (for yaml_VERSION_DIRECTIVE_TOKEN). + major, minor int8 +} + +// Events + +type yaml_event_type_t int8 + +// Event types. +const ( + // An empty event. + yaml_NO_EVENT yaml_event_type_t = iota + + yaml_STREAM_START_EVENT // A STREAM-START event. + yaml_STREAM_END_EVENT // A STREAM-END event. + yaml_DOCUMENT_START_EVENT // A DOCUMENT-START event. + yaml_DOCUMENT_END_EVENT // A DOCUMENT-END event. + yaml_ALIAS_EVENT // An ALIAS event. + yaml_SCALAR_EVENT // A SCALAR event. + yaml_SEQUENCE_START_EVENT // A SEQUENCE-START event. + yaml_SEQUENCE_END_EVENT // A SEQUENCE-END event. + yaml_MAPPING_START_EVENT // A MAPPING-START event. + yaml_MAPPING_END_EVENT // A MAPPING-END event. + yaml_TAIL_COMMENT_EVENT +) + +var eventStrings = []string{ + yaml_NO_EVENT: "none", + yaml_STREAM_START_EVENT: "stream start", + yaml_STREAM_END_EVENT: "stream end", + yaml_DOCUMENT_START_EVENT: "document start", + yaml_DOCUMENT_END_EVENT: "document end", + yaml_ALIAS_EVENT: "alias", + yaml_SCALAR_EVENT: "scalar", + yaml_SEQUENCE_START_EVENT: "sequence start", + yaml_SEQUENCE_END_EVENT: "sequence end", + yaml_MAPPING_START_EVENT: "mapping start", + yaml_MAPPING_END_EVENT: "mapping end", + yaml_TAIL_COMMENT_EVENT: "tail comment", +} + +func (e yaml_event_type_t) String() string { + if e < 0 || int(e) >= len(eventStrings) { + return fmt.Sprintf("unknown event %d", e) + } + return eventStrings[e] +} + +// The event structure. +type yaml_event_t struct { + + // The event type. + typ yaml_event_type_t + + // The start and end of the event. + start_mark, end_mark yaml_mark_t + + // The document encoding (for yaml_STREAM_START_EVENT). + encoding yaml_encoding_t + + // The version directive (for yaml_DOCUMENT_START_EVENT). + version_directive *yaml_version_directive_t + + // The list of tag directives (for yaml_DOCUMENT_START_EVENT). + tag_directives []yaml_tag_directive_t + + // The comments + head_comment []byte + line_comment []byte + foot_comment []byte + tail_comment []byte + + // The anchor (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT, yaml_ALIAS_EVENT). + anchor []byte + + // The tag (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT). + tag []byte + + // The scalar value (for yaml_SCALAR_EVENT). + value []byte + + // Is the document start/end indicator implicit, or the tag optional? + // (for yaml_DOCUMENT_START_EVENT, yaml_DOCUMENT_END_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT, yaml_SCALAR_EVENT). + implicit bool + + // Is the tag optional for any non-plain style? (for yaml_SCALAR_EVENT). + quoted_implicit bool + + // The style (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT). + style yaml_style_t +} + +func (e *yaml_event_t) scalar_style() yaml_scalar_style_t { return yaml_scalar_style_t(e.style) } +func (e *yaml_event_t) sequence_style() yaml_sequence_style_t { return yaml_sequence_style_t(e.style) } +func (e *yaml_event_t) mapping_style() yaml_mapping_style_t { return yaml_mapping_style_t(e.style) } + +// Nodes + +const ( + yaml_NULL_TAG = "tag:yaml.org,2002:null" // The tag !!null with the only possible value: null. + yaml_BOOL_TAG = "tag:yaml.org,2002:bool" // The tag !!bool with the values: true and false. + yaml_STR_TAG = "tag:yaml.org,2002:str" // The tag !!str for string values. + yaml_INT_TAG = "tag:yaml.org,2002:int" // The tag !!int for integer values. + yaml_FLOAT_TAG = "tag:yaml.org,2002:float" // The tag !!float for float values. + yaml_TIMESTAMP_TAG = "tag:yaml.org,2002:timestamp" // The tag !!timestamp for date and time values. + + yaml_SEQ_TAG = "tag:yaml.org,2002:seq" // The tag !!seq is used to denote sequences. + yaml_MAP_TAG = "tag:yaml.org,2002:map" // The tag !!map is used to denote mapping. + + // Not in original libyaml. + yaml_BINARY_TAG = "tag:yaml.org,2002:binary" + yaml_MERGE_TAG = "tag:yaml.org,2002:merge" + + yaml_DEFAULT_SCALAR_TAG = yaml_STR_TAG // The default scalar tag is !!str. + yaml_DEFAULT_SEQUENCE_TAG = yaml_SEQ_TAG // The default sequence tag is !!seq. + yaml_DEFAULT_MAPPING_TAG = yaml_MAP_TAG // The default mapping tag is !!map. +) + +type yaml_node_type_t int + +// Node types. +const ( + // An empty node. + yaml_NO_NODE yaml_node_type_t = iota + + yaml_SCALAR_NODE // A scalar node. + yaml_SEQUENCE_NODE // A sequence node. + yaml_MAPPING_NODE // A mapping node. +) + +// An element of a sequence node. +type yaml_node_item_t int + +// An element of a mapping node. +type yaml_node_pair_t struct { + key int // The key of the element. + value int // The value of the element. +} + +// The node structure. +type yaml_node_t struct { + typ yaml_node_type_t // The node type. + tag []byte // The node tag. + + // The node data. + + // The scalar parameters (for yaml_SCALAR_NODE). + scalar struct { + value []byte // The scalar value. + length int // The length of the scalar value. + style yaml_scalar_style_t // The scalar style. + } + + // The sequence parameters (for YAML_SEQUENCE_NODE). + sequence struct { + items_data []yaml_node_item_t // The stack of sequence items. + style yaml_sequence_style_t // The sequence style. + } + + // The mapping parameters (for yaml_MAPPING_NODE). + mapping struct { + pairs_data []yaml_node_pair_t // The stack of mapping pairs (key, value). + pairs_start *yaml_node_pair_t // The beginning of the stack. + pairs_end *yaml_node_pair_t // The end of the stack. + pairs_top *yaml_node_pair_t // The top of the stack. + style yaml_mapping_style_t // The mapping style. + } + + start_mark yaml_mark_t // The beginning of the node. + end_mark yaml_mark_t // The end of the node. + +} + +// The document structure. +type yaml_document_t struct { + + // The document nodes. + nodes []yaml_node_t + + // The version directive. + version_directive *yaml_version_directive_t + + // The list of tag directives. + tag_directives_data []yaml_tag_directive_t + tag_directives_start int // The beginning of the tag directives list. + tag_directives_end int // The end of the tag directives list. + + start_implicit int // Is the document start indicator implicit? + end_implicit int // Is the document end indicator implicit? + + // The start/end of the document. + start_mark, end_mark yaml_mark_t +} + +// The prototype of a read handler. +// +// The read handler is called when the parser needs to read more bytes from the +// source. The handler should write not more than size bytes to the buffer. +// The number of written bytes should be set to the size_read variable. +// +// [in,out] data A pointer to an application data specified by +// yaml_parser_set_input(). +// [out] buffer The buffer to write the data from the source. +// [in] size The size of the buffer. +// [out] size_read The actual number of bytes read from the source. +// +// On success, the handler should return 1. If the handler failed, +// the returned value should be 0. On EOF, the handler should set the +// size_read to 0 and return 1. +type yaml_read_handler_t func(parser *yaml_parser_t, buffer []byte) (n int, err error) + +// This structure holds information about a potential simple key. +type yaml_simple_key_t struct { + possible bool // Is a simple key possible? + required bool // Is a simple key required? + token_number int // The number of the token. + mark yaml_mark_t // The position mark. +} + +// The states of the parser. +type yaml_parser_state_t int + +const ( + yaml_PARSE_STREAM_START_STATE yaml_parser_state_t = iota + + yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE // Expect the beginning of an implicit document. + yaml_PARSE_DOCUMENT_START_STATE // Expect DOCUMENT-START. + yaml_PARSE_DOCUMENT_CONTENT_STATE // Expect the content of a document. + yaml_PARSE_DOCUMENT_END_STATE // Expect DOCUMENT-END. + yaml_PARSE_BLOCK_NODE_STATE // Expect a block node. + yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE // Expect a block node or indentless sequence. + yaml_PARSE_FLOW_NODE_STATE // Expect a flow node. + yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a block sequence. + yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE // Expect an entry of a block sequence. + yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE // Expect an entry of an indentless sequence. + yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping. + yaml_PARSE_BLOCK_MAPPING_KEY_STATE // Expect a block mapping key. + yaml_PARSE_BLOCK_MAPPING_VALUE_STATE // Expect a block mapping value. + yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a flow sequence. + yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE // Expect an entry of a flow sequence. + yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE // Expect a key of an ordered mapping. + yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE // Expect a value of an ordered mapping. + yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE // Expect the and of an ordered mapping entry. + yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping. + yaml_PARSE_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping. + yaml_PARSE_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping. + yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE // Expect an empty value of a flow mapping. + yaml_PARSE_END_STATE // Expect nothing. +) + +func (ps yaml_parser_state_t) String() string { + switch ps { + case yaml_PARSE_STREAM_START_STATE: + return "yaml_PARSE_STREAM_START_STATE" + case yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE: + return "yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE" + case yaml_PARSE_DOCUMENT_START_STATE: + return "yaml_PARSE_DOCUMENT_START_STATE" + case yaml_PARSE_DOCUMENT_CONTENT_STATE: + return "yaml_PARSE_DOCUMENT_CONTENT_STATE" + case yaml_PARSE_DOCUMENT_END_STATE: + return "yaml_PARSE_DOCUMENT_END_STATE" + case yaml_PARSE_BLOCK_NODE_STATE: + return "yaml_PARSE_BLOCK_NODE_STATE" + case yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE: + return "yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE" + case yaml_PARSE_FLOW_NODE_STATE: + return "yaml_PARSE_FLOW_NODE_STATE" + case yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE: + return "yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE" + case yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE: + return "yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE" + case yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE: + return "yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE" + case yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE: + return "yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE" + case yaml_PARSE_BLOCK_MAPPING_KEY_STATE: + return "yaml_PARSE_BLOCK_MAPPING_KEY_STATE" + case yaml_PARSE_BLOCK_MAPPING_VALUE_STATE: + return "yaml_PARSE_BLOCK_MAPPING_VALUE_STATE" + case yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE: + return "yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE" + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE: + return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE" + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE: + return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE" + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE: + return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE" + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE: + return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE" + case yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE: + return "yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE" + case yaml_PARSE_FLOW_MAPPING_KEY_STATE: + return "yaml_PARSE_FLOW_MAPPING_KEY_STATE" + case yaml_PARSE_FLOW_MAPPING_VALUE_STATE: + return "yaml_PARSE_FLOW_MAPPING_VALUE_STATE" + case yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE: + return "yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE" + case yaml_PARSE_END_STATE: + return "yaml_PARSE_END_STATE" + } + return "" +} + +// This structure holds aliases data. +type yaml_alias_data_t struct { + anchor []byte // The anchor. + index int // The node id. + mark yaml_mark_t // The anchor mark. +} + +// The parser structure. +// +// All members are internal. Manage the structure using the +// yaml_parser_ family of functions. +type yaml_parser_t struct { + + // Error handling + + error yaml_error_type_t // Error type. + + problem string // Error description. + + // The byte about which the problem occurred. + problem_offset int + problem_value int + problem_mark yaml_mark_t + + // The error context. + context string + context_mark yaml_mark_t + + // Reader stuff + + read_handler yaml_read_handler_t // Read handler. + + input_reader io.Reader // File input data. + input []byte // String input data. + input_pos int + + eof bool // EOF flag + + buffer []byte // The working buffer. + buffer_pos int // The current position of the buffer. + + unread int // The number of unread characters in the buffer. + + newlines int // The number of line breaks since last non-break/non-blank character + + raw_buffer []byte // The raw buffer. + raw_buffer_pos int // The current position of the buffer. + + encoding yaml_encoding_t // The input encoding. + + offset int // The offset of the current position (in bytes). + mark yaml_mark_t // The mark of the current position. + + // Comments + + head_comment []byte // The current head comments + line_comment []byte // The current line comments + foot_comment []byte // The current foot comments + tail_comment []byte // Foot comment that happens at the end of a block. + stem_comment []byte // Comment in item preceding a nested structure (list inside list item, etc) + + comments []yaml_comment_t // The folded comments for all parsed tokens + comments_head int + + // Scanner stuff + + stream_start_produced bool // Have we started to scan the input stream? + stream_end_produced bool // Have we reached the end of the input stream? + + flow_level int // The number of unclosed '[' and '{' indicators. + + tokens []yaml_token_t // The tokens queue. + tokens_head int // The head of the tokens queue. + tokens_parsed int // The number of tokens fetched from the queue. + token_available bool // Does the tokens queue contain a token ready for dequeueing. + + indent int // The current indentation level. + indents []int // The indentation levels stack. + + simple_key_allowed bool // May a simple key occur at the current position? + simple_keys []yaml_simple_key_t // The stack of simple keys. + simple_keys_by_tok map[int]int // possible simple_key indexes indexed by token_number + + // Parser stuff + + state yaml_parser_state_t // The current parser state. + states []yaml_parser_state_t // The parser states stack. + marks []yaml_mark_t // The stack of marks. + tag_directives []yaml_tag_directive_t // The list of TAG directives. + + // Dumper stuff + + aliases []yaml_alias_data_t // The alias data. + + document *yaml_document_t // The currently parsed document. +} + +type yaml_comment_t struct { + scan_mark yaml_mark_t // Position where scanning for comments started + token_mark yaml_mark_t // Position after which tokens will be associated with this comment + start_mark yaml_mark_t // Position of '#' comment mark + end_mark yaml_mark_t // Position where comment terminated + + head []byte + line []byte + foot []byte +} + +// Emitter Definitions + +// The prototype of a write handler. +// +// The write handler is called when the emitter needs to flush the accumulated +// characters to the output. The handler should write @a size bytes of the +// @a buffer to the output. +// +// @param[in,out] data A pointer to an application data specified by +// yaml_emitter_set_output(). +// @param[in] buffer The buffer with bytes to be written. +// @param[in] size The size of the buffer. +// +// @returns On success, the handler should return @c 1. If the handler failed, +// the returned value should be @c 0. +type yaml_write_handler_t func(emitter *yaml_emitter_t, buffer []byte) error + +type yaml_emitter_state_t int + +// The emitter states. +const ( + // Expect STREAM-START. + yaml_EMIT_STREAM_START_STATE yaml_emitter_state_t = iota + + yaml_EMIT_FIRST_DOCUMENT_START_STATE // Expect the first DOCUMENT-START or STREAM-END. + yaml_EMIT_DOCUMENT_START_STATE // Expect DOCUMENT-START or STREAM-END. + yaml_EMIT_DOCUMENT_CONTENT_STATE // Expect the content of a document. + yaml_EMIT_DOCUMENT_END_STATE // Expect DOCUMENT-END. + yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a flow sequence. + yaml_EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE // Expect the next item of a flow sequence, with the comma already written out + yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE // Expect an item of a flow sequence. + yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping. + yaml_EMIT_FLOW_MAPPING_TRAIL_KEY_STATE // Expect the next key of a flow mapping, with the comma already written out + yaml_EMIT_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping. + yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a flow mapping. + yaml_EMIT_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping. + yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a block sequence. + yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE // Expect an item of a block sequence. + yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping. + yaml_EMIT_BLOCK_MAPPING_KEY_STATE // Expect the key of a block mapping. + yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a block mapping. + yaml_EMIT_BLOCK_MAPPING_VALUE_STATE // Expect a value of a block mapping. + yaml_EMIT_END_STATE // Expect nothing. +) + +// The emitter structure. +// +// All members are internal. Manage the structure using the @c yaml_emitter_ +// family of functions. +type yaml_emitter_t struct { + + // Error handling + + error yaml_error_type_t // Error type. + problem string // Error description. + + // Writer stuff + + write_handler yaml_write_handler_t // Write handler. + + output_buffer *[]byte // String output data. + output_writer io.Writer // File output data. + + buffer []byte // The working buffer. + buffer_pos int // The current position of the buffer. + + raw_buffer []byte // The raw buffer. + raw_buffer_pos int // The current position of the buffer. + + encoding yaml_encoding_t // The stream encoding. + + // Emitter stuff + + canonical bool // If the output is in the canonical style? + best_indent int // The number of indentation spaces. + best_width int // The preferred width of the output lines. + unicode bool // Allow unescaped non-ASCII characters? + line_break yaml_break_t // The preferred line break. + + state yaml_emitter_state_t // The current emitter state. + states []yaml_emitter_state_t // The stack of states. + + events []yaml_event_t // The event queue. + events_head int // The head of the event queue. + + indents []int // The stack of indentation levels. + + tag_directives []yaml_tag_directive_t // The list of tag directives. + + indent int // The current indentation level. + + compact_sequence_indent bool // Is '- ' is considered part of the indentation for sequence elements? + + flow_level int // The current flow level. + + root_context bool // Is it the document root context? + sequence_context bool // Is it a sequence context? + mapping_context bool // Is it a mapping context? + simple_key_context bool // Is it a simple mapping key context? + + line int // The current line. + column int // The current column. + whitespace bool // If the last character was a whitespace? + indention bool // If the last character was an indentation character (' ', '-', '?', ':')? + open_ended bool // If an explicit document end is required? + + space_above bool // Is there's an empty line above? + foot_indent int // The indent used to write the foot comment above, or -1 if none. + + // Anchor analysis. + anchor_data struct { + anchor []byte // The anchor value. + alias bool // Is it an alias? + } + + // Tag analysis. + tag_data struct { + handle []byte // The tag handle. + suffix []byte // The tag suffix. + } + + // Scalar analysis. + scalar_data struct { + value []byte // The scalar value. + multiline bool // Does the scalar contain line breaks? + flow_plain_allowed bool // Can the scalar be expessed in the flow plain style? + block_plain_allowed bool // Can the scalar be expressed in the block plain style? + single_quoted_allowed bool // Can the scalar be expressed in the single quoted style? + block_allowed bool // Can the scalar be expressed in the literal or folded styles? + style yaml_scalar_style_t // The output style. + } + + // Comments + head_comment []byte + line_comment []byte + foot_comment []byte + tail_comment []byte + + key_line_comment []byte + + // Dumper stuff + + opened bool // If the stream was already opened? + closed bool // If the stream was already closed? + + // The information associated with the document nodes. + anchors *struct { + references int // The number of references. + anchor int // The anchor id. + serialized bool // If the node has been emitted? + } + + last_anchor_id int // The last assigned anchor id. + + document *yaml_document_t // The currently emitted document. +} diff --git a/vendor/go.yaml.in/yaml/v3/yamlprivateh.go b/vendor/go.yaml.in/yaml/v3/yamlprivateh.go new file mode 100644 index 000000000..dea1ba961 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v3/yamlprivateh.go @@ -0,0 +1,198 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// 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. + +package yaml + +const ( + // The size of the input raw buffer. + input_raw_buffer_size = 512 + + // The size of the input buffer. + // It should be possible to decode the whole raw buffer. + input_buffer_size = input_raw_buffer_size * 3 + + // The size of the output buffer. + output_buffer_size = 128 + + // The size of the output raw buffer. + // It should be possible to encode the whole output buffer. + output_raw_buffer_size = (output_buffer_size*2 + 2) + + // The size of other stacks and queues. + initial_stack_size = 16 + initial_queue_size = 16 + initial_string_size = 16 +) + +// Check if the character at the specified position is an alphabetical +// character, a digit, '_', or '-'. +func is_alpha(b []byte, i int) bool { + return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'Z' || b[i] >= 'a' && b[i] <= 'z' || b[i] == '_' || b[i] == '-' +} + +// Check if the character at the specified position is a digit. +func is_digit(b []byte, i int) bool { + return b[i] >= '0' && b[i] <= '9' +} + +// Get the value of a digit. +func as_digit(b []byte, i int) int { + return int(b[i]) - '0' +} + +// Check if the character at the specified position is a hex-digit. +func is_hex(b []byte, i int) bool { + return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'F' || b[i] >= 'a' && b[i] <= 'f' +} + +// Get the value of a hex-digit. +func as_hex(b []byte, i int) int { + bi := b[i] + if bi >= 'A' && bi <= 'F' { + return int(bi) - 'A' + 10 + } + if bi >= 'a' && bi <= 'f' { + return int(bi) - 'a' + 10 + } + return int(bi) - '0' +} + +// Check if the character is ASCII. +func is_ascii(b []byte, i int) bool { + return b[i] <= 0x7F +} + +// Check if the character at the start of the buffer can be printed unescaped. +func is_printable(b []byte, i int) bool { + return ((b[i] == 0x0A) || // . == #x0A + (b[i] >= 0x20 && b[i] <= 0x7E) || // #x20 <= . <= #x7E + (b[i] == 0xC2 && b[i+1] >= 0xA0) || // #0xA0 <= . <= #xD7FF + (b[i] > 0xC2 && b[i] < 0xED) || + (b[i] == 0xED && b[i+1] < 0xA0) || + (b[i] == 0xEE) || + (b[i] == 0xEF && // #xE000 <= . <= #xFFFD + !(b[i+1] == 0xBB && b[i+2] == 0xBF) && // && . != #xFEFF + !(b[i+1] == 0xBF && (b[i+2] == 0xBE || b[i+2] == 0xBF)))) +} + +// Check if the character at the specified position is NUL. +func is_z(b []byte, i int) bool { + return b[i] == 0x00 +} + +// Check if the beginning of the buffer is a BOM. +func is_bom(b []byte, i int) bool { + return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF +} + +// Check if the character at the specified position is space. +func is_space(b []byte, i int) bool { + return b[i] == ' ' +} + +// Check if the character at the specified position is tab. +func is_tab(b []byte, i int) bool { + return b[i] == '\t' +} + +// Check if the character at the specified position is blank (space or tab). +func is_blank(b []byte, i int) bool { + //return is_space(b, i) || is_tab(b, i) + return b[i] == ' ' || b[i] == '\t' +} + +// Check if the character at the specified position is a line break. +func is_break(b []byte, i int) bool { + return (b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9) // PS (#x2029) +} + +func is_crlf(b []byte, i int) bool { + return b[i] == '\r' && b[i+1] == '\n' +} + +// Check if the character is a line break or NUL. +func is_breakz(b []byte, i int) bool { + //return is_break(b, i) || is_z(b, i) + return ( + // is_break: + b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) + // is_z: + b[i] == 0) +} + +// Check if the character is a line break, space, or NUL. +func is_spacez(b []byte, i int) bool { + //return is_space(b, i) || is_breakz(b, i) + return ( + // is_space: + b[i] == ' ' || + // is_breakz: + b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) + b[i] == 0) +} + +// Check if the character is a line break, space, tab, or NUL. +func is_blankz(b []byte, i int) bool { + //return is_blank(b, i) || is_breakz(b, i) + return ( + // is_blank: + b[i] == ' ' || b[i] == '\t' || + // is_breakz: + b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) + b[i] == 0) +} + +// Determine the width of the character. +func width(b byte) int { + // Don't replace these by a switch without first + // confirming that it is being inlined. + if b&0x80 == 0x00 { + return 1 + } + if b&0xE0 == 0xC0 { + return 2 + } + if b&0xF0 == 0xE0 { + return 3 + } + if b&0xF8 == 0xF0 { + return 4 + } + return 0 + +} diff --git a/vendor/golang.org/x/mod/modfile/read.go b/vendor/golang.org/x/mod/modfile/read.go index 5b528c718..9e35e1ac5 100644 --- a/vendor/golang.org/x/mod/modfile/read.go +++ b/vendor/golang.org/x/mod/modfile/read.go @@ -924,7 +924,7 @@ var ( moduleStr = []byte("module") ) -// ModulePath returns the module path from the gomod file text. +// ModulePath returns the module path from the go.mod file text. // If it cannot find a module path, it returns an empty string. // It is tolerant of unrelated problems in the go.mod file. func ModulePath(mod []byte) string { diff --git a/vendor/golang.org/x/mod/modfile/rule.go b/vendor/golang.org/x/mod/modfile/rule.go index 9ab203b56..20ba825d2 100644 --- a/vendor/golang.org/x/mod/modfile/rule.go +++ b/vendor/golang.org/x/mod/modfile/rule.go @@ -1477,7 +1477,7 @@ func (f *File) setRequireSeparateIndirect(req []*Require, simplify bool) { // Delete requirements we don't want anymore. // Update versions and indirect comments on requirements we want to keep. // If a requirement is in last{Direct,Indirect}Block with the wrong - // indirect marking after this, or if the requirement is in an single + // indirect marking after this, or if the requirement is in a single // uncommented mixed block (oneFlatUncommentedBlock), move it to the // correct block. // @@ -1537,7 +1537,7 @@ func (f *File) DropRequire(path string) error { return nil } -// AddExclude adds a exclude statement to the mod file. Errors if the provided +// AddExclude adds an exclude statement to the mod file. Errors if the provided // version is not a canonical version string func (f *File) AddExclude(path, vers string) error { if err := checkCanonicalVersion(path, vers); err != nil { @@ -1708,7 +1708,7 @@ func (f *File) AddIgnore(path string) error { return nil } -// DropIgnore removes a ignore directive with the given path. +// DropIgnore removes an ignore directive with the given path. // It does nothing if no such ignore directive exists. func (f *File) DropIgnore(path string) error { for _, t := range f.Ignore { diff --git a/vendor/modules.txt b/vendor/modules.txt index 4a5e0849f..e4d6e564f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -39,16 +39,13 @@ github.com/cloudfoundry/jibber_jabber # github.com/creack/pty v1.1.24 ## explicit; go 1.18 github.com/creack/pty -# github.com/davecgh/go-spew v1.1.1 -## explicit -github.com/davecgh/go-spew/spew # github.com/fatih/color v1.9.0 ## explicit; go 1.13 github.com/fatih/color # github.com/gdamore/encoding v1.0.1 ## explicit; go 1.9 github.com/gdamore/encoding -# github.com/gdamore/tcell/v3 v3.4.1 +# github.com/gdamore/tcell/v3 v3.4.2 ## explicit; go 1.25.0 github.com/gdamore/tcell/v3 github.com/gdamore/tcell/v3/color @@ -61,13 +58,6 @@ github.com/go-errors/errors # github.com/go-logfmt/logfmt v0.5.0 ## explicit; go 1.13 github.com/go-logfmt/logfmt -# github.com/google/go-cmp v0.7.0 -## explicit; go 1.21 -github.com/google/go-cmp/cmp -github.com/google/go-cmp/cmp/internal/diff -github.com/google/go-cmp/cmp/internal/flags -github.com/google/go-cmp/cmp/internal/function -github.com/google/go-cmp/cmp/internal/value # github.com/gookit/color v1.6.1 ## explicit; go 1.18 github.com/gookit/color @@ -99,7 +89,7 @@ github.com/kr/logfmt # github.com/kyokomi/emoji/v2 v2.2.14 ## explicit; go 1.21 github.com/kyokomi/emoji/v2 -# github.com/lucasb-eyer/go-colorful v1.4.0 +# github.com/lucasb-eyer/go-colorful v1.4.1 ## explicit; go 1.12 github.com/lucasb-eyer/go-colorful # github.com/mailru/easyjson v0.7.7 @@ -125,9 +115,6 @@ github.com/mitchellh/go-ps # github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe ## explicit; go 1.17 github.com/petermattis/goid -# github.com/pmezard/go-difflib v1.0.0 -## explicit -github.com/pmezard/go-difflib/difflib # github.com/rivo/uniseg v0.4.7 ## explicit; go 1.18 github.com/rivo/uniseg @@ -147,8 +134,8 @@ github.com/sanity-io/litter # github.com/sasha-s/go-deadlock v0.3.9 ## explicit github.com/sasha-s/go-deadlock -# github.com/sirupsen/logrus v1.9.4 -## explicit; go 1.17 +# github.com/sirupsen/logrus v1.10.2 +## explicit; go 1.23 github.com/sirupsen/logrus # github.com/spf13/afero v1.15.0 ## explicit; go 1.23.0 @@ -161,21 +148,26 @@ github.com/spkg/bom # github.com/stefanhaller/git-todo-parser v0.0.7-0.20250905083220-c50528f08304 ## explicit; go 1.13 github.com/stefanhaller/git-todo-parser/todo -# github.com/stretchr/testify v1.11.1 +# github.com/stretchr/testify v1.12.1 ## explicit; go 1.17 github.com/stretchr/testify/assert github.com/stretchr/testify/assert/yaml +github.com/stretchr/testify/internal/difflib +github.com/stretchr/testify/internal/spew # github.com/wk8/go-ordered-map/v2 v2.1.8 ## explicit; go 1.18 github.com/wk8/go-ordered-map/v2 -# github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e +# github.com/xo/terminfo v1.0.0 ## explicit; go 1.19 github.com/xo/terminfo +# go.yaml.in/yaml/v3 v3.0.5 +## explicit; go 1.16 +go.yaml.in/yaml/v3 # golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 ## explicit; go 1.20 golang.org/x/exp/constraints golang.org/x/exp/slices -# golang.org/x/mod v0.37.0 +# golang.org/x/mod v0.38.0 ## explicit; go 1.25.0 golang.org/x/mod/internal/lazyregexp golang.org/x/mod/modfile @@ -193,7 +185,7 @@ golang.org/x/sys/windows # golang.org/x/term v0.45.0 ## explicit; go 1.25.0 golang.org/x/term -# golang.org/x/text v0.40.0 +# golang.org/x/text v0.41.0 ## explicit; go 1.25.0 golang.org/x/text/cases golang.org/x/text/encoding @@ -206,7 +198,7 @@ golang.org/x/text/language golang.org/x/text/runes golang.org/x/text/transform golang.org/x/text/unicode/norm -# golang.org/x/tools v0.47.0 +# golang.org/x/tools v0.48.0 ## explicit; go 1.25.0 golang.org/x/tools/go/ast/astutil # gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c @@ -221,8 +213,8 @@ gopkg.in/ozeidan/fuzzy-patricia.v3/patricia # gopkg.in/yaml.v3 v3.0.1 ## explicit gopkg.in/yaml.v3 -# mvdan.cc/gofumpt v0.9.2 -## explicit; go 1.24.0 +# mvdan.cc/gofumpt v0.11.0 +## explicit; go 1.25.0 mvdan.cc/gofumpt mvdan.cc/gofumpt/format mvdan.cc/gofumpt/internal/govendor/diff diff --git a/vendor/mvdan.cc/gofumpt/CHANGELOG.md b/vendor/mvdan.cc/gofumpt/CHANGELOG.md index f3a384077..1168ddc63 100644 --- a/vendor/mvdan.cc/gofumpt/CHANGELOG.md +++ b/vendor/mvdan.cc/gofumpt/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## [v0.11.0] - 2026-07-27 + +Like v0.10.0, this release is based on Go 1.26's gofmt, and requires Go 1.25 or later. + +The multi-line function call rule introduced in v0.10.0 proved controversial, +so it is now the extra rule `balance_calls`, disabled by default. +It is also narrowed to only place the closing parenthesis on its own line +when the opening parenthesis ends a line. See #74. + +Avoid crashing when compiled with tinygo for Wasm, which lacks recover support, +by detecting commented-out code without the parser's bailout panic. See #230. + +Produce stable output in a single pass when a lone var declaration is adjacent +to a single-element var group, which previously required a second run. See #355. + +Keep the parentheses around an expression which begins with a composite literal +of the form `T{...}`, such as `(s{}.Foo())`, as they are required when the +expression starts an `if`, `for`, or `switch` clause. See #356. + +## [v0.10.0] - 2026-05-04 + +This release is based on Go 1.26's gofmt, and requires Go 1.25 or later. + +A new rule is introduced to drop unnecessary parentheses around expressions +where the inner expression is unambiguous on its own, such as `f((3))`. +Parentheses are kept where they are useful, such as on binary expressions. See #44. + +A new rule is introduced to require multi-line function calls to match +the opening and closing parenthesis in terms of the use of newlines. See #74. + +The `-extra` flag now accepts a comma-separated list of rule names to enable +individual extra rules, rather than enabling all of them at once. See #339. + +The following changes are included as well: + +* Avoid crashing on `go.mod` files without a `module` directive - #350 +* Avoid failing when an ignored directory cannot be read - #351 +* Avoid prefixing more kinds of commented-out Go code with spaces - #230 +* Avoid prefixing a shebang comment with a space - #237 +* Narrow the newlines on assignments rule to ignore complex cases - #354 +* Fix three bugs which caused a second gofumpt run to make changes - #132, #345 + ## [v0.9.1] - 2025-09-07 This is a bugfix release to address a regression in detecting @@ -187,6 +229,8 @@ those building programs with gofumpt. Finally, this release adds the `-version` flag, to print the tool's own version. The flag will work for "master" builds too. +[v0.11.0]: https://github.com/mvdan/gofumpt/releases/tag/v0.11.0 +[v0.10.0]: https://github.com/mvdan/gofumpt/releases/tag/v0.10.0 [v0.9.0]: https://github.com/mvdan/gofumpt/releases/tag/v0.9.0 [v0.8.0]: https://github.com/mvdan/gofumpt/releases/tag/v0.8.0 [v0.7.0]: https://github.com/mvdan/gofumpt/releases/tag/v0.7.0 diff --git a/vendor/mvdan.cc/gofumpt/README.md b/vendor/mvdan.cc/gofumpt/README.md index f391ef969..609cf65bf 100644 --- a/vendor/mvdan.cc/gofumpt/README.md +++ b/vendor/mvdan.cc/gofumpt/README.md @@ -7,7 +7,7 @@ Enforce a stricter format than `gofmt`, while being backwards compatible. That is, `gofumpt` is happy with a subset of the formats that `gofmt` is happy with. -The tool is a fork of `gofmt` as of Go 1.25.0, and requires Go 1.24 or later. +The tool is a fork of `gofmt` as of Go 1.26.0, and requires Go 1.25 or later. It can be used as a drop-in replacement to format your Go code, and running `gofmt` after `gofumpt` should produce no changes. For example: @@ -15,7 +15,7 @@ For example: gofumpt -l -w . Some of the Go source files in this repository belong to the Go project. -The project includes copies of `go/printer` and `go/doc/comment` as of Go 1.25.0 +The project includes copies of `go/printer` and `go/doc/comment` as of Go 1.26.0 to ensure consistent formatting independent of what Go version is being used. The [added formatting rules](#Added-rules) are implemented in the `format` package. @@ -31,7 +31,7 @@ and the `-s` flag is hidden as it is always enabled. ### Added rules -**No empty lines following an assignment operator** +**No newline after a simple assignment's operator**
Example @@ -438,6 +438,27 @@ type ZeroFields struct {
+**Definitely useless parentheses should be removed** + +
Example + +```go +type C chan (int) + +var _ = f((3)) +``` + +```go +type C chan int + +var _ = f(3) +``` + +Parentheses around binary or unary expressions, as well as around types +which require them (such as `chan (<-chan T)`), are kept as is. + +
+ ### Extra rules behind `-extra` **Adjacent parameters with the same type should be grouped together** @@ -472,6 +493,28 @@ func Foo() (err error) { +**Multi-line function calls with the opening parenthesis at the end of a line +should place the closing parenthesis at the start of a line** + +
Example + +```go +result := compute( + a, + b, + c) +``` + +```go +result := compute( + a, + b, + c, +) +``` + +
+ ### Installation `gofumpt` is a replacement for `gofmt`, so you can simply `go install` it as @@ -627,6 +670,18 @@ well might be proposed for `gofmt` itself. The tool is also compatible with `gofmt` and is aimed to be stable, so you can rely on it for your code as long as you pin a version of it. +### Updating with `go/format` and `cmd/gofmt` + +`internal/govendor` contains frozen copies of `go/format` and its dependencies +at a specific Go version, so that installing a specific version of `gofumpt` +results in exactly the same formatting behavior regardless of the Go version. + +As this tool is a fork of `cmd/gofmt`, the `gofmt.go`, `internal.go`, +`format/rewrite.go`, and `format/simplify.go` are inherited from upstream. +These include some modifications where necessary, and are updated manually. +Note that two live under the `format` package as we want to expose +syntax simplification via the Go API. + ### Frequently Asked Questions > Why attempt to replace `gofmt` instead of building on top of it? diff --git a/vendor/mvdan.cc/gofumpt/format/format.go b/vendor/mvdan.cc/gofumpt/format/format.go index 879969da9..438a73b53 100644 --- a/vendor/mvdan.cc/gofumpt/format/format.go +++ b/vendor/mvdan.cc/gofumpt/format/format.go @@ -23,7 +23,6 @@ import ( "unicode" "unicode/utf8" - "github.com/google/go-cmp/cmp" "golang.org/x/tools/go/ast/astutil" "mvdan.cc/gofumpt/internal/govendor/go/format" @@ -57,11 +56,81 @@ type Options struct { // is formatted as if it weren't inside a module. ModulePath string - // ExtraRules enables extra formatting rules, such as grouping function + // ExtraRules enables all extra formatting rules, such as grouping function // parameters with repeated types together. + // + // Deprecated: use [Options.Extra] instead. ExtraRules bool + + // Extra allows enabling extra formatting rules which are disabled by default. + Extra Extra } +// Extra is the set of extra formatting rules which are available. +// +// As the formatter evolves, we might add or remove boolean fields here. +// Go API users who wish to avoid build errors in such cases +// can use the string API in [Extra.Set]. +type Extra struct { + // TODO: should we have "All" to turn them all on, + // akin to how the CLI has -extra=true for historical reasons? + // I lean against it, as it should be a conscious choice to turn on + // each of these extra rules, and we should be able to add more rules + // without fear of causing unexpected changes for users. + + // GroupParams groups function parameters with repeated types. + GroupParams bool + + // ClotheReturns clothes naked returns in functions with named results. + ClotheReturns bool + + // BalanceCalls places a multi-line call's closing parenthesis on its + // own line when the opening parenthesis ends a line. + BalanceCalls bool +} + +func (e *Extra) String() string { + var active []string + if e.GroupParams { + active = append(active, "group_params") + } + if e.ClotheReturns { + active = append(active, "clothe_returns") + } + if e.BalanceCalls { + active = append(active, "balance_calls") + } + return strings.Join(active, ",") +} + +func (e *Extra) Set(v string) error { + if v == "true" { + e.GroupParams = true + e.ClotheReturns = true + e.BalanceCalls = true + return nil + } + *e = Extra{} + if v == "false" { + return nil + } + for s := range strings.SplitSeq(v, ",") { + switch s { + case "group_params": + e.GroupParams = true + case "clothe_returns": + e.ClotheReturns = true + case "balance_calls": + e.BalanceCalls = true + default: + return fmt.Errorf("unknown rule: %q", s) + } + } + return nil +} + +func (e *Extra) IsBoolFlag() bool { return true } + // Source formats src in gofumpt's format, assuming that src holds a valid Go // source file. func Source(src []byte, opts Options) ([]byte, error) { @@ -91,6 +160,10 @@ func Source(src []byte, opts Options) ([]byte, error) { func File(fset *token.FileSet, file *ast.File, opts Options) { simplify(file) + if opts.ExtraRules { + opts.Extra.Set("true") // enable all the extra rules + } + if opts.LangVersion == "" { opts.LangVersion = "go1" } else { @@ -258,6 +331,37 @@ func (f *fumpter) removeLinesBetween(from, to token.Pos) { f.removeLines(f.Line(from)+1, f.Line(to)) } +// removeParens unwraps a single-spec var group like "var (\n\tx = 1\n)" into a +// lone "var x = 1". It only acts on such groups without a doc comment. +func (f *fumpter) removeParens(node *ast.GenDecl) { + if node.Tok != token.VAR || len(node.Specs) != 1 || + !node.Lparen.IsValid() || node.Doc != nil { + return + } + specPos := node.Specs[0].Pos() + specEnd := node.Specs[0].End() + + if len(f.commentsBetween(node.TokPos, specPos)) > 0 { + // If the single spec has a comment on the line above, + // the comment must go before the entire declaration now. + node.TokPos = specPos + } else { + f.removeLines(f.Line(node.TokPos), f.Line(specPos)) + } + if len(f.commentsBetween(specEnd, node.Rparen)) > 0 { + // Leave one newline to not force a comment on the next line to + // become an inline comment. + f.removeLines(f.Line(specEnd)+1, f.Line(node.Rparen)) + } else { + f.removeLines(f.Line(specEnd), f.Line(node.Rparen)) + } + + // Remove the parentheses. go/printer will automatically + // get rid of the newlines. + node.Lparen = token.NoPos + node.Rparen = token.NoPos +} + func (f *fumpter) Position(p token.Pos) token.Position { return f.file.PositionFor(p, false) } @@ -337,11 +441,68 @@ var rxCommentDirective = regexp.MustCompile( `|sys(?:nb)?\b` + `)`) +// rxShebangComment matches a shebang like `//usr/bin/env go run`. +var rxShebangComment = regexp.MustCompile(`^//[^ /].*\bbin/`) + +// commentGroupLooksLikeCode reports whether the lines of a //-style comment +// group parse as Go statements with at least one non-trivial statement. +// A bare identifier path or label is treated as trivial, since prose like +// "// foo" or "// TODO: bar" parses but is not commented-out code. +func commentGroupLooksLikeCode(group *ast.CommentGroup) bool { + src := "package p\nfunc _() {\n" + group.Text() + "}\n" + // AllErrors avoids the parser's panic/recover bailout on too many errors, + // which crashes under tinygo's Wasm target as it lacks recover support. + file, err := parser.ParseFile(token.NewFileSet(), "", src, parser.SkipObjectResolution|parser.AllErrors) + if err != nil { + return false + } + fn, _ := file.Decls[0].(*ast.FuncDecl) + if fn == nil || fn.Body == nil { + return false + } + for _, stmt := range fn.Body.List { + if !isTrivialStmt(stmt) { + return true + } + } + return false +} + +func isTrivialStmt(stmt ast.Stmt) bool { + switch s := stmt.(type) { + case *ast.ExprStmt: + return isIdentPath(s.X) + case *ast.LabeledStmt: + return isTrivialStmt(s.Stmt) + case *ast.EmptyStmt: + return true + } + return false +} + +func isIdentPath(expr ast.Expr) bool { + switch e := expr.(type) { + case *ast.Ident: + return true + case *ast.SelectorExpr: + return isIdentPath(e.X) + } + return false +} + func (f *fumpter) applyPre(c *astutil.Cursor) { f.splitLongLine(c) switch node := c.Node().(type) { case *ast.File: + // Unwrap single-spec var groups before the joining below, + // so an adjacent var line and var group merge in one pass. + for _, decl := range node.Decls { + if decl, ok := decl.(*ast.GenDecl); ok { + f.removeParens(decl) + } + } + // Join contiguous lone var/const/import lines. // Abort if there are empty lines in between, // including a leading comment if it's a directive. @@ -354,6 +515,7 @@ func (f *fumpter) applyPre(c *astutil.Cursor) { continue } lastPos := start.Pos() + merged := false contLoop: for i++; i < len(node.Decls); { cont, ok := node.Decls[i].(*ast.GenDecl) @@ -377,17 +539,25 @@ func (f *fumpter) applyPre(c *astutil.Cursor) { } start.Specs = append(start.Specs, cont.Specs...) + merged = true + end := cont.End() if c := f.inlineComment(cont.End()); c != nil { // don't move an inline comment outside - start.Rparen = c.End() - } else { - // so the code below treats the joined - // decl group as multi-line - start.Rparen = cont.End() + end = c.End() } + // Point Rparen at the last content character, like a real + // ')', so start.End() stays on the content's final line and + // the empty-line separator below is idempotent in one pass. + start.Rparen = end - 1 lastPos = cont.Pos() i++ } + // Re-sort imports in the new group so the output is idempotent. + // Set Lparen so ast.SortImports doesn't skip the merged decl. + if merged && start.Tok == token.IMPORT { + start.Lparen = start.TokPos + token.Pos(len("import")) + ast.SortImports(f.fset, f.astFile) + } } node.Decls = newDecls @@ -399,15 +569,30 @@ func (f *fumpter) applyPre(c *astutil.Cursor) { var lastEnd token.Pos for _, decl := range node.Decls { pos := decl.Pos() - comments := f.commentsBetween(lastEnd, pos) - if len(comments) > 0 { - pos = comments[0].Pos() + // Trailing inline comments on lastEnd's line belong to the + // previous decl and extend its effective end. + effectiveEnd := lastEnd + lastEndLine := f.Line(lastEnd) + for _, cg := range f.commentsBetween(lastEnd, pos) { + if f.Line(cg.Pos()) != lastEndLine { + pos = cg.Pos() + break + } + effectiveEnd = cg.End() } // Note that we want End-1, as End is the character after the node. multi := f.Line(pos) < f.Line(decl.End()-1) - if multi && lastMulti && f.Line(lastEnd)+1 == f.Line(pos) { - f.addNewline(lastEnd) + // A func declaration which fits on a single source line may + // still be printed across multiple lines: go/printer's funcBody + // breaks the body onto its own lines once header+body exceeds + // 100 bytes. Approximate that with the source byte length. + if fn, _ := decl.(*ast.FuncDecl); fn != nil && !multi && fn.Body != nil && + f.Offset(fn.End())-f.Offset(fn.Pos()) > 100 { + multi = true + } + if multi && lastMulti && f.Line(effectiveEnd)+1 == f.Line(pos) { + f.addNewline(effectiveEnd) } lastMulti = multi @@ -418,6 +603,10 @@ func (f *fumpter) applyPre(c *astutil.Cursor) { groupLoop: for _, group := range node.Comments { for _, comment := range group.List { + // Leave shebang lines like `//usr/bin/env go run` alone. + if f.Line(comment.Slash) == 1 && rxShebangComment.MatchString(comment.Text) { + continue groupLoop + } if comment.Text == "//gofumpt:diagnose" || strings.HasPrefix(comment.Text, "//gofumpt:diagnose ") { slc := []string{ "//gofumpt:diagnose", @@ -427,8 +616,8 @@ func (f *fumpter) applyPre(c *astutil.Cursor) { "-lang=" + f.LangVersion, "-modpath=" + f.ModulePath, } - if f.ExtraRules { - slc = append(slc, "-extra") + if s := f.Extra.String(); s != "" { + slc = append(slc, "-extra="+s) } comment.Text = strings.Join(slc, " ") } @@ -447,6 +636,9 @@ func (f *fumpter) applyPre(c *astutil.Cursor) { continue groupLoop } } + if commentGroupLooksLikeCode(group) { + continue groupLoop + } // If none of the comment group's lines look like a // directive or code, add spaces, if needed. for _, comment := range group.List { @@ -488,31 +680,7 @@ func (f *fumpter) applyPre(c *astutil.Cursor) { // Single var declarations shouldn't use parentheses, unless // there's a comment on the grouped declaration. - if node.Tok == token.VAR && len(node.Specs) == 1 && - node.Lparen.IsValid() && node.Doc == nil { - specPos := node.Specs[0].Pos() - specEnd := node.Specs[0].End() - - if len(f.commentsBetween(node.TokPos, specPos)) > 0 { - // If the single spec has a comment on the line above, - // the comment must go before the entire declaration now. - node.TokPos = specPos - } else { - f.removeLines(f.Line(node.TokPos), f.Line(specPos)) - } - if len(f.commentsBetween(specEnd, node.Rparen)) > 0 { - // Leave one newline to not force a comment on the next line to - // become an inline comment. - f.removeLines(f.Line(specEnd)+1, f.Line(node.Rparen)) - } else { - f.removeLines(f.Line(specEnd), f.Line(node.Rparen)) - } - - // Remove the parentheses. go/printer will automatically - // get rid of the newlines. - node.Lparen = token.NoPos - node.Rparen = token.NoPos - } + f.removeParens(node) case *ast.InterfaceType: if len(node.Methods.List) > 0 { @@ -682,8 +850,7 @@ func (f *fumpter) applyPre(c *astutil.Cursor) { f.removeLinesBetween(bodyEnd, node.End()) } - // Merging adjacent fields (e.g. parameters) is disabled by default. - if !f.ExtraRules { + if !f.Extra.GroupParams { break } switch c.Parent().(type) { @@ -694,6 +861,14 @@ func (f *fumpter) applyPre(c *astutil.Cursor) { // Do not merge adjacent fields in structs. } + case *ast.ParenExpr: + // Unwrap any chain of redundant inner parens first, + // since astutil.Apply does not walk replacement nodes. + node.X = ast.Unparen(node.X) + if f.canRemoveParens(node) { + c.Replace(node.X) + } + case *ast.BasicLit: // Octal number literals were introduced in Go 1.13. if goversion.Compare(f.LangVersion, "go1.13") >= 0 { @@ -704,15 +879,21 @@ func (f *fumpter) applyPre(c *astutil.Cursor) { } case *ast.AssignStmt: - // Only remove lines between the assignment token and the first right-hand side expression - f.removeLines(f.Line(node.TokPos), f.Line(node.Rhs[0].Pos())) + // Only remove lines between the assignment token and the right-hand side + // for simple single-value assignments. Skip multi-value assignments and + // binary expressions like long string concatenations, where a line break + // after the assignment token can improve readability. + if len(node.Rhs) == 1 { + if _, ok := node.Rhs[0].(*ast.BinaryExpr); !ok { + f.removeLines(f.Line(node.TokPos), f.Line(node.Rhs[0].Pos())) + } + } case *ast.ReturnStmt: if len(node.Results) > 0 { break } - // Clothing naked returns is disabled by default. - if !f.ExtraRules { + if !f.Extra.ClotheReturns { break } results := f.parentFuncTypes[len(f.parentFuncTypes)-1].Results @@ -822,6 +1003,33 @@ func (f *fumpter) applyPost(c *astutil.Cursor) { f.addNewline(elem1.End()) } } + + // In a multi-line call, if the opening parenthesis is at the end of a + // line, the closing parenthesis should be at the start of a line. + // See https://github.com/mvdan/gofumpt/issues/74. + case *ast.CallExpr: + if !f.Extra.BalanceCalls { + break + } + if len(node.Args) == 0 { + break + } + openLine := f.Line(node.Lparen) + closeLine := f.Line(node.Rparen) + if openLine == closeLine { + break + } + firstLine := f.Line(node.Args[0].Pos()) + lastEnd := node.Args[len(node.Args)-1].End() + if comment := f.inlineComment(lastEnd); comment != nil { + lastEnd = comment.End() + } + lastLine := f.Line(lastEnd) + openAtEOL := openLine != firstLine + closeAtBOL := closeLine != lastLine + if openAtEOL && !closeAtBOL { + f.addNewline(node.Rparen) + } } } @@ -908,6 +1116,46 @@ func (f *fumpter) splitLongLine(c *astutil.Cursor) { } } +// canRemoveParens reports whether the parentheses around node are definitely +// useless and can be safely removed without changing intent. +func (f *fumpter) canRemoveParens(node *ast.ParenExpr) bool { + // Don't drop parens which contain comments, + // as the printer may not place them well without the parens. + if len(f.commentsBetween(node.Lparen, node.Rparen)) > 0 { + return false + } + return !keepParens(node.X, true) +} + +// keepParens reports whether the parentheses directly around expr should be +// kept: around binary, unary, and type expressions for readability and for +// conversions like `(<-chan T)(v)`, but only when outermost; and around an +// expression whose leftmost operand is a composite literal, whose brace would +// otherwise open an if, for, or switch body. +func keepParens(expr ast.Expr, outermost bool) bool { + switch expr := expr.(type) { + case *ast.CompositeLit: + return true + case *ast.CallExpr: + return keepParens(expr.Fun, false) + case *ast.SelectorExpr: + return keepParens(expr.X, false) + case *ast.IndexExpr: + return keepParens(expr.X, false) + case *ast.IndexListExpr: + return keepParens(expr.X, false) + case *ast.SliceExpr: + return keepParens(expr.X, false) + case *ast.TypeAssertExpr: + return keepParens(expr.X, false) + case *ast.BinaryExpr, *ast.UnaryExpr, *ast.StarExpr, + *ast.ChanType, *ast.ArrayType, *ast.MapType, + *ast.FuncType, *ast.InterfaceType, *ast.StructType: + return outermost + } + return false +} + func isComposite(node ast.Node) *ast.CompositeLit { switch node := node.(type) { case *ast.CompositeLit: @@ -1093,16 +1341,28 @@ func (f *fumpter) shouldMergeAdjacentFields(f1, f2 *ast.Field) bool { // Only merge if the types that the syntax nodes represent are equal, // e.g. two *ast.Ident nodes "int" are equal, but the two *ast.Ident nodes - // "string" and "bool" are not. Hence we use go-cmp to do deep comparisons - // while ignoring position information, as it is irrelevant. + // "string" and "bool" are not. We use reflection to quickly discard most cases. + // + // We use an empty [token.FileSet] so that positions are ignored when printing, + // and two syntax nodes with different uses of newlines end up the same. // // Note that we could in theory use go/types here, but in practice gofumpt // needs to be fast, hence it shouldn't rely on expensive typechecking. - opt := cmp.Comparer(func(x, y token.Pos) bool { return true }) - return cmp.Equal(f1.Type, f2.Type, opt) + if reflect.TypeOf(f1.Type) != reflect.TypeOf(f2.Type) { + return false + } + emptyFset := token.NewFileSet() + var b1, b2 bytes.Buffer + if err := format.Node(&b1, emptyFset, f1.Type); err != nil { + return false + } + if err := format.Node(&b2, emptyFset, f2.Type); err != nil { + return false + } + return bytes.Equal(b1.Bytes(), b2.Bytes()) } -var posType = reflect.TypeOf(token.NoPos) +var posType = reflect.TypeFor[token.Pos]() // setPos recursively sets all position fields in the node v to pos. func setPos(v reflect.Value, pos token.Pos) { diff --git a/vendor/mvdan.cc/gofumpt/format/rewrite.go b/vendor/mvdan.cc/gofumpt/format/rewrite.go index ec7a2e5db..47ff5ee7b 100644 --- a/vendor/mvdan.cc/gofumpt/format/rewrite.go +++ b/vendor/mvdan.cc/gofumpt/format/rewrite.go @@ -2,6 +2,13 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// NOTE(gofumpt): the original cmd/gofmt/rewrite.go is mostly stripped here. +// gofumpt drops the -r flag (use `gofmt -r` instead), so the rewrite engine +// (initRewrite, parseExpr, rewriteFile, apply, set, subst) and its +// reflect-helpers (objectPtrNil, scopePtrNil, scopePtrType) are gone. Only +// match/isWildcard remain because simplify.go still uses them to compare +// AST literals when omitting redundant types in composite literals. + package format import ( @@ -14,10 +21,10 @@ import ( // Values/types for special cases. var ( - identType = reflect.TypeOf((*ast.Ident)(nil)) - objectPtrType = reflect.TypeOf((*ast.Object)(nil)) - positionType = reflect.TypeOf(token.NoPos) - callExprType = reflect.TypeOf((*ast.CallExpr)(nil)) + identType = reflect.TypeFor[*ast.Ident]() + objectPtrType = reflect.TypeFor[*ast.Object]() + positionType = reflect.TypeFor[token.Pos]() + callExprType = reflect.TypeFor[*ast.CallExpr]() ) func isWildcard(s string) bool { diff --git a/vendor/mvdan.cc/gofumpt/format/simplify.go b/vendor/mvdan.cc/gofumpt/format/simplify.go index 117646464..363f8d059 100644 --- a/vendor/mvdan.cc/gofumpt/format/simplify.go +++ b/vendor/mvdan.cc/gofumpt/format/simplify.go @@ -2,6 +2,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// NOTE(gofumpt): moved into the format package (from package main) so that +// syntax simplification is exported via the Go API. gofumpt always simplifies, +// so the -s flag was dropped; see also the removal of the -r rewrite logic in +// rewrite.go, which left this file as the sole user of match/isWildcard. + package format import ( diff --git a/vendor/mvdan.cc/gofumpt/gofmt.go b/vendor/mvdan.cc/gofumpt/gofmt.go index 5c922ffbd..e6cd81d90 100644 --- a/vendor/mvdan.cc/gofumpt/gofmt.go +++ b/vendor/mvdan.cc/gofumpt/gofmt.go @@ -7,7 +7,6 @@ package main import ( "bytes" "context" - "errors" "flag" "fmt" "go/ast" @@ -16,23 +15,33 @@ import ( "go/token" "io" "io/fs" + "math/rand" "os" "path/filepath" "regexp" "runtime" "runtime/pprof" + "strconv" "strings" "sync" + // NOTE(gofumpt): x/mod/modfile is used to read each file's go.mod for the + // default -lang and -modpath, and to honor `ignore` directives. "golang.org/x/mod/modfile" "golang.org/x/sync/semaphore" + // NOTE(gofumpt): the format package exposes gofumpt's added rules and + // simplification as a public Go API. diff and go/printer are vendored + // copies frozen at a specific Go version, so gofumpt's output is + // reproducible regardless of the user's Go toolchain. gformat "mvdan.cc/gofumpt/format" "mvdan.cc/gofumpt/internal/govendor/diff" "mvdan.cc/gofumpt/internal/govendor/go/printer" gversion "mvdan.cc/gofumpt/internal/version" ) +// NOTE(gofumpt): regenerate the vendored Go source under internal/govendor, +// then re-format it with the freshly built gofumpt binary. //go:generate go run gen_govendor.go //go:generate go run . -w internal/govendor @@ -46,17 +55,36 @@ var ( // debugging cpuprofile = flag.String("cpuprofile", "", "") - // gofumpt's own flags + // NOTE(gofumpt): gofumpt's own flags. + // -lang sets the target Go language version for version-gated rules + // (e.g. octal literal syntax requires go1.13); defaulted from go.mod. + // -modpath sets the current module path so import grouping can treat + // imports sharing that prefix as third-party; defaulted from go.mod. + // -extra opts in to non-default rules like group_params. + // -version prints the gofumpt build version (set via -ldflags=main.version=). langVersion = flag.String("lang", "", "") modulePath = flag.String("modpath", "", "") - extraRules = flag.Bool("extra", false, "") + extraRules gformat.Extra showVersion = flag.Bool("version", false, "") - // DEPRECATED + // NOTE(gofumpt): -r and -s are kept only to print a friendly error. + // -r was dropped in favor of `gofmt -r`; -s is always on (gofumpt always + // simplifies). rewriteRule = flag.String("r", "", "") simplifyAST = flag.Bool("s", false, "") + + // errors + // NOTE(gofumpt): sentinel used to drive exit code 1 when -d found + // formatting differences. Upstream gofmt does not change its exit code + // on -d; gofumpt's -d acts like `diff` so CI checks can rely on the + // nonzero exit. See reporter.Report below. + errFormattingDiffers = fmt.Errorf("formatting differs from gofumpt's") ) +func init() { flag.Var(&extraRules, "extra", "") } + +// NOTE(gofumpt): set via -ldflags=main.version=... at release time so that +// `gofumpt -version` reports a meaningful string for prebuilt binaries. var version = "" // Keep these in sync with go/format/format.go. @@ -81,10 +109,22 @@ const ( // so this limit may be approximate. var fdSem = make(chan bool, 200) -var ( - fileSet = token.NewFileSet() // per process FileSet - parserMode parser.Mode -) +// NOTE(gofumpt): upstream gofmt declares `rewrite` here for the -r flag; we +// dropped that. +var parserMode parser.Mode + +// newFileSet returns a fresh token.FileSet for parsing a single file. +// +// NOTE(gofumpt): we reserve base 1 with a dummy ten-byte file so that +// token.NoPos+1 cannot be a valid position in any real file added later. +// Some of gofumpt's added rules construct positions via token.NoPos+1; +// without this guard, tests starting from an empty FileSet would silently +// map NoPos+1 to a valid offset and hide bugs like #166. +func newFileSet() *token.FileSet { + fset := token.NewFileSet() + fset.AddFile("gofumpt_base.go", 1, 10) + return fset +} func usage() { fmt.Fprintf(os.Stderr, `usage: gofumpt [flags] [path ...] @@ -94,7 +134,7 @@ func usage() { -e report all errors (not just the first 10 on different lines) -l list files whose formatting differs from gofumpt's -w write result to (source) file instead of stdout - -extra enable extra rules which should be vetted by a human + -extra enable extra rules, e.g. -extra=group_params,clothe_returns -lang str target Go version in the form "go1.X" (default from go.mod) -modpath str Go module path containing the source file (default from go.mod) @@ -102,16 +142,27 @@ func usage() { } func initParserMode() { + // NOTE(gofumpt): always SkipObjectResolution. Upstream only sets it when + // -r is unused (object resolution is needed for the rewrite engine), but + // gofumpt has no -r flag, so we can always skip it for speed. parserMode = parser.ParseComments | parser.SkipObjectResolution if *allErrors { parserMode |= parser.AllErrors } } +// NOTE(gofumpt): split out from upstream's isGoFile. Upstream combined the +// name check with `!f.IsDir()`, but gofumpt's WalkDir callback already +// distinguishes directories, and explicit non-.go arguments are formatted too, +// so the name-only test is needed independently. func isGoFilename(name string) bool { return !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go") } +// NOTE(gofumpt): generated-file detection. gofumpt's added rules are not +// applied to generated Go files unless they are passed explicitly on the +// command line; this avoids churning machine-written code that humans don't +// edit. See processFile below for the `explicit || !isGenerated(file)` gate. var rxCodeGenerated = regexp.MustCompile(`^// Code generated .* DO NOT EDIT\.$`) func isGenerated(file *ast.File) bool { @@ -255,10 +306,9 @@ func (r *reporter) Report(err error) { panic("Report with nil error") } st := r.getState() - switch err.(type) { - case printedDiff: + if err == errFormattingDiffers { st.exitCode = 1 - default: + } else { scanner.PrintError(st.err, err) st.exitCode = 2 } @@ -268,25 +318,23 @@ func (r *reporter) ExitCode() int { return r.getState().exitCode } -type printedDiff struct{} - -func (printedDiff) Error() string { return "printed a diff, exiting with status code 1" } - // If info == nil, we are formatting stdin instead of a file. // If in == nil, the source is the contents of the file with the given filename. +// +// NOTE(gofumpt): the `explicit` parameter (added vs upstream) tracks whether +// this file was named directly on the command line. Explicit files always get +// the gofumpt rules applied (even generated files); walked files do not when +// they look generated. It also forces non-.go explicit args to be formatted. func processFile(filename string, info fs.FileInfo, in io.Reader, r *reporter, explicit bool) error { src, err := readFile(filename, info, in) if err != nil { return err } - fileSet := token.NewFileSet() - fragmentOk := false - if info == nil { - // If we are formatting stdin, we accept a program fragment in lieu of a - // complete source file. - fragmentOk = true - } + fileSet := newFileSet() + // If we are formatting stdin, we accept a program fragment in lieu of a + // complete source file. + fragmentOk := info == nil file, sourceAdj, indentAdj, err := parse(fileSet, filename, src, fragmentOk) if err != nil { return err @@ -294,7 +342,12 @@ func processFile(filename string, info fs.FileInfo, in io.Reader, r *reporter, e ast.SortImports(fileSet, file) - // Apply gofumpt's changes before we print the code in gofumpt's format. + // NOTE(gofumpt): from here until the call to format() below is the + // gofumpt-specific work upstream gofmt does not do: resolve -lang and + // -modpath defaults from the file's containing go.mod, then run + // gformat.File to apply the added rules (and simplification, which + // gofumpt always runs in lieu of the dropped -s flag). Apply gofumpt's + // changes before we print the code in gofumpt's format. // If either -lang or -modpath aren't set, fetch them from go.mod. lang := *langVersion @@ -314,8 +367,8 @@ func processFile(filename string, info fs.FileInfo, in io.Reader, r *reporter, e lang = "go" + mod.file.Go.Version } } - if modpath == "" { - modpath = mod.file.Module.Mod.Path + if m := mod.file.Module; m != nil && modpath == "" { + modpath = m.Mod.Path } } } @@ -327,7 +380,7 @@ func processFile(filename string, info fs.FileInfo, in io.Reader, r *reporter, e gformat.File(fileSet, file, gformat.Options{ LangVersion: lang, ModulePath: modpath, - ExtraRules: *extraRules, + Extra: extraRules, }) } @@ -345,21 +398,9 @@ func processFile(filename string, info fs.FileInfo, in io.Reader, r *reporter, e if info == nil { panic("-w should not have been allowed with stdin") } - // make a temporary backup before overwriting original + perm := info.Mode().Perm() - bakname, err := backupFile(filename+".", src, perm) - if err != nil { - return err - } - fdSem <- true - err = os.WriteFile(filename, res, perm) - <-fdSem - if err != nil { - os.Rename(bakname, filename) - return err - } - err = os.Remove(bakname) - if err != nil { + if err := writeFile(filename, src, res, perm, info.Size()); err != nil { return err } } @@ -367,7 +408,7 @@ func processFile(filename string, info fs.FileInfo, in io.Reader, r *reporter, e newName := filepath.ToSlash(filename) oldName := newName + ".orig" r.Write(diff.Diff(oldName, src, newName, res)) - return printedDiff{} + return errFormattingDiffers } } @@ -459,13 +500,12 @@ func main() { } func gofmtMain(s *sequencer) { - // Ensure our parsed files never start with base 1, - // to ensure that using token.NoPos+1 will panic. - fileSet.AddFile("gofumpt_base.go", 1, 10) - flag.Usage = usage flag.Parse() + // NOTE(gofumpt): friendly handling of the dropped -s and -r flags so users + // migrating from gofmt get a clear message rather than "flag provided but + // not defined". -s is always on; -r is delegated to `gofmt -r`. if *simplifyAST { fmt.Fprintf(os.Stderr, "warning: -s is deprecated as it is always enabled\n") } @@ -474,7 +514,9 @@ func gofmtMain(s *sequencer) { os.Exit(2) } - // Print the gofumpt version if the user asks for it. + // NOTE(gofumpt): print the gofumpt version if the user asks for it. + // -version dumps the build version and any embedded build-info fields + // (see internal/version), useful for bug reports and `//gofumpt:diagnose`. if *showVersion { fmt.Println(gversion.String(version)) return @@ -510,6 +552,14 @@ func gofmtMain(s *sequencer) { return } + // NOTE(gofumpt): the argument-walking loop below is rewritten vs upstream. + // Upstream branched on os.Stat (file vs dir); gofumpt always uses + // filepath.WalkDir and tracks `explicit := path == arg` so that: + // - explicit non-.go and explicit generated files are still formatted; + // - vendor/testdata directories and go.mod `ignore` entries are skipped + // during walks but honored when named directly (so `gofumpt -w vendor` + // still works); + // - the explicit bit propagates into processFile to gate gofumpt rules. for _, arg := range args { // Walk each given argument as a directory tree. // If the argument is not a directory, it's always formatted as a Go file. @@ -544,6 +594,10 @@ func gofmtMain(s *sequencer) { } } +// NOTE(gofumpt): everything from here to the end of the file is gofumpt-only. +// shouldIgnore implements skipping `vendor` and `testdata` directories during +// walks, plus honoring Go 1.25's `ignore` directives in go.mod. These are +// skipped during recursive walks but still formatted when named explicitly. func shouldIgnore(path string) bool { switch filepath.Base(path) { case "vendor", "testdata": @@ -594,6 +648,13 @@ func matchIgnore(ignore, relPath string) bool { return strings.HasSuffix(relPath, ignore) } +// NOTE(gofumpt): module loading is gofumpt-only. The go.mod is consulted for +// the default -lang (Go language version, used by version-gated rules), the +// default -modpath (so imports sharing the module prefix are grouped as +// third-party), and the `ignore` directives consumed by shouldIgnore above. +// Results are cached per directory; loadModule walks up to find an enclosing +// go.mod just like the go command would. +// // A nil entry means the directory is not part of a Go module, // or a go.mod file was found but it's invalid. // A non-nil entry means this directory, or a parent, is in a valid Go module. @@ -614,7 +675,10 @@ func loadModule(dir string) *cachedModule { fdSem <- true data, err := os.ReadFile(path) <-fdSem - if errors.Is(err, fs.ErrNotExist) { + if err != nil { + // If the file is missing, or we can't read this directory at all + // (e.g. permission denied on a directory listed in `ignore`), keep + // walking up to find an enclosing go.mod. parent := filepath.Dir(dir) if parent == "." { panic("loadModule was not given an absolute path?") @@ -624,9 +688,6 @@ func loadModule(dir string) *cachedModule { } return loadModule(parent) // try the parent directory } - if err != nil { - return nil // some other file reading error - } file, err := modfile.Parse(filepath.Join(dir, "go.mod"), data, nil) if err != nil { return nil // invalid go.mod file @@ -663,32 +724,111 @@ func fileWeight(path string, info fs.FileInfo) int64 { return info.Size() } -const chmodSupported = runtime.GOOS != "windows" +// writeFile updates a file with the new formatted data. +func writeFile(filename string, orig, formatted []byte, perm fs.FileMode, size int64) error { + // Make a temporary backup file before rewriting the original file. + bakname, err := backupFile(filename, orig, perm) + if err != nil { + return err + } + + fdSem <- true + defer func() { <-fdSem }() + + fout, err := os.OpenFile(filename, os.O_WRONLY, perm) + if err != nil { + // We couldn't even open the file, so it should + // not have changed. + os.Remove(bakname) + return err + } + defer fout.Close() // for error paths + + restoreFail := func(err error) { + fmt.Fprintf(os.Stderr, "gofumpt: %s: error restoring file to original: %v; backup in %s\n", filename, err, bakname) + } + + n, err := fout.Write(formatted) + if err == nil && int64(n) < size { + err = fout.Truncate(int64(n)) + } + + if err != nil { + // Rewriting the file failed. + + if n == 0 { + // Original file unchanged. + os.Remove(bakname) + return err + } + + // Try to restore the original contents. + + no, erro := fout.WriteAt(orig, 0) + if erro != nil { + // That failed too. + restoreFail(erro) + return err + } + + if no < n { + // Original file is shorter. Truncate. + if erro = fout.Truncate(int64(no)); erro != nil { + restoreFail(erro) + return err + } + } + + if erro := fout.Close(); erro != nil { + restoreFail(erro) + return err + } + + // Original contents restored. + os.Remove(bakname) + return err + } + + if err := fout.Close(); err != nil { + restoreFail(err) + return err + } + + // File updated. + os.Remove(bakname) + return nil +} // backupFile writes data to a new file named filename with permissions perm, -// with randomly chosen such that the file name is unique. backupFile returns // the chosen file name. func backupFile(filename string, data []byte, perm fs.FileMode) (string, error) { fdSem <- true defer func() { <-fdSem }() - // create backup file - f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)) - if err != nil { - return "", err + nextRandom := func() string { + return strconv.Itoa(rand.Int()) } - bakname := f.Name() - if chmodSupported { - err = f.Chmod(perm) - if err != nil { - f.Close() - os.Remove(bakname) - return bakname, err + + dir, base := filepath.Split(filename) + var ( + bakname string + f *os.File + ) + for { + bakname = filepath.Join(dir, base+"."+nextRandom()) + var err error + f, err = os.OpenFile(bakname, os.O_RDWR|os.O_CREATE|os.O_EXCL, perm) + if err == nil { + break + } + if !os.IsExist(err) { + return "", err } } // write data to backup file - _, err = f.Write(data) + _, err := f.Write(data) if err1 := f.Close(); err == nil { err = err1 } diff --git a/vendor/mvdan.cc/gofumpt/internal.go b/vendor/mvdan.cc/gofumpt/internal.go index 2f7e51420..3c9f56037 100644 --- a/vendor/mvdan.cc/gofumpt/internal.go +++ b/vendor/mvdan.cc/gofumpt/internal.go @@ -16,6 +16,9 @@ import ( "go/token" "strings" + // NOTE(gofumpt): use a vendored copy of go/printer (and go/doc/comment) + // frozen at a specific Go version. This way installing a given gofumpt + // release produces the same output regardless of the user's Go toolchain. "mvdan.cc/gofumpt/internal/govendor/go/printer" ) diff --git a/vendor/mvdan.cc/gofumpt/internal/govendor/go/format/internal.go b/vendor/mvdan.cc/gofumpt/internal/govendor/go/format/internal.go index 383655f16..df0358714 100644 --- a/vendor/mvdan.cc/gofumpt/internal/govendor/go/format/internal.go +++ b/vendor/mvdan.cc/gofumpt/internal/govendor/go/format/internal.go @@ -33,7 +33,7 @@ func parse(fset *token.FileSet, filename string, src []byte, fragmentOk bool) ( // package line and source fragments are ok, fall through to // try as a source fragment. Stop and return on any other error. if err == nil || !fragmentOk || !strings.Contains(err.Error(), "expected 'package'") { - return file, sourceAdj, indentAdj, err + return } // If this is a declaration list, make it a source file @@ -49,13 +49,13 @@ func parse(fset *token.FileSet, filename string, src []byte, fragmentOk bool) ( src = src[indent+len("package p\n"):] return bytes.TrimSpace(src) } - return file, sourceAdj, indentAdj, err + return } // If the error is that the source file didn't begin with a // declaration, fall through to try as a statement list. // Stop and return on any other error. if !strings.Contains(err.Error(), "expected declaration") { - return file, sourceAdj, indentAdj, err + return } // If this is a statement list, make it a source file @@ -86,7 +86,7 @@ func parse(fset *token.FileSet, filename string, src []byte, fragmentOk bool) ( } // Succeeded, or out of options. - return file, sourceAdj, indentAdj, err + return } // format formats the given package file originally obtained from src diff --git a/vendor/mvdan.cc/gofumpt/internal/govendor/go/printer/nodes.go b/vendor/mvdan.cc/gofumpt/internal/govendor/go/printer/nodes.go index df3b7250e..c7d2b0f14 100644 --- a/vendor/mvdan.cc/gofumpt/internal/govendor/go/printer/nodes.go +++ b/vendor/mvdan.cc/gofumpt/internal/govendor/go/printer/nodes.go @@ -57,7 +57,7 @@ func (p *printer) linebreak(line, min int, ws whiteSpace, newSection bool) (nbre p.print(newline) } } - return nbreaks + return } // setComment sets g as the next comment if g != nil and if node comments @@ -465,7 +465,7 @@ func identListSize(list []*ast.Ident, maxSize int) (size int) { break } } - return size + return } func (p *printer) isOneLineFieldList(list []*ast.Field) bool { @@ -693,7 +693,7 @@ func walkBinary(e *ast.BinaryExpr) (has4, has5 bool, maxProblem int) { maxProblem = max(maxProblem, 4) } } - return has4, has5, maxProblem + return } func cutoff(e *ast.BinaryExpr, depth int) int { @@ -1818,14 +1818,14 @@ func (p *printer) nodeSize(n ast.Node, maxSize int) (size int) { cfg := Config{Mode: RawFormat} var counter sizeCounter if err := cfg.fprint(&counter, p.fset, n, p.nodeSizes); err != nil { - return size + return } if counter.size <= maxSize && !counter.hasNewline { // n fits in a single line size = counter.size p.nodeSizes[n] = size } - return size + return } // numLines returns the number of lines spanned by node n in the original source. @@ -1959,7 +1959,7 @@ func declToken(decl ast.Decl) (tok token.Token) { case *ast.FuncDecl: tok = token.FUNC } - return tok + return } func (p *printer) declList(list []ast.Decl) { diff --git a/vendor/mvdan.cc/gofumpt/internal/govendor/go/printer/printer.go b/vendor/mvdan.cc/gofumpt/internal/govendor/go/printer/printer.go index 00713309b..a6c74c729 100644 --- a/vendor/mvdan.cc/gofumpt/internal/govendor/go/printer/printer.go +++ b/vendor/mvdan.cc/gofumpt/internal/govendor/go/printer/printer.go @@ -715,7 +715,7 @@ func (p *printer) writeCommentSuffix(needsLinebreak bool) (wroteNewline, dropped wroteNewline = true } - return wroteNewline, droppedFF + return } // containsLinebreak reports whether the whitespace buffer contains any line breaks. @@ -809,7 +809,7 @@ func (p *printer) intersperseComments(next token.Position, tok token.Token) (wro // no comment was written - we should never reach here since // intersperseComments should not be called in that case p.internalError("intersperseComments called without pending comments") - return wroteNewline, droppedFF + return } // writeWhitespace writes the first n whitespace entries. @@ -878,7 +878,7 @@ func mayCombine(prev token.Token, next byte) (b bool) { case token.AND: b = next == '&' || next == '^' // && or &^ } - return b + return } func (p *printer) setPos(pos token.Pos) { @@ -1041,7 +1041,7 @@ func (p *printer) flush(next token.Position, tok token.Token) (wroteNewline, dro // otherwise, write any leftover whitespace p.writeWhitespace(len(p.wsbuf)) } - return wroteNewline, droppedFF + return } // getDoc returns the ast.CommentGroup associated with n, if any. @@ -1269,7 +1269,7 @@ func (p *trimmer) Write(data []byte) (n int, err error) { panic("unreachable") } if err != nil { - return n, err + return } } n = len(data) @@ -1280,7 +1280,7 @@ func (p *trimmer) Write(data []byte) (n int, err error) { p.resetSpace() } - return n, err + return } // ---------------------------------------------------------------------------- @@ -1361,7 +1361,7 @@ func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node any, nodeS p := newPrinter(cfg, fset, nodeSizes) defer p.free() if err = p.printNode(node); err != nil { - return err + return } // print outstanding comments p.impliedSemi = false // EOF acts like a newline @@ -1397,7 +1397,7 @@ func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node any, nodeS // write printer result via tabwriter/trimmer to output if _, err = output.Write(p.output); err != nil { - return err + return } // flush tabwriter, if any @@ -1405,7 +1405,7 @@ func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node any, nodeS err = tw.Flush() } - return err + return } // A CommentedNode bundles an AST node and corresponding comments.