mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
Show pull requests against branches (#2781)
- **PR Description** If the user has `gh` installed and is logged in (`gh auth login`), lazygit shows GitHub PR icons next to the names of branches that have an associated PR, colored by the PR's status (green=open, red=closed, purple=merged). Selecting a branch and pressing `shift-G` opens the PR in the browser.
This commit is contained in:
commit
a33f83cf76
|
|
@ -228,6 +228,10 @@ If you press `shift+w` on a commit (or branch/ref) a menu will open that allows
|
|||
|
||||

|
||||
|
||||
### Show GitHub pull requests
|
||||
|
||||
In the branches panel, lazygit can show which of your branches have an associated GitHub pull request by showing a GitHub icon next to the branch name; its color shows the state of the PR (open, merged, etc.). For those that have one, you can press `shift-G` to open the PR in the browser. There is no configuration needed to enable this, but it requires the [`gh`](https://cli.github.com/) tool to be installed, and you need to do `gh auth login` once to allow lazygit to access GitHub.
|
||||
|
||||
## Tutorials
|
||||
|
||||
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@ keybinding:
|
|||
branches:
|
||||
createPullRequest: o
|
||||
viewPullRequestOptions: O
|
||||
openPullRequestInBrowser: G
|
||||
copyPullRequestURL: <c-y>
|
||||
checkoutBranchByName: c
|
||||
forceCheckoutBranch: F
|
||||
|
|
@ -749,6 +750,7 @@ keybinding:
|
|||
copyCommitAttributeToClipboard: "y"
|
||||
openLogMenu: <c-l>
|
||||
openInBrowser: o
|
||||
openPullRequestInBrowser: G
|
||||
viewBisectOptions: b
|
||||
startInteractiveRebase: i
|
||||
selectCommitsOfCurrentBranch: '*'
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||
| `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. |
|
||||
| `` T `` | Tag commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` <c-l> `` | View log options | 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 | |
|
||||
| `` <space> `` | Checkout | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
|
|
@ -180,6 +181,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||
| `` 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.<br><br>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). |
|
||||
| `` o `` | Create pull request | |
|
||||
| `` O `` | View create pull request options | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <c-y> `` | Copy pull request URL to clipboard | |
|
||||
| `` c `` | Checkout by name | Checkout by name. In the input box you can enter '-' to switch to the previous branch. |
|
||||
| `` - `` | Checkout previous branch | |
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
|
|||
| `` t `` | リバート | 選択したコミットの変更を逆に適用する、リバートコミットを作成します。 |
|
||||
| `` T `` | コミットにタグを付ける | 選択したコミットを指すタグを新規作成します。タグ名とオプションの説明を入力するよう促されます。 |
|
||||
| `` <c-l> `` | ログオプションを表示 | コミットログのオプションを表示します(例:並び順の変更、Gitグラフの非表示、Gitグラフ全体の表示)。 |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したコミットをデタッチドヘッド(特定のブランチに属さない状態)としてチェックアウトします。 |
|
||||
| `` y `` | コミット属性をクリップボードにコピー | コミット属性をクリップボードにコピーします(例:ハッシュ、URL、差分、メッセージ、作者)。 |
|
||||
| `` o `` | ブラウザでコミットを開く | |
|
||||
|
|
@ -379,6 +380,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
|
|||
| `` N `` | コミットを新しいブランチに移動 | 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.<br><br>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). |
|
||||
| `` o `` | プルリクエストを作成 | |
|
||||
| `` O `` | プルリクエスト作成オプションを表示 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <c-y> `` | プルリクエストURLをクリップボードにコピー | |
|
||||
| `` c `` | 名前でチェックアウト | 名前でチェックアウトします。入力ボックスに「-」を入力すると、最後のブランチをチェックアウトすることができます。 |
|
||||
| `` - `` | 直前のブランチにチェックアウト | |
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||
| `` 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.<br><br>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). |
|
||||
| `` o `` | 풀 리퀘스트 생성 | |
|
||||
| `` O `` | 풀 리퀘스트 생성 옵션 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <c-y> `` | 풀 리퀘스트 URL을 클립보드에 복사 | |
|
||||
| `` c `` | 이름으로 체크아웃 | Checkout by name. In the input box you can enter '-' to switch to the previous branch. |
|
||||
| `` - `` | Checkout previous branch | |
|
||||
|
|
@ -318,6 +319,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||
| `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. |
|
||||
| `` T `` | Tag commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` <c-l> `` | 로그 메뉴 열기 | 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 | |
|
||||
| `` <space> `` | 체크아웃 | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | 브라우저에서 커밋 열기 | |
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||
| `` 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.<br><br>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). |
|
||||
| `` o `` | Maak een pull-request | |
|
||||
| `` O `` | Bekijk opties voor pull-aanvraag | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <c-y> `` | Kopieer de URL van het pull-verzoek naar het klembord | |
|
||||
| `` c `` | Uitchecken bij naam | Checkout by name. In the input box you can enter '-' to switch to the previous branch. |
|
||||
| `` - `` | Checkout previous branch | |
|
||||
|
|
@ -178,6 +179,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||
| `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. |
|
||||
| `` T `` | Tag commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` <c-l> `` | View log options | 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 | |
|
||||
| `` <space> `` | Uitchecken | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
|
|||
| `` t `` | Cofnij | Utwórz commit cofający dla wybranego commita, który stosuje zmiany wybranego commita w odwrotnej kolejności. |
|
||||
| `` T `` | Otaguj commit | Utwórz nowy tag wskazujący na wybrany commit. Zostaniesz poproszony o wprowadzenie nazwy tagu i opcjonalnego opisu. |
|
||||
| `` <c-l> `` | Zobacz opcje logów | Zobacz opcje dla logów commitów, np. zmiana kolejności sortowania, ukrywanie grafu gita, pokazywanie całego grafu gita. |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <space> `` | Przełącz | Przełącz wybrany commit jako odłączoną HEAD. |
|
||||
| `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). |
|
||||
| `` o `` | Otwórz commit w przeglądarce | |
|
||||
|
|
@ -146,6 +147,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
|
|||
| `` 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.<br><br>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). |
|
||||
| `` o `` | Utwórz żądanie ściągnięcia | |
|
||||
| `` O `` | Zobacz opcje tworzenia pull requesta | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <c-y> `` | Kopiuj adres URL żądania ściągnięcia do schowka | |
|
||||
| `` c `` | Przełącz według nazwy | Przełącz według nazwy. W polu wprowadzania możesz wpisać '-' aby przełączyć się na ostatnią gałąź. |
|
||||
| `` - `` | Checkout previous branch | |
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||
| `` N `` | Mover commits para uma nova 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.<br><br>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). |
|
||||
| `` o `` | Create pull request | |
|
||||
| `` O `` | View create pull request options | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <c-y> `` | Copiar URL do pull request para área de transferência | |
|
||||
| `` c `` | Checar por nome | Checar por nome. Na caixa de entrada você pode inserir '-' para trocar para a última branch |
|
||||
| `` - `` | Checkout da branch anterior | |
|
||||
|
|
@ -182,6 +183,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||
| `` t `` | Reverter | Crie um commit reverter para o commit selecionado, que aplica as alterações do commit selecionado em reverso. |
|
||||
| `` T `` | Tag commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` <c-l> `` | View log options | 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 | |
|
||||
| `` <space> `` | Verificar | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ _Связки клавиш_
|
|||
| `` t `` | Revert | 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. |
|
||||
| `` <c-l> `` | Открыть меню журнала | 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 | |
|
||||
| `` <space> `` | Переключить | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Открыть коммит в браузере | |
|
||||
|
|
@ -214,6 +215,7 @@ _Связки клавиш_
|
|||
| `` 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.<br><br>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). |
|
||||
| `` o `` | Создать запрос на принятие изменений | |
|
||||
| `` O `` | Создать параметры запроса принятие изменений | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <c-y> `` | Скопировать URL запроса на принятие изменений в буфер обмена | |
|
||||
| `` c `` | Переключить по названию | Checkout by name. In the input box you can enter '-' to switch to the previous branch. |
|
||||
| `` - `` | Checkout previous branch | |
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
|||
| `` t `` | 撤销(Revert) | 为所选提交创建还原提交,这会反向应用所选提交的更改。 |
|
||||
| `` T `` | 标签提交 | 创建一个新标签指向所选提交。您可以在弹窗中输入标签名称和描述(可选)。 |
|
||||
| `` <c-l> `` | 打开日志菜单 | 查看提交日志的选项,例如更改排序顺序、隐藏 git graph、显示整个 git graph。 |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <space> `` | 检出 | 检出所选择的提交作为分离HEAD。 |
|
||||
| `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(如hash、URL、diff、消息、作者)。 |
|
||||
| `` o `` | 在浏览器中打开提交 | |
|
||||
|
|
@ -229,6 +230,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
|||
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
|
||||
| `` o `` | 创建拉取请求 | |
|
||||
| `` O `` | 创建拉取请求选项 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <c-y> `` | 复制拉取请求 URL 到剪贴板 | |
|
||||
| `` c `` | 按名称检出 | 按名称检出。在输入框中,您可以输入'-' 来切换到最后一个分支。 |
|
||||
| `` - `` | 签出上一个分支 | |
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
|
|||
| `` 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. |
|
||||
| `` <c-l> `` | 開啟記錄選單 | 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 | |
|
||||
| `` <space> `` | 檢出 | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | 在瀏覽器中開啟提交 | |
|
||||
|
|
@ -289,6 +290,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
|
|||
| `` 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.<br><br>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). |
|
||||
| `` o `` | 建立拉取請求 | |
|
||||
| `` O `` | 建立拉取請求選項 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <c-y> `` | 複製拉取請求的 URL 到剪貼板 | |
|
||||
| `` c `` | 根據名稱檢出 | Checkout by name. In the input box you can enter '-' to switch to the previous branch. |
|
||||
| `` - `` | Checkout previous branch | |
|
||||
|
|
|
|||
8
go.mod
8
go.mod
|
|
@ -10,6 +10,7 @@ require (
|
|||
github.com/adrg/xdg v0.4.0
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/aybabtme/humanlog v0.4.1
|
||||
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/v2 v2.13.8
|
||||
|
|
@ -35,7 +36,7 @@ require (
|
|||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
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-20210125001918-ca9a967f8778
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.42.0
|
||||
|
|
@ -46,6 +47,7 @@ require (
|
|||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/cli/safeexec v1.0.1 // 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
|
||||
|
|
@ -57,8 +59,8 @@ require (
|
|||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
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/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect
|
||||
|
|
|
|||
20
go.sum
20
go.sum
|
|
@ -11,6 +11,10 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
|
|||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
|
||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/cli/go-gh/v2 v2.13.0 h1:jEHZu/VPVoIJkciK3pzZd3rbT8J90swsK5Ui4ewH1ys=
|
||||
github.com/cli/go-gh/v2 v2.13.0/go.mod h1:Us/NbQ8VNM0fdaILgoXSz6PKkV5PWaEzkJdc9vR2geM=
|
||||
github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
|
||||
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
|
|
@ -72,13 +76,14 @@ 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=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
|
||||
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
|
|
@ -125,8 +130,9 @@ github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xz
|
|||
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-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
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=
|
||||
|
|
@ -152,13 +158,13 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
|
|
|||
|
|
@ -16,27 +16,29 @@ import (
|
|||
|
||||
// GitCommand is our main git interface
|
||||
type GitCommand struct {
|
||||
Blame *git_commands.BlameCommands
|
||||
Branch *git_commands.BranchCommands
|
||||
Commit *git_commands.CommitCommands
|
||||
Config *git_commands.ConfigCommands
|
||||
Custom *git_commands.CustomCommands
|
||||
Diff *git_commands.DiffCommands
|
||||
File *git_commands.FileCommands
|
||||
Flow *git_commands.FlowCommands
|
||||
Patch *git_commands.PatchCommands
|
||||
Rebase *git_commands.RebaseCommands
|
||||
Remote *git_commands.RemoteCommands
|
||||
Stash *git_commands.StashCommands
|
||||
Status *git_commands.StatusCommands
|
||||
Submodule *git_commands.SubmoduleCommands
|
||||
Sync *git_commands.SyncCommands
|
||||
Tag *git_commands.TagCommands
|
||||
WorkingTree *git_commands.WorkingTreeCommands
|
||||
Bisect *git_commands.BisectCommands
|
||||
Worktree *git_commands.WorktreeCommands
|
||||
Version *git_commands.GitVersion
|
||||
RepoPaths *git_commands.RepoPaths
|
||||
Blame *git_commands.BlameCommands
|
||||
Branch *git_commands.BranchCommands
|
||||
Commit *git_commands.CommitCommands
|
||||
Config *git_commands.ConfigCommands
|
||||
Custom *git_commands.CustomCommands
|
||||
Diff *git_commands.DiffCommands
|
||||
File *git_commands.FileCommands
|
||||
Flow *git_commands.FlowCommands
|
||||
Patch *git_commands.PatchCommands
|
||||
Rebase *git_commands.RebaseCommands
|
||||
Remote *git_commands.RemoteCommands
|
||||
Stash *git_commands.StashCommands
|
||||
Status *git_commands.StatusCommands
|
||||
Submodule *git_commands.SubmoduleCommands
|
||||
Sync *git_commands.SyncCommands
|
||||
Tag *git_commands.TagCommands
|
||||
WorkingTree *git_commands.WorkingTreeCommands
|
||||
Bisect *git_commands.BisectCommands
|
||||
Worktree *git_commands.WorktreeCommands
|
||||
Version *git_commands.GitVersion
|
||||
RepoPaths *git_commands.RepoPaths
|
||||
GitHub *git_commands.GitHubCommands
|
||||
HostingService *git_commands.HostingService
|
||||
|
||||
Loaders Loaders
|
||||
}
|
||||
|
|
@ -122,6 +124,8 @@ func NewGitCommandAux(
|
|||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
|
||||
blameCommands := git_commands.NewBlameCommands(gitCommon)
|
||||
gitHubCommands := git_commands.NewGitHubCommands(gitCommon)
|
||||
hostingServiceCommands := git_commands.NewHostingServiceCommand(gitCommon)
|
||||
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||
|
|
@ -133,26 +137,28 @@ func NewGitCommandAux(
|
|||
tagLoader := git_commands.NewTagLoader(cmn, cmd)
|
||||
|
||||
return &GitCommand{
|
||||
Blame: blameCommands,
|
||||
Branch: branchCommands,
|
||||
Commit: commitCommands,
|
||||
Config: configCommands,
|
||||
Custom: customCommands,
|
||||
Diff: diffCommands,
|
||||
File: fileCommands,
|
||||
Flow: flowCommands,
|
||||
Patch: patchCommands,
|
||||
Rebase: rebaseCommands,
|
||||
Remote: remoteCommands,
|
||||
Stash: stashCommands,
|
||||
Status: statusCommands,
|
||||
Submodule: submoduleCommands,
|
||||
Sync: syncCommands,
|
||||
Tag: tagCommands,
|
||||
Bisect: bisectCommands,
|
||||
WorkingTree: workingTreeCommands,
|
||||
Worktree: worktreeCommands,
|
||||
Version: version,
|
||||
Blame: blameCommands,
|
||||
Branch: branchCommands,
|
||||
Commit: commitCommands,
|
||||
Config: configCommands,
|
||||
Custom: customCommands,
|
||||
Diff: diffCommands,
|
||||
File: fileCommands,
|
||||
Flow: flowCommands,
|
||||
Patch: patchCommands,
|
||||
Rebase: rebaseCommands,
|
||||
Remote: remoteCommands,
|
||||
Stash: stashCommands,
|
||||
Status: statusCommands,
|
||||
Submodule: submoduleCommands,
|
||||
Sync: syncCommands,
|
||||
Tag: tagCommands,
|
||||
Bisect: bisectCommands,
|
||||
WorkingTree: workingTreeCommands,
|
||||
Worktree: worktreeCommands,
|
||||
Version: version,
|
||||
GitHub: gitHubCommands,
|
||||
HostingService: hostingServiceCommands,
|
||||
Loaders: Loaders{
|
||||
BranchLoader: branchLoader,
|
||||
CommitFileLoader: commitFileLoader,
|
||||
|
|
|
|||
380
pkg/commands/git_commands/github.go
Normal file
380
pkg/commands/git_commands/github.go
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
package git_commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/go-gh/v2/pkg/auth"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type GitHubCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
|
||||
func NewGitHubCommands(gitCommon *GitCommon) *GitHubCommands {
|
||||
return &GitHubCommands{
|
||||
GitCommon: gitCommon,
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/cli/cli/issues/2300
|
||||
func (self *GitHubCommands) ConfiguredBaseRemoteName() string {
|
||||
// TODO: we only support the (common) case where the value of the config is "base", meaning that
|
||||
// the remote's URL determines the GitHub repo. Since `gh repo set-default` on the command line
|
||||
// sets the config this way, it's probably good enough in practice, but for completeness it
|
||||
// would be nice to also support the case where the config value is a full remote name (e.g.
|
||||
// "jesseduffield/lazygit").
|
||||
|
||||
cmdArgs := NewGitCmd("config").
|
||||
Arg("--local", "--get-regexp", `remote\..*\.gh-resolved`).
|
||||
ToArgv()
|
||||
|
||||
output, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
regex := regexp.MustCompile(`remote\.(.+)\.gh-resolved`)
|
||||
matches := regex.FindStringSubmatch(output)
|
||||
if len(matches) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
func (self *GitHubCommands) SetConfiguredBaseRemoteName(remoteName string) error {
|
||||
cmdArgs := NewGitCmd("config").
|
||||
Arg("--local", "--add", fmt.Sprintf("remote.%s.gh-resolved", remoteName), "base").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).DontLog().Run()
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Data RepositoryQuery `json:"data"`
|
||||
}
|
||||
|
||||
type RepositoryQuery struct {
|
||||
Repository map[string]PullRequest `json:"repository"`
|
||||
}
|
||||
|
||||
type PullRequest struct {
|
||||
Edges []PullRequestEdge `json:"edges"`
|
||||
}
|
||||
|
||||
type PullRequestEdge struct {
|
||||
Node PullRequestNode `json:"node"`
|
||||
}
|
||||
|
||||
type PullRequestNode struct {
|
||||
Title string `json:"title"`
|
||||
HeadRefName string `json:"headRefName"`
|
||||
Number int `json:"number"`
|
||||
Url string `json:"url"`
|
||||
HeadRepositoryOwner GithubRepositoryOwner `json:"headRepositoryOwner"`
|
||||
State string `json:"state"`
|
||||
IsDraft bool `json:"isDraft"`
|
||||
}
|
||||
|
||||
type GithubRepositoryOwner struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
type graphQLRequest struct {
|
||||
Query string `json:"query"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
|
||||
func fetchPullRequestsQuery(branches []string, owner string, repo string) (string, map[string]string) {
|
||||
variables := make(map[string]string, len(branches)+2)
|
||||
variables["owner"] = owner
|
||||
variables["repo"] = repo
|
||||
varDecls := make([]string, 0, len(branches)+2)
|
||||
varDecls = append(varDecls, "$owner: String!", "$repo: String!")
|
||||
queries := make([]string, 0, len(branches))
|
||||
for i, branch := range branches {
|
||||
// We're making a sub-query per branch, and arbitrarily labelling each subquery
|
||||
// as a1, a2, etc.
|
||||
fieldName := fmt.Sprintf("a%d", i+1)
|
||||
varName := fmt.Sprintf("branch%d", i+1)
|
||||
variables[varName] = branch
|
||||
varDecls = append(varDecls, fmt.Sprintf("$%s: String!", varName))
|
||||
// We fetch a few PRs per branch name because multiple forks may have PRs
|
||||
// with the same head ref name. The mapping logic filters by owner later.
|
||||
queries = append(queries, fmt.Sprintf(`%s: pullRequests(first: 5, headRefName: $%s, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
title
|
||||
headRefName
|
||||
state
|
||||
number
|
||||
url
|
||||
isDraft
|
||||
headRepositoryOwner {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, fieldName, varName))
|
||||
}
|
||||
|
||||
queryString := fmt.Sprintf(`query(%s) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
%s
|
||||
}
|
||||
}`, strings.Join(varDecls, ", "), strings.Join(queries, "\n"))
|
||||
|
||||
return queryString, variables
|
||||
}
|
||||
|
||||
func (self *GitHubCommands) GetAuthToken() string {
|
||||
defaultHost, _ := auth.DefaultHost()
|
||||
token, _ := auth.TokenForHost(defaultHost)
|
||||
return token
|
||||
}
|
||||
|
||||
// FetchRecentPRs fetches recent pull requests using GraphQL.
|
||||
func (self *GitHubCommands) FetchRecentPRs(branches []string, baseRemote *models.Remote, token string) ([]*models.GithubPullRequest, error) {
|
||||
repoOwner, repoName, err := self.GetBaseRepoOwnerAndName(baseRemote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
|
||||
var g errgroup.Group
|
||||
|
||||
// We want at most 5 concurrent requests, but no less than 10 branches per request
|
||||
concurrency := 5
|
||||
minBranchesPerRequest := 10
|
||||
branchesPerRequest := max(len(branches)/concurrency, minBranchesPerRequest)
|
||||
numChunks := (len(branches) + branchesPerRequest - 1) / branchesPerRequest
|
||||
results := make(chan []*models.GithubPullRequest, numChunks)
|
||||
|
||||
for i := 0; i < len(branches); i += branchesPerRequest {
|
||||
end := i + branchesPerRequest
|
||||
if end > len(branches) {
|
||||
end = len(branches)
|
||||
}
|
||||
branchChunk := branches[i:end]
|
||||
|
||||
// Launch a goroutine for each chunk of branches
|
||||
g.Go(func() error {
|
||||
prs, err := self.fetchRecentPRsAux(repoOwner, repoName, branchChunk, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
results <- prs
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for all goroutines, then close the channel so the range loop exits
|
||||
err = g.Wait()
|
||||
close(results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Collect results from all goroutines
|
||||
var allPRs []*models.GithubPullRequest
|
||||
for prs := range results {
|
||||
allPRs = append(allPRs, prs...)
|
||||
}
|
||||
|
||||
self.Log.Infof("Fetched %d PRs in %s", len(allPRs), time.Since(t))
|
||||
|
||||
return allPRs, nil
|
||||
}
|
||||
|
||||
func (self *GitHubCommands) fetchRecentPRsAux(repoOwner string, repoName string, branches []string, token string) ([]*models.GithubPullRequest, error) {
|
||||
queryString, variables := fetchPullRequestsQuery(branches, repoOwner, repoName)
|
||||
|
||||
bodyBytes, err := json.Marshal(graphQLRequest{Query: queryString, Variables: variables})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", "https://api.github.com/graphql", bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyStr := new(bytes.Buffer)
|
||||
_, _ = bodyStr.ReadFrom(resp.Body)
|
||||
return nil, fmt.Errorf("GraphQL query failed with status: %s. Body: %s", resp.Status, bodyStr.String())
|
||||
}
|
||||
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Response
|
||||
err = json.Unmarshal(respBytes, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prs := []*models.GithubPullRequest{}
|
||||
for _, repoQuery := range result.Data.Repository {
|
||||
for _, edge := range repoQuery.Edges {
|
||||
node := edge.Node
|
||||
pr := &models.GithubPullRequest{
|
||||
HeadRefName: node.HeadRefName,
|
||||
Number: node.Number,
|
||||
Title: node.Title,
|
||||
State: lo.Ternary(node.IsDraft && node.State != "CLOSED", "DRAFT", node.State),
|
||||
Url: node.Url,
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{
|
||||
Login: node.HeadRepositoryOwner.Login,
|
||||
},
|
||||
}
|
||||
prs = append(prs, pr)
|
||||
}
|
||||
}
|
||||
|
||||
return prs, nil
|
||||
}
|
||||
|
||||
// returns a map from branch name to pull request
|
||||
func GenerateGithubPullRequestMap(
|
||||
prs []*models.GithubPullRequest,
|
||||
branches []*models.Branch,
|
||||
remotes []*models.Remote,
|
||||
) map[string]*models.GithubPullRequest {
|
||||
res := map[string]*models.GithubPullRequest{}
|
||||
|
||||
if len(prs) == 0 {
|
||||
return res
|
||||
}
|
||||
|
||||
remotesToOwnersMap := getRemotesToOwnersMap(remotes)
|
||||
|
||||
// A PR can be identified by two things: the owner e.g. 'jesseduffield' and the
|
||||
// branch name e.g. 'feature/my-feature'. The owner might be different
|
||||
// to the owner of the repo if the PR is from a fork of that repo.
|
||||
type prKey struct {
|
||||
owner string
|
||||
branchName string
|
||||
}
|
||||
|
||||
prByKey := map[prKey]models.GithubPullRequest{}
|
||||
|
||||
for _, pr := range prs {
|
||||
key := prKey{owner: pr.UserName(), branchName: pr.BranchName()}
|
||||
// PRs are returned newest-first from the API, so the first one we
|
||||
// see for each key is the most recent and therefore the most relevant.
|
||||
if _, exists := prByKey[key]; !exists {
|
||||
prByKey[key] = *pr
|
||||
}
|
||||
}
|
||||
|
||||
for _, branch := range branches {
|
||||
if !branch.IsTrackingRemote() {
|
||||
continue
|
||||
}
|
||||
|
||||
owner, foundRemoteOwner := remotesToOwnersMap[branch.UpstreamRemote]
|
||||
if !foundRemoteOwner {
|
||||
// UpstreamRemote may be a full URL rather than a remote name;
|
||||
// try parsing the owner directly from it.
|
||||
repoInfo, err := hosting_service.GetRepoInfoFromURL(branch.UpstreamRemote)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
owner = repoInfo.Owner
|
||||
}
|
||||
|
||||
pr, hasPr := prByKey[prKey{owner: owner, branchName: branch.UpstreamBranch}]
|
||||
|
||||
if !hasPr {
|
||||
continue
|
||||
}
|
||||
|
||||
res[branch.Name] = &pr
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func getRemotesToOwnersMap(remotes []*models.Remote) map[string]string {
|
||||
res := map[string]string{}
|
||||
for _, remote := range remotes {
|
||||
if len(remote.Urls) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
repoInfo, err := hosting_service.GetRepoInfoFromURL(remote.Urls[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
res[remote.Name] = repoInfo.Owner
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (self *GitHubCommands) InGithubRepo(remotes []*models.Remote) bool {
|
||||
if len(remotes) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
remote := getMainRemote(remotes)
|
||||
|
||||
if len(remote.Urls) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
url := remote.Urls[0]
|
||||
return strings.Contains(url, "github.com")
|
||||
}
|
||||
|
||||
func getMainRemote(remotes []*models.Remote) *models.Remote {
|
||||
for _, remote := range remotes {
|
||||
if remote.Name == "origin" {
|
||||
return remote
|
||||
}
|
||||
}
|
||||
|
||||
// need to sort remotes by name so that this is deterministic
|
||||
return lo.MinBy(remotes, func(a, b *models.Remote) bool {
|
||||
return a.Name < b.Name
|
||||
})
|
||||
}
|
||||
|
||||
func (self *GitHubCommands) GetBaseRepoOwnerAndName(baseRemote *models.Remote) (string, string, error) {
|
||||
if len(baseRemote.Urls) == 0 {
|
||||
return "", "", fmt.Errorf("No URLs found for remote")
|
||||
}
|
||||
|
||||
url := baseRemote.Urls[0]
|
||||
|
||||
repoInfo, err := hosting_service.GetRepoInfoFromURL(url)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return repoInfo.Owner, repoInfo.Repository, nil
|
||||
}
|
||||
329
pkg/commands/git_commands/github_test.go
Normal file
329
pkg/commands/git_commands/github_test.go
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetRepoInfoFromURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
url string
|
||||
expected hosting_service.RepoInformation
|
||||
}{
|
||||
{
|
||||
name: "SSH URL",
|
||||
url: "git@github.com:jesseduffield/lazygit.git",
|
||||
expected: hosting_service.RepoInformation{
|
||||
Owner: "jesseduffield",
|
||||
Repository: "lazygit",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTTPS URL",
|
||||
url: "https://github.com/jesseduffield/lazygit.git",
|
||||
expected: hosting_service.RepoInformation{
|
||||
Owner: "jesseduffield",
|
||||
Repository: "lazygit",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTTPS URL without .git",
|
||||
url: "https://github.com/jesseduffield/lazygit",
|
||||
expected: hosting_service.RepoInformation{
|
||||
Owner: "jesseduffield",
|
||||
Repository: "lazygit",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSH URL with org nesting",
|
||||
url: "git@github.com:my-org/sub-group/lazygit.git",
|
||||
expected: hosting_service.RepoInformation{
|
||||
Owner: "my-org/sub-group",
|
||||
Repository: "lazygit",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
result, err := hosting_service.GetRepoInfoFromURL(c.url)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateGithubPullRequestMap(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
prs []*models.GithubPullRequest
|
||||
branches []*models.Branch
|
||||
remotes []*models.Remote
|
||||
expected map[string]*models.GithubPullRequest
|
||||
}{
|
||||
{
|
||||
name: "empty inputs",
|
||||
prs: []*models.GithubPullRequest{},
|
||||
branches: []*models.Branch{},
|
||||
remotes: []*models.Remote{},
|
||||
expected: map[string]*models.GithubPullRequest{},
|
||||
},
|
||||
{
|
||||
name: "matches PR to branch tracking origin",
|
||||
prs: []*models.GithubPullRequest{
|
||||
{
|
||||
HeadRefName: "feature-branch",
|
||||
Number: 42,
|
||||
Title: "Add feature",
|
||||
State: "OPEN",
|
||||
Url: "https://github.com/jesseduffield/lazygit/pull/42",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
|
||||
},
|
||||
},
|
||||
branches: []*models.Branch{
|
||||
{
|
||||
Name: "feature-branch",
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "feature-branch",
|
||||
},
|
||||
},
|
||||
remotes: []*models.Remote{
|
||||
{
|
||||
Name: "origin",
|
||||
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
|
||||
},
|
||||
},
|
||||
expected: map[string]*models.GithubPullRequest{
|
||||
"feature-branch": {
|
||||
HeadRefName: "feature-branch",
|
||||
Number: 42,
|
||||
Title: "Add feature",
|
||||
State: "OPEN",
|
||||
Url: "https://github.com/jesseduffield/lazygit/pull/42",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not match branch without upstream",
|
||||
prs: []*models.GithubPullRequest{
|
||||
{
|
||||
HeadRefName: "feature-branch",
|
||||
Number: 42,
|
||||
Title: "Add feature",
|
||||
State: "OPEN",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
|
||||
},
|
||||
},
|
||||
branches: []*models.Branch{
|
||||
{
|
||||
Name: "feature-branch",
|
||||
// no upstream set
|
||||
},
|
||||
},
|
||||
remotes: []*models.Remote{
|
||||
{
|
||||
Name: "origin",
|
||||
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
|
||||
},
|
||||
},
|
||||
expected: map[string]*models.GithubPullRequest{},
|
||||
},
|
||||
{
|
||||
name: "matches fork PR to branch tracking fork remote",
|
||||
prs: []*models.GithubPullRequest{
|
||||
{
|
||||
HeadRefName: "fix-bug",
|
||||
Number: 99,
|
||||
Title: "Fix bug",
|
||||
State: "OPEN",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "contributor"},
|
||||
},
|
||||
},
|
||||
branches: []*models.Branch{
|
||||
{
|
||||
Name: "fix-bug",
|
||||
UpstreamRemote: "contributor",
|
||||
UpstreamBranch: "fix-bug",
|
||||
},
|
||||
},
|
||||
remotes: []*models.Remote{
|
||||
{
|
||||
Name: "origin",
|
||||
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
|
||||
},
|
||||
{
|
||||
Name: "contributor",
|
||||
Urls: []string{"git@github.com:contributor/lazygit.git"},
|
||||
},
|
||||
},
|
||||
expected: map[string]*models.GithubPullRequest{
|
||||
"fix-bug": {
|
||||
HeadRefName: "fix-bug",
|
||||
Number: 99,
|
||||
Title: "Fix bug",
|
||||
State: "OPEN",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "contributor"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not match when owner differs",
|
||||
prs: []*models.GithubPullRequest{
|
||||
{
|
||||
HeadRefName: "feature-branch",
|
||||
Number: 42,
|
||||
Title: "Add feature",
|
||||
State: "OPEN",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "someone-else"},
|
||||
},
|
||||
},
|
||||
branches: []*models.Branch{
|
||||
{
|
||||
Name: "feature-branch",
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "feature-branch",
|
||||
},
|
||||
},
|
||||
remotes: []*models.Remote{
|
||||
{
|
||||
Name: "origin",
|
||||
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
|
||||
},
|
||||
},
|
||||
expected: map[string]*models.GithubPullRequest{},
|
||||
},
|
||||
{
|
||||
name: "matches when UpstreamRemote is a full URL",
|
||||
prs: []*models.GithubPullRequest{
|
||||
{
|
||||
HeadRefName: "my-branch",
|
||||
Number: 55,
|
||||
Title: "Full URL upstream",
|
||||
State: "OPEN",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "contributor"},
|
||||
},
|
||||
},
|
||||
branches: []*models.Branch{
|
||||
{
|
||||
Name: "my-branch",
|
||||
UpstreamRemote: "git@github.com:contributor/lazygit.git",
|
||||
UpstreamBranch: "my-branch",
|
||||
},
|
||||
},
|
||||
remotes: []*models.Remote{
|
||||
{
|
||||
Name: "origin",
|
||||
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
|
||||
},
|
||||
},
|
||||
expected: map[string]*models.GithubPullRequest{
|
||||
"my-branch": {
|
||||
HeadRefName: "my-branch",
|
||||
Number: 55,
|
||||
Title: "Full URL upstream",
|
||||
State: "OPEN",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "contributor"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uses first PR when branch name is reused (API returns newest first)",
|
||||
prs: []*models.GithubPullRequest{
|
||||
// API returns newest first (CREATED_AT DESC)
|
||||
{
|
||||
HeadRefName: "update-sponsors",
|
||||
Number: 50,
|
||||
Title: "Newest PR",
|
||||
State: "CLOSED",
|
||||
Url: "https://github.com/jesseduffield/lazygit/pull/50",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
|
||||
},
|
||||
{
|
||||
HeadRefName: "update-sponsors",
|
||||
Number: 30,
|
||||
Title: "Middle PR",
|
||||
State: "OPEN",
|
||||
Url: "https://github.com/jesseduffield/lazygit/pull/30",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
|
||||
},
|
||||
{
|
||||
HeadRefName: "update-sponsors",
|
||||
Number: 10,
|
||||
Title: "Oldest PR",
|
||||
State: "CLOSED",
|
||||
Url: "https://github.com/jesseduffield/lazygit/pull/10",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
|
||||
},
|
||||
},
|
||||
branches: []*models.Branch{
|
||||
{
|
||||
Name: "update-sponsors",
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "update-sponsors",
|
||||
},
|
||||
},
|
||||
remotes: []*models.Remote{
|
||||
{
|
||||
Name: "origin",
|
||||
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
|
||||
},
|
||||
},
|
||||
expected: map[string]*models.GithubPullRequest{
|
||||
"update-sponsors": {
|
||||
HeadRefName: "update-sponsors",
|
||||
Number: 50,
|
||||
Title: "Newest PR",
|
||||
State: "CLOSED",
|
||||
Url: "https://github.com/jesseduffield/lazygit/pull/50",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "matches with HTTPS remote URL",
|
||||
prs: []*models.GithubPullRequest{
|
||||
{
|
||||
HeadRefName: "my-pr",
|
||||
Number: 10,
|
||||
Title: "My PR",
|
||||
State: "MERGED",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
|
||||
},
|
||||
},
|
||||
branches: []*models.Branch{
|
||||
{
|
||||
Name: "my-pr",
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "my-pr",
|
||||
},
|
||||
},
|
||||
remotes: []*models.Remote{
|
||||
{
|
||||
Name: "origin",
|
||||
Urls: []string{"https://github.com/jesseduffield/lazygit.git"},
|
||||
},
|
||||
},
|
||||
expected: map[string]*models.GithubPullRequest{
|
||||
"my-pr": {
|
||||
HeadRefName: "my-pr",
|
||||
Number: 10,
|
||||
Title: "My PR",
|
||||
State: "MERGED",
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
result := GenerateGithubPullRequestMap(c.prs, c.branches, c.remotes)
|
||||
assert.Equal(t, c.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
34
pkg/commands/git_commands/hosting_service.go
Normal file
34
pkg/commands/git_commands/hosting_service.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package git_commands
|
||||
|
||||
import "github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
|
||||
|
||||
// a hosting service is something like github, gitlab, bitbucket etc
|
||||
type HostingService struct {
|
||||
*GitCommon
|
||||
}
|
||||
|
||||
func NewHostingServiceCommand(gitCommon *GitCommon) *HostingService {
|
||||
return &HostingService{
|
||||
GitCommon: gitCommon,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *HostingService) GetPullRequestURL(from string, to string) (string, error) {
|
||||
return self.getHostingServiceMgr(self.config.GetRemoteURL()).GetPullRequestURL(from, to)
|
||||
}
|
||||
|
||||
func (self *HostingService) GetCommitURL(commitSha string) (string, error) {
|
||||
return self.getHostingServiceMgr(self.config.GetRemoteURL()).GetCommitURL(commitSha)
|
||||
}
|
||||
|
||||
func (self *HostingService) GetRepoNameFromRemoteURL(remoteURL string) (string, error) {
|
||||
return self.getHostingServiceMgr(remoteURL).GetRepoName()
|
||||
}
|
||||
|
||||
// getting this on every request rather than storing it in state in case our remoteURL changes
|
||||
// from one invocation to the next. Note however that we're currently caching config
|
||||
// results so we might want to invalidate the cache here if it becomes a problem.
|
||||
func (self *HostingService) getHostingServiceMgr(remoteURL string) *hosting_service.HostingServiceMgr {
|
||||
configServices := self.UserConfig().Services
|
||||
return hosting_service.NewHostingServiceMgr(self.Log, self.Tr, remoteURL, configServices)
|
||||
}
|
||||
|
|
@ -6,7 +6,11 @@ var defaultUrlRegexStrings = []string{
|
|||
`^(?:https?|ssh)://[^/]+/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^(.*?@)?.*:/*(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
}
|
||||
var defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
|
||||
|
||||
var (
|
||||
defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
|
||||
defaultRepoNameTemplate = "{{.owner}}/{{.repo}}"
|
||||
)
|
||||
|
||||
// we've got less type safety using go templates but this lends itself better to
|
||||
// users adding custom service definitions in their config
|
||||
|
|
@ -17,6 +21,7 @@ var githubServiceDef = ServiceDefinition{
|
|||
commitURL: "/commit/{{.CommitHash}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
repoNameTemplate: defaultRepoNameTemplate,
|
||||
}
|
||||
|
||||
var bitbucketServiceDef = ServiceDefinition{
|
||||
|
|
@ -28,7 +33,8 @@ var bitbucketServiceDef = ServiceDefinition{
|
|||
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^.*@.*:/*(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
},
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
repoNameTemplate: defaultRepoNameTemplate,
|
||||
}
|
||||
|
||||
var gitLabServiceDef = ServiceDefinition{
|
||||
|
|
@ -38,6 +44,7 @@ var gitLabServiceDef = ServiceDefinition{
|
|||
commitURL: "/-/commit/{{.CommitHash}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
repoNameTemplate: defaultRepoNameTemplate,
|
||||
}
|
||||
|
||||
var azdoServiceDef = ServiceDefinition{
|
||||
|
|
@ -51,7 +58,8 @@ var azdoServiceDef = ServiceDefinition{
|
|||
`^https://.*@dev.azure.com/(?P<org>.*?)/(?P<project>.*?)/_git/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^https://.*/(?P<org>.*?)/(?P<project>.*?)/_git/(?P<repo>.*?)(?:\.git)?$`,
|
||||
},
|
||||
repoURLTemplate: "https://{{.webDomain}}/{{.org}}/{{.project}}/_git/{{.repo}}",
|
||||
repoURLTemplate: "https://{{.webDomain}}/{{.org}}/{{.project}}/_git/{{.repo}}",
|
||||
repoNameTemplate: "{{.org}}/{{.project}}/{{.repo}}",
|
||||
}
|
||||
|
||||
var bitbucketServerServiceDef = ServiceDefinition{
|
||||
|
|
@ -63,7 +71,8 @@ var bitbucketServerServiceDef = ServiceDefinition{
|
|||
`^ssh://git@.*/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^https://.*/scm/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
},
|
||||
repoURLTemplate: "https://{{.webDomain}}/projects/{{.project}}/repos/{{.repo}}",
|
||||
repoURLTemplate: "https://{{.webDomain}}/projects/{{.project}}/repos/{{.repo}}",
|
||||
repoNameTemplate: "{{.project}}/{{.repo}}",
|
||||
}
|
||||
|
||||
var giteaServiceDef = ServiceDefinition{
|
||||
|
|
|
|||
|
|
@ -61,6 +61,18 @@ func (self *HostingServiceMgr) GetCommitURL(commitHash string) (string, error) {
|
|||
return pullRequestURL, nil
|
||||
}
|
||||
|
||||
// e.g. 'jesseduffield/lazygit'
|
||||
func (self *HostingServiceMgr) GetRepoName() (string, error) {
|
||||
gitService, err := self.getService()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
repoName := gitService.repoName
|
||||
|
||||
return repoName, nil
|
||||
}
|
||||
|
||||
func (self *HostingServiceMgr) getService() (*Service, error) {
|
||||
serviceDomain, err := self.getServiceDomain(self.remoteURL)
|
||||
if err != nil {
|
||||
|
|
@ -72,8 +84,14 @@ func (self *HostingServiceMgr) getService() (*Service, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
repoName, err := serviceDomain.serviceDefinition.getRepoNameFromRemoteURL(self.remoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
repoURL: repoURL,
|
||||
repoName: repoName,
|
||||
ServiceDefinition: serviceDomain.serviceDefinition,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -144,24 +162,68 @@ type ServiceDefinition struct {
|
|||
regexStrings []string
|
||||
|
||||
// can expect 'webdomain' to be passed in. Otherwise, you get to pick what we match in the regex
|
||||
repoURLTemplate string
|
||||
repoURLTemplate string
|
||||
repoNameTemplate string
|
||||
}
|
||||
|
||||
func (self ServiceDefinition) getRepoURLFromRemoteURL(url string, webDomain string) (string, error) {
|
||||
matches, err := self.parseRemoteUrl(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
matches["webDomain"] = webDomain
|
||||
return utils.ResolvePlaceholderString(self.repoURLTemplate, matches), nil
|
||||
}
|
||||
|
||||
func (self ServiceDefinition) getRepoNameFromRemoteURL(url string) (string, error) {
|
||||
matches, err := self.parseRemoteUrl(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return utils.ResolvePlaceholderString(self.repoNameTemplate, matches), nil
|
||||
}
|
||||
|
||||
func (self ServiceDefinition) parseRemoteUrl(url string) (map[string]string, error) {
|
||||
for _, regexStr := range self.regexStrings {
|
||||
re := regexp.MustCompile(regexStr)
|
||||
input := utils.FindNamedMatches(re, url)
|
||||
if input != nil {
|
||||
input["webDomain"] = webDomain
|
||||
return utils.ResolvePlaceholderString(self.repoURLTemplate, input), nil
|
||||
matches := utils.FindNamedMatches(re, url)
|
||||
if matches != nil {
|
||||
return matches, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("Failed to parse repo information from url")
|
||||
return nil, errors.New("Failed to parse repo information from url")
|
||||
}
|
||||
|
||||
// RepoInformation holds the owner and repository name parsed from a remote URL.
|
||||
type RepoInformation struct {
|
||||
Owner string
|
||||
Repository string
|
||||
}
|
||||
|
||||
// GetRepoInfoFromURL parses a remote URL (SSH or HTTPS) and extracts the
|
||||
// owner and repository name using the default URL regex patterns.
|
||||
func GetRepoInfoFromURL(url string) (RepoInformation, error) {
|
||||
for _, regexStr := range defaultUrlRegexStrings {
|
||||
re := regexp.MustCompile(regexStr)
|
||||
matches := utils.FindNamedMatches(re, url)
|
||||
if matches != nil {
|
||||
return RepoInformation{
|
||||
Owner: matches["owner"],
|
||||
Repository: matches["repo"],
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return RepoInformation{}, errors.New("Failed to parse repo information from url")
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
repoURL string
|
||||
// e.g. 'jesseduffield/lazygit'
|
||||
repoName string
|
||||
ServiceDefinition
|
||||
}
|
||||
|
||||
|
|
|
|||
24
pkg/commands/models/github.go
Normal file
24
pkg/commands/models/github.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package models
|
||||
|
||||
type GithubPullRequest struct {
|
||||
HeadRefName string `json:"headRefName"`
|
||||
Number int `json:"number"`
|
||||
Title string `json:"title"`
|
||||
State string `json:"state"` // "MERGED", "OPEN", "CLOSED", "DRAFT"
|
||||
Url string `json:"url"`
|
||||
HeadRepositoryOwner GithubRepositoryOwner `json:"headRepositoryOwner"`
|
||||
}
|
||||
|
||||
func (pr *GithubPullRequest) UserName() string {
|
||||
// e.g. 'jesseduffield'
|
||||
return pr.HeadRepositoryOwner.Login
|
||||
}
|
||||
|
||||
func (pr *GithubPullRequest) BranchName() string {
|
||||
// e.g. 'feature/my-feature'
|
||||
return pr.HeadRefName
|
||||
}
|
||||
|
||||
type GithubRepositoryOwner struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
|
@ -704,10 +704,27 @@ type AppState struct {
|
|||
ShellCommandsHistory []string `yaml:"customcommandshistory"`
|
||||
|
||||
HideCommandLog bool
|
||||
|
||||
// Cache of GitHub pull requests per repo path, so that PR info can be
|
||||
// shown instantly on startup before the async refresh completes.
|
||||
GithubPullRequests map[string][]CachedPullRequest `yaml:"githubPullRequests"`
|
||||
}
|
||||
|
||||
// CachedPullRequest stores the essential fields of a GitHub pull request
|
||||
// for persisting in the app state cache.
|
||||
type CachedPullRequest struct {
|
||||
HeadRefName string `yaml:"headRefName"`
|
||||
Number int `yaml:"number"`
|
||||
Title string `yaml:"title"`
|
||||
State string `yaml:"state"`
|
||||
Url string `yaml:"url"`
|
||||
HeadRepositoryOwner string `yaml:"headRepositoryOwner"`
|
||||
}
|
||||
|
||||
func getDefaultAppState() *AppState {
|
||||
return &AppState{}
|
||||
return &AppState{
|
||||
GithubPullRequests: make(map[string][]CachedPullRequest),
|
||||
}
|
||||
}
|
||||
|
||||
func LogPath() (string, error) {
|
||||
|
|
|
|||
|
|
@ -531,24 +531,25 @@ type KeybindingFilesConfig struct {
|
|||
}
|
||||
|
||||
type KeybindingBranchesConfig struct {
|
||||
CreatePullRequest string `yaml:"createPullRequest"`
|
||||
ViewPullRequestOptions string `yaml:"viewPullRequestOptions"`
|
||||
CopyPullRequestURL string `yaml:"copyPullRequestURL"`
|
||||
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
|
||||
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
|
||||
CheckoutPreviousBranch string `yaml:"checkoutPreviousBranch"`
|
||||
RebaseBranch string `yaml:"rebaseBranch"`
|
||||
RenameBranch string `yaml:"renameBranch"`
|
||||
MergeIntoCurrentBranch string `yaml:"mergeIntoCurrentBranch"`
|
||||
MoveCommitsToNewBranch string `yaml:"moveCommitsToNewBranch"`
|
||||
ViewGitFlowOptions string `yaml:"viewGitFlowOptions"`
|
||||
FastForward string `yaml:"fastForward"`
|
||||
CreateTag string `yaml:"createTag"`
|
||||
PushTag string `yaml:"pushTag"`
|
||||
SetUpstream string `yaml:"setUpstream"`
|
||||
FetchRemote string `yaml:"fetchRemote"`
|
||||
AddForkRemote string `yaml:"addForkRemote"`
|
||||
SortOrder string `yaml:"sortOrder"`
|
||||
CreatePullRequest string `yaml:"createPullRequest"`
|
||||
ViewPullRequestOptions string `yaml:"viewPullRequestOptions"`
|
||||
OpenPullRequestInBrowser string `yaml:"openPullRequestInBrowser"`
|
||||
CopyPullRequestURL string `yaml:"copyPullRequestURL"`
|
||||
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
|
||||
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
|
||||
CheckoutPreviousBranch string `yaml:"checkoutPreviousBranch"`
|
||||
RebaseBranch string `yaml:"rebaseBranch"`
|
||||
RenameBranch string `yaml:"renameBranch"`
|
||||
MergeIntoCurrentBranch string `yaml:"mergeIntoCurrentBranch"`
|
||||
MoveCommitsToNewBranch string `yaml:"moveCommitsToNewBranch"`
|
||||
ViewGitFlowOptions string `yaml:"viewGitFlowOptions"`
|
||||
FastForward string `yaml:"fastForward"`
|
||||
CreateTag string `yaml:"createTag"`
|
||||
PushTag string `yaml:"pushTag"`
|
||||
SetUpstream string `yaml:"setUpstream"`
|
||||
FetchRemote string `yaml:"fetchRemote"`
|
||||
AddForkRemote string `yaml:"addForkRemote"`
|
||||
SortOrder string `yaml:"sortOrder"`
|
||||
}
|
||||
|
||||
type KeybindingWorktreesConfig struct {
|
||||
|
|
@ -579,6 +580,7 @@ type KeybindingCommitsConfig struct {
|
|||
CopyCommitAttributeToClipboard string `yaml:"copyCommitAttributeToClipboard"`
|
||||
OpenLogMenu string `yaml:"openLogMenu"`
|
||||
OpenInBrowser string `yaml:"openInBrowser"`
|
||||
OpenPullRequestInBrowser string `yaml:"openPullRequestInBrowser"`
|
||||
ViewBisectOptions string `yaml:"viewBisectOptions"`
|
||||
StartInteractiveRebase string `yaml:"startInteractiveRebase"`
|
||||
SelectCommitsOfCurrentBranch string `yaml:"selectCommitsOfCurrentBranch"`
|
||||
|
|
@ -996,24 +998,25 @@ func GetDefaultConfig() *UserConfig {
|
|||
ExpandAll: "=",
|
||||
},
|
||||
Branches: KeybindingBranchesConfig{
|
||||
CopyPullRequestURL: "<c-y>",
|
||||
CreatePullRequest: "o",
|
||||
ViewPullRequestOptions: "O",
|
||||
CheckoutBranchByName: "c",
|
||||
ForceCheckoutBranch: "F",
|
||||
CheckoutPreviousBranch: "-",
|
||||
RebaseBranch: "r",
|
||||
RenameBranch: "R",
|
||||
MergeIntoCurrentBranch: "M",
|
||||
MoveCommitsToNewBranch: "N",
|
||||
ViewGitFlowOptions: "i",
|
||||
FastForward: "f",
|
||||
CreateTag: "T",
|
||||
PushTag: "P",
|
||||
SetUpstream: "u",
|
||||
FetchRemote: "f",
|
||||
AddForkRemote: "F",
|
||||
SortOrder: "s",
|
||||
CopyPullRequestURL: "<c-y>",
|
||||
CreatePullRequest: "o",
|
||||
ViewPullRequestOptions: "O",
|
||||
OpenPullRequestInBrowser: "G",
|
||||
CheckoutBranchByName: "c",
|
||||
ForceCheckoutBranch: "F",
|
||||
CheckoutPreviousBranch: "-",
|
||||
RebaseBranch: "r",
|
||||
RenameBranch: "R",
|
||||
MergeIntoCurrentBranch: "M",
|
||||
MoveCommitsToNewBranch: "N",
|
||||
ViewGitFlowOptions: "i",
|
||||
FastForward: "f",
|
||||
CreateTag: "T",
|
||||
PushTag: "P",
|
||||
SetUpstream: "u",
|
||||
FetchRemote: "f",
|
||||
AddForkRemote: "F",
|
||||
SortOrder: "s",
|
||||
},
|
||||
Worktrees: KeybindingWorktreesConfig{
|
||||
ViewWorktreeOptions: "w",
|
||||
|
|
@ -1042,6 +1045,7 @@ func GetDefaultConfig() *UserConfig {
|
|||
CopyCommitAttributeToClipboard: "y",
|
||||
OpenLogMenu: "<c-l>",
|
||||
OpenInBrowser: "o",
|
||||
OpenPullRequestInBrowser: "G",
|
||||
ViewBisectOptions: "b",
|
||||
StartInteractiveRebase: "i",
|
||||
SelectCommitsOfCurrentBranch: "*",
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
|
|||
func (self *BackgroundRoutineMgr) backgroundFetch() (err error) {
|
||||
err = self.gui.git.Sync.FetchBackground()
|
||||
|
||||
self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.SYNC})
|
||||
self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS, types.PULL_REQUESTS}, Mode: types.SYNC})
|
||||
|
||||
if err == nil {
|
||||
err = self.gui.helpers.BranchesHelper.AutoForwardBranches()
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
|||
return presentation.GetBranchListDisplayStrings(
|
||||
viewModel.GetItems(),
|
||||
c.State().GetItemOperation,
|
||||
c.Model().PullRequestsMap,
|
||||
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
||||
c.Modes().Diffing.Ref,
|
||||
c.Views().Branches.InnerWidth()+c.Views().Branches.OriginX(),
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ type MenuViewModel struct {
|
|||
columnAlignment []utils.Alignment
|
||||
allowFilteringKeybindings bool
|
||||
keybindingsTakePrecedence bool
|
||||
onCancel func() error
|
||||
*FilteredListViewModel[*types.MenuItem]
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +98,10 @@ func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem, columnAlignment
|
|||
self.columnAlignment = columnAlignment
|
||||
}
|
||||
|
||||
func (self *MenuViewModel) SetOnCancel(onCancel func() error) {
|
||||
self.onCancel = onCancel
|
||||
}
|
||||
|
||||
func (self *MenuViewModel) GetPrompt() string {
|
||||
return self.prompt
|
||||
}
|
||||
|
|
@ -239,6 +244,9 @@ func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error {
|
|||
self.c.Context().Pop()
|
||||
|
||||
if selectedItem == nil {
|
||||
if self.onCancel != nil {
|
||||
return self.onCancel()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@ package controllers
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
|
|
@ -77,6 +81,12 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
|||
Description: self.c.Tr.CreatePullRequestOptions,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Branches.OpenPullRequestInBrowser),
|
||||
Handler: self.withItem(self.openPRInBrowser),
|
||||
GetDisabledReason: self.require(self.singleItemSelected(self.branchHasPR)),
|
||||
Description: self.c.Tr.OpenPullRequestInBrowser,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Branches.CopyPullRequestURL),
|
||||
Handler: self.copyPullRequestURL,
|
||||
|
|
@ -192,7 +202,19 @@ func (self *BranchesController) GetOnRenderToMain() func() {
|
|||
} else {
|
||||
cmdObj := self.c.Git().Branch.GetGraphCmdObj(branch.FullRefName())
|
||||
|
||||
task = types.NewRunPtyTask(cmdObj.GetCmd())
|
||||
ptyTask := types.NewRunPtyTask(cmdObj.GetCmd())
|
||||
task = ptyTask
|
||||
|
||||
if pr, ok := self.c.Model().PullRequestsMap[branch.Name]; ok {
|
||||
icon := lo.Ternary(icons.IsIconEnabled(), icons.IconForRemoteUrl(pr.Url)+" ", "")
|
||||
ptyTask.Prefix = style.PrintHyperlink(fmt.Sprintf("%s%s %s %s\n",
|
||||
icon,
|
||||
coloredStateText(pr.State),
|
||||
pr.Title,
|
||||
style.FgCyan.Sprintf("#%d", pr.Number)),
|
||||
pr.Url)
|
||||
ptyTask.Prefix += strings.Repeat("─", self.c.Contexts().Normal.GetView().InnerWidth()) + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||
|
|
@ -206,6 +228,67 @@ func (self *BranchesController) GetOnRenderToMain() func() {
|
|||
}
|
||||
}
|
||||
|
||||
func stateText(state string) string {
|
||||
var icon, label string
|
||||
switch state {
|
||||
case "OPEN":
|
||||
icon, label = " ", "Open"
|
||||
case "CLOSED":
|
||||
icon, label = " ", "Closed"
|
||||
case "MERGED":
|
||||
icon, label = " ", "Merged"
|
||||
case "DRAFT":
|
||||
icon, label = " ", "Draft"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
if icons.IsIconEnabled() {
|
||||
return icon + label
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
func coloredStateText(state string) string {
|
||||
if icons.IsIconEnabled() {
|
||||
return fmt.Sprintf("%s%s%s",
|
||||
withPrFgColor(state, ""),
|
||||
withPrBgColor(state, style.FgWhite.Sprint(stateText(state))),
|
||||
withPrFgColor(state, ""))
|
||||
}
|
||||
|
||||
return withPrFgColor(state, stateText(state))
|
||||
}
|
||||
|
||||
func withPrFgColor(state string, text string) string {
|
||||
switch state {
|
||||
case "OPEN":
|
||||
return style.FgGreen.Sprint(text)
|
||||
case "CLOSED":
|
||||
return style.FgRed.Sprint(text)
|
||||
case "MERGED":
|
||||
return style.FgMagenta.Sprint(text)
|
||||
case "DRAFT":
|
||||
return color.RGB(0x66, 0x66, 0x66, false).Sprint(text)
|
||||
default:
|
||||
return style.FgDefault.Sprint(text)
|
||||
}
|
||||
}
|
||||
|
||||
func withPrBgColor(state string, text string) string {
|
||||
switch state {
|
||||
case "OPEN":
|
||||
return style.BgGreen.Sprint(text)
|
||||
case "CLOSED":
|
||||
return style.BgRed.Sprint(text)
|
||||
case "MERGED":
|
||||
return style.BgMagenta.Sprint(text)
|
||||
case "DRAFT":
|
||||
return color.RGB(0x66, 0x66, 0x66, true).Sprint(text)
|
||||
default:
|
||||
return style.BgDefault.Sprint(text)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branch) error {
|
||||
upstream := lo.Ternary(selectedBranch.RemoteBranchStoredLocally(),
|
||||
selectedBranch.ShortUpstreamRefName(),
|
||||
|
|
@ -442,16 +525,23 @@ func (self *BranchesController) handleCreatePullRequestMenu(selectedBranch *mode
|
|||
return self.createPullRequestMenu(selectedBranch, checkedOutBranch)
|
||||
}
|
||||
|
||||
func (self *BranchesController) copyPullRequestURL() error {
|
||||
func (self *BranchesController) getPullRequestURL() (string, error) {
|
||||
branch := self.context().GetSelected()
|
||||
if pr, ok := self.c.Model().PullRequestsMap[branch.Name]; ok {
|
||||
return pr.Url, nil
|
||||
}
|
||||
|
||||
branchExistsOnRemote := self.c.Git().Remote.CheckRemoteBranchExists(branch.Name)
|
||||
|
||||
if !branchExistsOnRemote {
|
||||
return errors.New(self.c.Tr.NoBranchOnRemote)
|
||||
return "", errors.New(self.c.Tr.NoBranchOnRemote)
|
||||
}
|
||||
|
||||
url, err := self.c.Helpers().Host.GetPullRequestURL(branch.Name, "")
|
||||
return self.c.Helpers().Host.GetPullRequestURL(branch.Name, "")
|
||||
}
|
||||
|
||||
func (self *BranchesController) copyPullRequestURL() error {
|
||||
url, err := self.getPullRequestURL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -853,6 +943,27 @@ func (self *BranchesController) branchIsReal(branch *models.Branch) *types.Disab
|
|||
return nil
|
||||
}
|
||||
|
||||
func (self *BranchesController) branchHasPR(branch *models.Branch) *types.DisabledReason {
|
||||
if _, ok := self.c.Model().PullRequestsMap[branch.Name]; !ok {
|
||||
return &types.DisabledReason{Text: self.c.Tr.NoPullRequestForBranch, ShowErrorInPanel: true}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BranchesController) openPRInBrowser(branch *models.Branch) error {
|
||||
pr, ok := self.c.Model().PullRequestsMap[branch.Name]
|
||||
if !ok {
|
||||
// Should be guarded against by the DisabledReason check, but be defensive in case
|
||||
// PullRequestsMap was updated concurrently by a background refresh
|
||||
return errors.New(self.c.Tr.NoPullRequestForBranch)
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.OpenPullRequest)
|
||||
|
||||
return self.c.OS().OpenLink(pr.Url)
|
||||
}
|
||||
|
||||
func (self *BranchesController) branchesAreReal(selectedBranches []*models.Branch, startIdx int, endIdx int) *types.DisabledReason {
|
||||
if !lo.EveryBy(selectedBranches, func(branch *models.Branch) bool {
|
||||
return branch.IsRealBranch()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -9,10 +10,12 @@ import (
|
|||
"github.com/jesseduffield/gocui"
|
||||
"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/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
|
|
@ -27,6 +30,12 @@ type RefreshHelper struct {
|
|||
mergeConflictsHelper *MergeConflictsHelper
|
||||
worktreeHelper *WorktreeHelper
|
||||
searchHelper *SearchHelper
|
||||
|
||||
// Tracks repos for which the user has dismissed the "select base GitHub remote"
|
||||
// prompt, to avoid re-prompting on every subsequent refresh within the same session.
|
||||
// Keyed by repo path so that switching to a different repo while lazygit is running
|
||||
// still triggers the prompt there.
|
||||
githubBaseRemotePromptDismissed map[string]bool
|
||||
}
|
||||
|
||||
func NewRefreshHelper(
|
||||
|
|
@ -91,6 +100,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) {
|
|||
types.STATUS,
|
||||
types.BISECT_INFO,
|
||||
types.STAGING,
|
||||
types.PULL_REQUESTS,
|
||||
})
|
||||
} else {
|
||||
scopeSet = set.NewFromSlice(options.Scope)
|
||||
|
|
@ -117,6 +127,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) {
|
|||
}
|
||||
}
|
||||
|
||||
branchesAndRemotesWg := sync.WaitGroup{}
|
||||
includeWorktreesWithBranches := false
|
||||
if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) {
|
||||
// whenever we change commits, we should update branches because the upstream/downstream
|
||||
|
|
@ -126,9 +137,17 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) {
|
|||
|
||||
includeWorktreesWithBranches = scopeSet.Includes(types.WORKTREES)
|
||||
if self.c.UserConfig().Git.LocalBranchSortOrder == "recency" {
|
||||
refresh("reflog and branches", func() { self.refreshReflogAndBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex) })
|
||||
branchesAndRemotesWg.Add(1)
|
||||
refresh("reflog and branches", func() {
|
||||
self.refreshReflogAndBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex)
|
||||
branchesAndRemotesWg.Done()
|
||||
})
|
||||
} else {
|
||||
refresh("branches", func() { self.refreshBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex, true) })
|
||||
branchesAndRemotesWg.Add(1)
|
||||
refresh("branches", func() {
|
||||
self.refreshBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex, true)
|
||||
branchesAndRemotesWg.Done()
|
||||
})
|
||||
refresh("reflog", func() { _ = self.refreshReflogCommits() })
|
||||
}
|
||||
} else if scopeSet.Includes(types.REBASE_COMMITS) {
|
||||
|
|
@ -164,7 +183,18 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) {
|
|||
}
|
||||
|
||||
if scopeSet.Includes(types.REMOTES) {
|
||||
refresh("remotes", func() { _ = self.refreshRemotes() })
|
||||
branchesAndRemotesWg.Add(1)
|
||||
refresh("remotes", func() {
|
||||
_ = self.refreshRemotes()
|
||||
branchesAndRemotesWg.Done()
|
||||
})
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.PULL_REQUESTS) {
|
||||
refresh("pull requests", func() {
|
||||
branchesAndRemotesWg.Wait()
|
||||
self.refreshGithubPullRequests()
|
||||
})
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.WORKTREES) && !includeWorktreesWithBranches {
|
||||
|
|
@ -209,6 +239,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) {
|
|||
func getScopeNames(scopes []types.RefreshableView) []string {
|
||||
scopeNameMap := map[types.RefreshableView]string{
|
||||
types.COMMITS: "commits",
|
||||
types.REBASE_COMMITS: "rebaseCommits",
|
||||
types.BRANCHES: "branches",
|
||||
types.FILES: "files",
|
||||
types.SUBMODULES: "submodules",
|
||||
|
|
@ -221,7 +252,10 @@ func getScopeNames(scopes []types.RefreshableView) []string {
|
|||
types.STATUS: "status",
|
||||
types.BISECT_INFO: "bisect",
|
||||
types.STAGING: "staging",
|
||||
types.PATCH_BUILDING: "patchBuilding",
|
||||
types.MERGE_CONFLICTS: "mergeConflicts",
|
||||
types.COMMIT_FILES: "commitFiles",
|
||||
types.PULL_REQUESTS: "pullRequests",
|
||||
}
|
||||
|
||||
return lo.Map(scopes, func(scope types.RefreshableView, _ int) string {
|
||||
|
|
@ -477,6 +511,7 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
|
|||
prevSelectedBranch := self.c.Contexts().Branches.GetSelected()
|
||||
|
||||
self.c.Model().Branches = branches
|
||||
self.rebuildPullRequestsMap()
|
||||
|
||||
if refreshWorktrees {
|
||||
self.loadWorktrees()
|
||||
|
|
@ -658,6 +693,13 @@ func (self *RefreshHelper) refreshRemotes() error {
|
|||
|
||||
self.c.Model().Remotes = remotes
|
||||
|
||||
hadPrs := len(self.c.Model().PullRequestsMap) != 0
|
||||
self.rebuildPullRequestsMap()
|
||||
if !hadPrs && len(self.c.Model().PullRequestsMap) != 0 {
|
||||
// if we didn't have PRs in the map before but now we do, we need to redraw the branches view
|
||||
self.refreshView(self.c.Contexts().Branches)
|
||||
}
|
||||
|
||||
// we need to ensure our selected remote branches aren't now outdated
|
||||
if prevSelectedRemote != nil && self.c.Model().RemoteBranches != nil {
|
||||
// find remote now
|
||||
|
|
@ -757,3 +799,154 @@ func (self *RefreshHelper) refreshView(context types.Context) {
|
|||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshGithubPullRequests() {
|
||||
self.c.Mutexes().RefreshingPullRequestsMutex.Lock()
|
||||
defer self.c.Mutexes().RefreshingPullRequestsMutex.Unlock()
|
||||
|
||||
if !self.c.Git().GitHub.InGithubRepo(self.c.Model().Remotes) {
|
||||
self.c.Model().PullRequests = nil
|
||||
self.c.Model().PullRequestsMap = nil
|
||||
return
|
||||
}
|
||||
|
||||
authToken := self.c.Git().GitHub.GetAuthToken()
|
||||
if authToken == "" {
|
||||
self.c.Model().PullRequests = nil
|
||||
self.c.Model().PullRequestsMap = nil
|
||||
return
|
||||
}
|
||||
|
||||
baseRemote := self.getGithubBaseRemote()
|
||||
if baseRemote == nil {
|
||||
if !self.githubBaseRemotePromptDismissed[self.c.Git().RepoPaths.RepoPath()] {
|
||||
self.promptForBaseGithubRepo(authToken)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := self.setGithubPullRequests(authToken, baseRemote); err != nil {
|
||||
self.c.LogAction(fmt.Sprintf("Error fetching pull requests from GitHub: %s", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) getGithubBaseRemote() *models.Remote {
|
||||
remotes := self.c.Model().Remotes
|
||||
|
||||
findRemoteByName := func(name string) *models.Remote {
|
||||
remote, _ := lo.Find(remotes, func(remote *models.Remote) bool {
|
||||
return remote.Name == name
|
||||
})
|
||||
return remote
|
||||
}
|
||||
|
||||
if configuredRemote := self.c.Git().GitHub.ConfiguredBaseRemoteName(); configuredRemote != "" {
|
||||
return findRemoteByName(configuredRemote)
|
||||
}
|
||||
|
||||
if len(remotes) == 1 {
|
||||
return remotes[0]
|
||||
}
|
||||
|
||||
for _, remoteName := range []string{"upstream", "origin"} {
|
||||
if remote := findRemoteByName(remoteName); remote != nil {
|
||||
return remote
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) promptForBaseGithubRepo(authToken string) {
|
||||
menuItems := lo.FilterMap(self.c.Model().Remotes, func(remote *models.Remote, _ int) (*types.MenuItem, bool) {
|
||||
if len(remote.Urls) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
repoName, err := self.c.Git().HostingService.GetRepoNameFromRemoteURL(remote.Urls[0])
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &types.MenuItem{
|
||||
LabelColumns: []string{remote.Name, style.FgCyan.Sprint(repoName)},
|
||||
OnPress: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.FetchingPullRequests, func(gocui.Task) error {
|
||||
if err := self.c.Git().GitHub.SetConfiguredBaseRemoteName(remote.Name); err != nil {
|
||||
self.c.Log.Error(err)
|
||||
}
|
||||
|
||||
if err := self.setGithubPullRequests(authToken, remote); err != nil {
|
||||
self.c.LogAction(fmt.Sprintf("Error fetching pull requests from GitHub: %s", err.Error()))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
}, true
|
||||
})
|
||||
|
||||
_ = self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.SelectRemoteRepository,
|
||||
Items: menuItems,
|
||||
OnCancel: func() error {
|
||||
if self.githubBaseRemotePromptDismissed == nil {
|
||||
self.githubBaseRemotePromptDismissed = make(map[string]bool)
|
||||
}
|
||||
self.githubBaseRemotePromptDismissed[self.c.Git().RepoPaths.RepoPath()] = true
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) rebuildPullRequestsMap() {
|
||||
self.c.Model().PullRequestsMap = git_commands.GenerateGithubPullRequestMap(
|
||||
self.c.Model().PullRequests,
|
||||
self.c.Model().Branches,
|
||||
self.c.Model().Remotes,
|
||||
)
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) setGithubPullRequests(authToken string, baseRemote *models.Remote) error {
|
||||
if len(self.c.Model().Branches) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
branches := lo.Filter(self.c.Model().Branches, func(branch *models.Branch, _ int) bool {
|
||||
return branch.IsTrackingRemote()
|
||||
})
|
||||
branchNames := lo.Map(branches, func(branch *models.Branch, _ int) string {
|
||||
return branch.UpstreamBranch
|
||||
})
|
||||
|
||||
prs, err := self.c.Git().GitHub.FetchRecentPRs(branchNames, baseRemote, authToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.c.Model().PullRequests = prs
|
||||
self.savePullRequestsToCache(prs)
|
||||
self.rebuildPullRequestsMap()
|
||||
|
||||
self.c.PostRefreshUpdate(self.c.Contexts().Branches)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) savePullRequestsToCache(prs []*models.GithubPullRequest) {
|
||||
repoPath := self.c.Git().RepoPaths.RepoPath()
|
||||
cached := lo.Map(prs, func(pr *models.GithubPullRequest, _ int) config.CachedPullRequest {
|
||||
return config.CachedPullRequest{
|
||||
HeadRefName: pr.HeadRefName,
|
||||
Number: pr.Number,
|
||||
Title: pr.Title,
|
||||
State: pr.State,
|
||||
Url: pr.Url,
|
||||
HeadRepositoryOwner: pr.HeadRepositoryOwner.Login,
|
||||
}
|
||||
})
|
||||
|
||||
appState := self.c.GetAppState()
|
||||
if appState.GithubPullRequests == nil {
|
||||
appState.GithubPullRequests = make(map[string][]config.CachedPullRequest)
|
||||
}
|
||||
appState.GithubPullRequests[repoPath] = cached
|
||||
self.c.SaveAppStateAndLogError()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,19 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions
|
|||
// loading a heap of commits is slow so we limit them whenever doing a reset
|
||||
self.c.Contexts().LocalCommits.SetLimitCommits(true)
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI, KeepBranchSelectionIndex: true})
|
||||
scope := []types.RefreshableView{
|
||||
types.COMMITS,
|
||||
types.BRANCHES,
|
||||
types.FILES,
|
||||
types.REFLOG,
|
||||
types.WORKTREES,
|
||||
types.BISECT_INFO,
|
||||
types.STAGING,
|
||||
}
|
||||
if options.RefreshPullRequests {
|
||||
scope = append(scope, types.PULL_REQUESTS)
|
||||
}
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI, Scope: scope, KeepBranchSelectionIndex: true})
|
||||
}
|
||||
|
||||
localBranch, found := lo.Find(self.c.Model().Branches, func(branch *models.Branch) bool {
|
||||
|
|
@ -120,8 +132,8 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions
|
|||
|
||||
// Shows a prompt to choose between creating a new branch or checking out a detached head
|
||||
func (self *RefsHelper) CheckoutRemoteBranch(fullBranchName string, localBranchName string) error {
|
||||
checkout := func(branchName string) error {
|
||||
return self.CheckoutRef(branchName, types.CheckoutRefOptions{})
|
||||
checkout := func(branchName string, refreshPullRequests bool) error {
|
||||
return self.CheckoutRef(branchName, types.CheckoutRefOptions{RefreshPullRequests: refreshPullRequests})
|
||||
}
|
||||
|
||||
// If a branch with this name already exists locally, just check it out. We
|
||||
|
|
@ -130,7 +142,7 @@ func (self *RefsHelper) CheckoutRemoteBranch(fullBranchName string, localBranchN
|
|||
if lo.ContainsBy(self.c.Model().Branches, func(branch *models.Branch) bool {
|
||||
return branch.Name == localBranchName
|
||||
}) {
|
||||
return checkout(localBranchName)
|
||||
return checkout(localBranchName, false)
|
||||
}
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
|
|
@ -156,14 +168,14 @@ func (self *RefsHelper) CheckoutRemoteBranch(fullBranchName string, localBranchN
|
|||
Mode: types.SYNC,
|
||||
Scope: []types.RefreshableView{types.BRANCHES},
|
||||
})
|
||||
return checkout(localBranchName)
|
||||
return checkout(localBranchName, true)
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: self.c.Tr.CheckoutTypeDetachedHead,
|
||||
Tooltip: self.c.Tr.CheckoutTypeDetachedHeadTooltip,
|
||||
OnPress: func() error {
|
||||
return checkout(fullBranchName)
|
||||
return checkout(fullBranchName, false)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -253,11 +253,38 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
|
|||
Tooltip: self.c.Tr.OpenLogMenuTooltip,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Commits.OpenPullRequestInBrowser),
|
||||
Handler: self.openPRInBrowser,
|
||||
GetDisabledReason: self.checkedOutBranchHasPR,
|
||||
Description: self.c.Tr.OpenPullRequestInBrowser,
|
||||
},
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) checkedOutBranchHasPR() *types.DisabledReason {
|
||||
branch := self.c.Model().CheckedOutBranch
|
||||
if _, ok := self.c.Model().PullRequestsMap[branch]; !ok {
|
||||
return &types.DisabledReason{Text: self.c.Tr.NoPullRequestForBranch, ShowErrorInPanel: true}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) openPRInBrowser() error {
|
||||
pr, ok := self.c.Model().PullRequestsMap[self.c.Model().CheckedOutBranch]
|
||||
if !ok {
|
||||
// Should be guarded against by the DisabledReason check, but be defensive in case
|
||||
// PullRequestsMap was updated concurrently by a background refresh
|
||||
return errors.New(self.c.Tr.NoPullRequestForBranch)
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.OpenPullRequest)
|
||||
|
||||
return self.c.OS().OpenLink(pr.Url)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) GetOnRenderToMain() func() {
|
||||
return func() {
|
||||
self.c.Helpers().Diff.WithDiffModeCheck(func() {
|
||||
|
|
|
|||
|
|
@ -78,8 +78,7 @@ func (self *MenuController) close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
self.c.Context().Pop()
|
||||
return nil
|
||||
return self.context().OnMenuPress(nil)
|
||||
}
|
||||
|
||||
func (self *MenuController) context() *context.MenuContext {
|
||||
|
|
|
|||
|
|
@ -489,6 +489,8 @@ func (gui *Gui) onUserConfigLoaded() error {
|
|||
icons.SetNerdFontsVersion(userConfig.Gui.NerdFontsVersion)
|
||||
} else if userConfig.Gui.ShowIcons {
|
||||
icons.SetNerdFontsVersion("2")
|
||||
} else {
|
||||
icons.SetNerdFontsVersion("")
|
||||
}
|
||||
|
||||
if len(userConfig.Gui.BranchColorPatterns) > 0 {
|
||||
|
|
@ -599,6 +601,8 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
|
|||
Authors: map[string]*models.Author{},
|
||||
MainBranches: git_commands.NewMainBranches(gui.c.Common, gui.os.Cmd),
|
||||
HashPool: &utils.StringPool{},
|
||||
PullRequests: gui.loadCachedPullRequests(),
|
||||
PullRequestsMap: make(map[string]*models.GithubPullRequest),
|
||||
},
|
||||
Modes: &types.Modes{
|
||||
Filtering: filtering.New(startArgs.FilterPath, ""),
|
||||
|
|
@ -619,6 +623,24 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
|
|||
return initialContext(contextTree, startArgs)
|
||||
}
|
||||
|
||||
func (gui *Gui) loadCachedPullRequests() []*models.GithubPullRequest {
|
||||
repoPath := gui.git.RepoPaths.RepoPath()
|
||||
cachedPRs := gui.c.GetAppState().GithubPullRequests[repoPath]
|
||||
|
||||
return lo.Map(cachedPRs, func(cached config.CachedPullRequest, _ int) *models.GithubPullRequest {
|
||||
return &models.GithubPullRequest{
|
||||
HeadRefName: cached.HeadRefName,
|
||||
Number: cached.Number,
|
||||
Title: cached.Title,
|
||||
State: cached.State,
|
||||
Url: cached.Url,
|
||||
HeadRepositoryOwner: models.GithubRepositoryOwner{
|
||||
Login: cached.HeadRepositoryOwner,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) getViewBufferManagerForView(view *gocui.View) *tasks.ViewBufferManager {
|
||||
manager, ok := gui.viewBufferManagerMap[view.Name()]
|
||||
if !ok {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
|
|||
opts.Items = append(opts.Items, &types.MenuItem{
|
||||
LabelColumns: []string{gui.c.Tr.Cancel},
|
||||
OnPress: func() error {
|
||||
if opts.OnCancel != nil {
|
||||
return opts.OnCancel()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
|
@ -59,6 +62,7 @@ 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.SetOnCancel(opts.OnCancel)
|
||||
gui.State.Contexts.Menu.SetSelection(0)
|
||||
|
||||
gui.Views.Menu.SetOriginY(0)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ var colorPatterns *colorMatcher
|
|||
func GetBranchListDisplayStrings(
|
||||
branches []*models.Branch,
|
||||
getItemOperation func(item types.HasUrn) types.ItemOperation,
|
||||
prs map[string]*models.GithubPullRequest,
|
||||
fullDescription bool,
|
||||
diffName string,
|
||||
viewWidth int,
|
||||
|
|
@ -37,7 +38,7 @@ func GetBranchListDisplayStrings(
|
|||
) [][]string {
|
||||
return lo.Map(branches, func(branch *models.Branch, _ int) []string {
|
||||
diffed := branch.Name == diffName
|
||||
return getBranchDisplayStrings(branch, getItemOperation(branch), fullDescription, diffed, viewWidth, tr, userConfig, worktrees, time.Now())
|
||||
return getBranchDisplayStrings(branch, getItemOperation(branch), fullDescription, diffed, viewWidth, tr, userConfig, worktrees, time.Now(), prs)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +53,7 @@ func getBranchDisplayStrings(
|
|||
userConfig *config.UserConfig,
|
||||
worktrees []*models.Worktree,
|
||||
now time.Time,
|
||||
prs map[string]*models.GithubPullRequest,
|
||||
) []string {
|
||||
checkedOutByWorkTree := git_commands.CheckedOutByOtherWorktree(b, worktrees)
|
||||
showCommitHash := fullDescription || userConfig.Gui.ShowBranchCommitHash
|
||||
|
|
@ -63,12 +65,13 @@ func getBranchDisplayStrings(
|
|||
if len(divergence) > 0 {
|
||||
availableWidth -= utils.StringWidth(divergence) + 1
|
||||
}
|
||||
if icons.IsIconEnabled() {
|
||||
availableWidth -= 2 // one for the icon, one for the space
|
||||
}
|
||||
if showCommitHash {
|
||||
availableWidth -= utils.COMMIT_HASH_SHORT_SIZE + 1
|
||||
}
|
||||
if len(prs) > 0 {
|
||||
// if we have PRs then we assume that at least one branch in the list has one
|
||||
availableWidth -= 2
|
||||
}
|
||||
paddingNeededForDivergence := availableWidth
|
||||
|
||||
displayName := b.Name
|
||||
|
|
@ -136,9 +139,18 @@ func getBranchDisplayStrings(
|
|||
res := make([]string, 0, 6)
|
||||
res = append(res, recencyColor.Sprint(b.Recency))
|
||||
|
||||
if icons.IsIconEnabled() {
|
||||
res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b)))
|
||||
var coloredPrIcon string
|
||||
pr, hasPr := prs[b.Name]
|
||||
if hasPr {
|
||||
var prIcon string
|
||||
if icons.IsIconEnabled() {
|
||||
prIcon = icons.IconForRemoteUrl(pr.Url)
|
||||
} else {
|
||||
prIcon = "●"
|
||||
}
|
||||
coloredPrIcon = prColor(pr.State).Sprint(prIcon)
|
||||
}
|
||||
res = append(res, coloredPrIcon)
|
||||
|
||||
if showCommitHash {
|
||||
res = append(res, utils.ShortHash(b.CommitHash))
|
||||
|
|
@ -258,3 +270,18 @@ func SetCustomBranches(customBranchColors map[string]string, isRegex bool) {
|
|||
isRegex: isRegex,
|
||||
}
|
||||
}
|
||||
|
||||
func prColor(state string) style.TextStyle {
|
||||
switch state {
|
||||
case "OPEN":
|
||||
return style.FgGreen
|
||||
case "CLOSED":
|
||||
return style.FgRed
|
||||
case "MERGED":
|
||||
return style.FgMagenta
|
||||
case "DRAFT":
|
||||
return style.FgBlackLighter
|
||||
default:
|
||||
return style.FgDefault
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_name"},
|
||||
expected: []string{"1m", "", "branch_name"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "🍉_special_char", Recency: "1m"},
|
||||
|
|
@ -52,7 +52,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "🍉_special_char"},
|
||||
expected: []string{"1m", "", "🍉_special_char"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
||||
|
|
@ -62,7 +62,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: true,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_name (worktree other-worktree)"},
|
||||
expected: []string{"1m", "", "branch_name (worktree other-worktree)"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
||||
|
|
@ -72,7 +72,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: true,
|
||||
checkedOutByWorktree: true,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "", "branch_name ( other-worktree)"},
|
||||
expected: []string{"1m", "", "branch_name ( other-worktree)"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
|
|
@ -88,7 +88,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_name ✓"},
|
||||
expected: []string{"1m", "", "branch_name ✓"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
|
|
@ -104,7 +104,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: true,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_name (worktree other-worktree) ↓5↑3"},
|
||||
expected: []string{"1m", "", "branch_name (worktree other-worktree) ↓5↑3"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
|
|
@ -118,7 +118,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "onlyArrow",
|
||||
expected: []string{"1m", "branch_name ↓"},
|
||||
expected: []string{"1m", "", "branch_name ↓"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
|
|
@ -135,7 +135,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "arrowAndNumber",
|
||||
expected: []string{"1m", "branch_name ✓ ↓2"},
|
||||
expected: []string{"1m", "", "branch_name ✓ ↓2"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
|
|
@ -152,7 +152,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "arrowAndNumber",
|
||||
expected: []string{"1m", "branch_name ↓5↑3 ↓2"},
|
||||
expected: []string{"1m", "", "branch_name ↓5↑3 ↓2"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
||||
|
|
@ -162,7 +162,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_name Pushing |"},
|
||||
expected: []string{"1m", "", "branch_name Pushing |"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
|
|
@ -181,7 +181,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "12345678", "branch_name ✓", "origin branch_name", "commit title"},
|
||||
expected: []string{"1m", "", "12345678", "branch_name ✓", "origin branch_name", "commit title"},
|
||||
},
|
||||
|
||||
// Now tests for how we truncate the branch name when there's not enough room:
|
||||
|
|
@ -193,7 +193,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_na…"},
|
||||
expected: []string{"1m", "", "branch_na…"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "🍉_special_char", Recency: "1m"},
|
||||
|
|
@ -203,7 +203,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "🍉_special_ch…"},
|
||||
expected: []string{"1m", "", "🍉_special_ch…"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
||||
|
|
@ -213,17 +213,17 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: true,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "bra… (worktree)"},
|
||||
expected: []string{"1m", "", "bra… (worktree)"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
||||
itemOperation: types.ItemOperationNone,
|
||||
fullDescription: false,
|
||||
viewWidth: 14,
|
||||
viewWidth: 12,
|
||||
useIcons: true,
|
||||
checkedOutByWorktree: true,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "", "branc… "},
|
||||
expected: []string{"1m", "", "branc… "},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
|
|
@ -239,7 +239,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_… ✓"},
|
||||
expected: []string{"1m", "", "branch_… ✓"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
|
|
@ -256,7 +256,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "arrowAndNumber",
|
||||
expected: []string{"1m", "branch_n… ↓5↑3 ↓4"},
|
||||
expected: []string{"1m", "", "branch_n… ↓5↑3 ↓4"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
|
|
@ -272,7 +272,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: true,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branch_na… (worktree) ↓5↑3"},
|
||||
expected: []string{"1m", "", "branch_na… (worktree) ↓5↑3"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
||||
|
|
@ -282,7 +282,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "branc… Pushing |"},
|
||||
expected: []string{"1m", "", "branc… Pushing |"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "abc", Recency: "1m"},
|
||||
|
|
@ -292,7 +292,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "abc Pushing |"},
|
||||
expected: []string{"1m", "", "abc Pushing |"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "ab", Recency: "1m"},
|
||||
|
|
@ -302,7 +302,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "ab Pushing |"},
|
||||
expected: []string{"1m", "", "ab Pushing |"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{Name: "a", Recency: "1m"},
|
||||
|
|
@ -312,7 +312,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "a Pushing |"},
|
||||
expected: []string{"1m", "", "a Pushing |"},
|
||||
},
|
||||
{
|
||||
branch: &models.Branch{
|
||||
|
|
@ -331,7 +331,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
useIcons: false,
|
||||
checkedOutByWorktree: false,
|
||||
showDivergenceCfg: "none",
|
||||
expected: []string{"1m", "12345678", "bran… ✓", "origin branch_name", "commit title"},
|
||||
expected: []string{"1m", "", "12345678", "bran… ✓", "origin branch_name", "commit title"},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -351,7 +351,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run(fmt.Sprintf("getBranchDisplayStrings_%d", i), func(t *testing.T) {
|
||||
strings := getBranchDisplayStrings(s.branch, s.itemOperation, s.fullDescription, false, s.viewWidth, c.Tr, c.UserConfig(), worktrees, time.Time{})
|
||||
strings := getBranchDisplayStrings(s.branch, s.itemOperation, s.fullDescription, false, s.viewWidth, c.Tr, c.UserConfig(), worktrees, time.Time{}, map[string]*models.GithubPullRequest{})
|
||||
assert.Equal(t, s.expected, strings)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,15 @@ func IconForRemote(remote *models.Remote) string {
|
|||
return DEFAULT_REMOTE_ICON
|
||||
}
|
||||
|
||||
func IconForRemoteUrl(url string) string {
|
||||
for domain, icon := range remoteIcons {
|
||||
if strings.Contains(url, domain) {
|
||||
return icon
|
||||
}
|
||||
}
|
||||
return DEFAULT_REMOTE_ICON
|
||||
}
|
||||
|
||||
func IconForStash(stash *models.StashEntry) string {
|
||||
return STASH_ICON
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ type CreateMenuOptions struct {
|
|||
Prompt string // a message that will be displayed above the menu options
|
||||
Items []*MenuItem
|
||||
HideCancel bool
|
||||
OnCancel func() error // called when the menu is dismissed without selecting an item
|
||||
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
|
||||
|
|
@ -287,15 +288,17 @@ func (self *MenuItem) ID() string {
|
|||
}
|
||||
|
||||
type Model struct {
|
||||
CommitFiles []*models.CommitFile
|
||||
Files []*models.File
|
||||
Submodules []*models.SubmoduleConfig
|
||||
Branches []*models.Branch
|
||||
Commits []*models.Commit
|
||||
StashEntries []*models.StashEntry
|
||||
SubCommits []*models.Commit
|
||||
Remotes []*models.Remote
|
||||
Worktrees []*models.Worktree
|
||||
CommitFiles []*models.CommitFile
|
||||
Files []*models.File
|
||||
Submodules []*models.SubmoduleConfig
|
||||
Branches []*models.Branch
|
||||
Commits []*models.Commit
|
||||
StashEntries []*models.StashEntry
|
||||
SubCommits []*models.Commit
|
||||
Remotes []*models.Remote
|
||||
Worktrees []*models.Worktree
|
||||
PullRequests []*models.GithubPullRequest
|
||||
PullRequestsMap map[string]*models.GithubPullRequest
|
||||
|
||||
// FilteredReflogCommits are the ones that appear in the reflog panel.
|
||||
// When in filtering mode we only include the ones that match the given path
|
||||
|
|
@ -326,15 +329,16 @@ type Model struct {
|
|||
}
|
||||
|
||||
type Mutexes struct {
|
||||
RefreshingFilesMutex deadlock.Mutex
|
||||
RefreshingBranchesMutex deadlock.Mutex
|
||||
RefreshingStatusMutex deadlock.Mutex
|
||||
LocalCommitsMutex deadlock.Mutex
|
||||
SubCommitsMutex deadlock.Mutex
|
||||
AuthorsMutex deadlock.Mutex
|
||||
SubprocessMutex deadlock.Mutex
|
||||
PopupMutex deadlock.Mutex
|
||||
PtyMutex deadlock.Mutex
|
||||
RefreshingFilesMutex deadlock.Mutex
|
||||
RefreshingBranchesMutex deadlock.Mutex
|
||||
RefreshingStatusMutex deadlock.Mutex
|
||||
RefreshingPullRequestsMutex deadlock.Mutex
|
||||
LocalCommitsMutex deadlock.Mutex
|
||||
SubCommitsMutex deadlock.Mutex
|
||||
AuthorsMutex deadlock.Mutex
|
||||
SubprocessMutex deadlock.Mutex
|
||||
PopupMutex deadlock.Mutex
|
||||
PtyMutex deadlock.Mutex
|
||||
}
|
||||
|
||||
// A long-running operation associated with an item. For example, we'll show
|
||||
|
|
|
|||
|
|
@ -4,4 +4,9 @@ type CheckoutRefOptions struct {
|
|||
WaitingStatus string
|
||||
EnvVars []string
|
||||
OnRefNotFound func(ref string) error
|
||||
|
||||
// Refreshing pull requests is necessary when checking out a branch that doesn't exist locally
|
||||
// (e.g. checking out a remote branch), but it not needed when checking out an existing local
|
||||
// branch or a detached head (e.g. a tag).
|
||||
RefreshPullRequests bool
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ const (
|
|||
PATCH_BUILDING
|
||||
MERGE_CONFLICTS
|
||||
COMMIT_FILES
|
||||
// not actually a view. Will refactor this later
|
||||
// not actually views. Will refactor this later
|
||||
BISECT_INFO
|
||||
PULL_REQUESTS
|
||||
)
|
||||
|
||||
type RefreshMode int
|
||||
|
|
|
|||
|
|
@ -282,6 +282,8 @@ type TranslationSet struct {
|
|||
AllBranchesLogGraphReverse string
|
||||
UnsupportedGitService string
|
||||
CopyPullRequestURL string
|
||||
OpenPullRequestInBrowser string
|
||||
NoPullRequestForBranch string
|
||||
NoBranchOnRemote string
|
||||
Fetch string
|
||||
FetchTooltip string
|
||||
|
|
@ -609,6 +611,8 @@ type TranslationSet struct {
|
|||
CyclePagersDisabledReason string
|
||||
StartSearch string
|
||||
StartFilter string
|
||||
SelectRemoteRepository string
|
||||
FetchingPullRequests string
|
||||
Keybindings string
|
||||
KeybindingsLegend string
|
||||
KeybindingsMenuSectionLocal string
|
||||
|
|
@ -1395,6 +1399,8 @@ func EnglishTranslationSet() *TranslationSet {
|
|||
UnsupportedGitService: `Unsupported git service`,
|
||||
CreatePullRequest: `Create pull request`,
|
||||
CopyPullRequestURL: `Copy pull request URL to clipboard`,
|
||||
OpenPullRequestInBrowser: `Open pull request in browser`,
|
||||
NoPullRequestForBranch: `No pull request found for this branch`,
|
||||
NoBranchOnRemote: `This branch doesn't exist on remote. You need to push it to remote first.`,
|
||||
Fetch: `Fetch`,
|
||||
FetchTooltip: "Fetch changes from remote.",
|
||||
|
|
@ -1730,6 +1736,8 @@ func EnglishTranslationSet() *TranslationSet {
|
|||
CyclePagersDisabledReason: "No other pagers configured",
|
||||
StartSearch: "Search the current view by text",
|
||||
StartFilter: "Filter the current view by text",
|
||||
SelectRemoteRepository: "Select base repository for pull requests",
|
||||
FetchingPullRequests: "Fetching pull requests",
|
||||
KeybindingsLegend: "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
|
||||
RenameBranch: "Rename branch",
|
||||
BranchUpstreamOptionsTitle: "Upstream options",
|
||||
|
|
|
|||
|
|
@ -861,6 +861,10 @@
|
|||
"type": "string",
|
||||
"default": "O"
|
||||
},
|
||||
"openPullRequestInBrowser": {
|
||||
"type": "string",
|
||||
"default": "G"
|
||||
},
|
||||
"copyPullRequestURL": {
|
||||
"type": "string",
|
||||
"default": "\u003cc-y\u003e"
|
||||
|
|
@ -1043,6 +1047,10 @@
|
|||
"type": "string",
|
||||
"default": "o"
|
||||
},
|
||||
"openPullRequestInBrowser": {
|
||||
"type": "string",
|
||||
"default": "G"
|
||||
},
|
||||
"viewBisectOptions": {
|
||||
"type": "string",
|
||||
"default": "b"
|
||||
|
|
|
|||
21
vendor/github.com/cli/go-gh/v2/LICENSE
generated
vendored
Normal file
21
vendor/github.com/cli/go-gh/v2/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 GitHub Inc.
|
||||
|
||||
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.
|
||||
70
vendor/github.com/cli/go-gh/v2/internal/set/string_set.go
generated
vendored
Normal file
70
vendor/github.com/cli/go-gh/v2/internal/set/string_set.go
generated
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package set
|
||||
|
||||
var exists = struct{}{}
|
||||
|
||||
type stringSet struct {
|
||||
v []string
|
||||
m map[string]struct{}
|
||||
}
|
||||
|
||||
func NewStringSet() *stringSet {
|
||||
s := &stringSet{}
|
||||
s.m = make(map[string]struct{})
|
||||
s.v = []string{}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *stringSet) Add(value string) {
|
||||
if s.Contains(value) {
|
||||
return
|
||||
}
|
||||
s.m[value] = exists
|
||||
s.v = append(s.v, value)
|
||||
}
|
||||
|
||||
func (s *stringSet) AddValues(values []string) {
|
||||
for _, v := range values {
|
||||
s.Add(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stringSet) Remove(value string) {
|
||||
if !s.Contains(value) {
|
||||
return
|
||||
}
|
||||
delete(s.m, value)
|
||||
s.v = sliceWithout(s.v, value)
|
||||
}
|
||||
|
||||
func sliceWithout(s []string, v string) []string {
|
||||
idx := -1
|
||||
for i, item := range s {
|
||||
if item == v {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return s
|
||||
}
|
||||
return append(s[:idx], s[idx+1:]...)
|
||||
}
|
||||
|
||||
func (s *stringSet) RemoveValues(values []string) {
|
||||
for _, v := range values {
|
||||
s.Remove(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stringSet) Contains(value string) bool {
|
||||
_, c := s.m[value]
|
||||
return c
|
||||
}
|
||||
|
||||
func (s *stringSet) Len() int {
|
||||
return len(s.m)
|
||||
}
|
||||
|
||||
func (s *stringSet) ToSlice() []string {
|
||||
return s.v
|
||||
}
|
||||
210
vendor/github.com/cli/go-gh/v2/internal/yamlmap/yaml_map.go
generated
vendored
Normal file
210
vendor/github.com/cli/go-gh/v2/internal/yamlmap/yaml_map.go
generated
vendored
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
// Package yamlmap is a wrapper of gopkg.in/yaml.v3 for interacting
|
||||
// with yaml data as if it were a map.
|
||||
package yamlmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
modified = "modifed"
|
||||
)
|
||||
|
||||
type Map struct {
|
||||
*yaml.Node
|
||||
}
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
var ErrInvalidYaml = errors.New("invalid yaml")
|
||||
var ErrInvalidFormat = errors.New("invalid format")
|
||||
|
||||
func StringValue(value string) *Map {
|
||||
return &Map{&yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!str",
|
||||
Value: value,
|
||||
}}
|
||||
}
|
||||
|
||||
func MapValue() *Map {
|
||||
return &Map{&yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
Tag: "!!map",
|
||||
}}
|
||||
}
|
||||
|
||||
func NullValue() *Map {
|
||||
return &Map{&yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!null",
|
||||
}}
|
||||
}
|
||||
|
||||
func Unmarshal(data []byte) (*Map, error) {
|
||||
var root yaml.Node
|
||||
err := yaml.Unmarshal(data, &root)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidYaml
|
||||
}
|
||||
if len(root.Content) == 0 {
|
||||
return MapValue(), nil
|
||||
}
|
||||
if root.Content[0].Kind != yaml.MappingNode {
|
||||
return nil, ErrInvalidFormat
|
||||
}
|
||||
return &Map{root.Content[0]}, nil
|
||||
}
|
||||
|
||||
func Marshal(m *Map) ([]byte, error) {
|
||||
return yaml.Marshal(m.Node)
|
||||
}
|
||||
|
||||
func (m *Map) AddEntry(key string, value *Map) {
|
||||
keyNode := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!str",
|
||||
Value: key,
|
||||
}
|
||||
m.Content = append(m.Content, keyNode, value.Node)
|
||||
m.SetModified()
|
||||
}
|
||||
|
||||
func (m *Map) Empty() bool {
|
||||
return len(m.Content) == 0
|
||||
}
|
||||
|
||||
func (m *Map) FindEntry(key string) (*Map, error) {
|
||||
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
|
||||
// When iterating over the content slice we only want to compare the keys of the yamlMap.
|
||||
for i, v := range m.Content {
|
||||
if i%2 != 0 {
|
||||
continue
|
||||
}
|
||||
if v.Value == key {
|
||||
if i+1 < len(m.Content) {
|
||||
return &Map{m.Content[i+1]}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func (m *Map) Keys() []string {
|
||||
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
|
||||
// When iterating over the content slice we only want to select the keys of the yamlMap.
|
||||
keys := []string{}
|
||||
for i, v := range m.Content {
|
||||
if i%2 != 0 {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, v.Value)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (m *Map) RemoveEntry(key string) error {
|
||||
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
|
||||
// When iterating over the content slice we only want to compare the keys of the yamlMap.
|
||||
// If we find they key to remove, remove the key and its value from the content slice.
|
||||
found, skipNext := false, false
|
||||
newContent := []*yaml.Node{}
|
||||
for i, v := range m.Content {
|
||||
if skipNext {
|
||||
skipNext = false
|
||||
continue
|
||||
}
|
||||
if i%2 != 0 || v.Value != key {
|
||||
newContent = append(newContent, v)
|
||||
} else {
|
||||
found = true
|
||||
skipNext = true
|
||||
m.SetModified()
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return ErrNotFound
|
||||
}
|
||||
m.Content = newContent
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Map) SetEntry(key string, value *Map) {
|
||||
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
|
||||
// When iterating over the content slice we only want to compare the keys of the yamlMap.
|
||||
// If we find they key to set, set the next item in the content slice to the new value.
|
||||
m.SetModified()
|
||||
for i, v := range m.Content {
|
||||
if i%2 != 0 || v.Value != key {
|
||||
continue
|
||||
}
|
||||
if v.Value == key {
|
||||
if i+1 < len(m.Content) {
|
||||
m.Content[i+1] = value.Node
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
m.AddEntry(key, value)
|
||||
}
|
||||
|
||||
// SetModified marks the map as modified.
|
||||
//
|
||||
// Note: This is a hack to introduce the concept of modified/unmodified
|
||||
// on top of gopkg.in/yaml.v3. This works by setting the Value property
|
||||
// of a MappingNode to a specific value and then later checking if the
|
||||
// node's Value property is that specific value. When a MappingNode gets
|
||||
// output as a string the Value property is not used, thus changing it
|
||||
// has no impact for our purposes.
|
||||
func (m *Map) SetModified() {
|
||||
// Can not mark a non-mapping node as modified
|
||||
if m.Node.Kind != yaml.MappingNode && m.Node.Tag == "!!null" {
|
||||
m.Node.Kind = yaml.MappingNode
|
||||
m.Node.Tag = "!!map"
|
||||
}
|
||||
if m.Node.Kind == yaml.MappingNode {
|
||||
m.Node.Value = modified
|
||||
}
|
||||
}
|
||||
|
||||
// SetUnmodified traverses the map using BFS to set all nodes as unmodified.
|
||||
func (m *Map) SetUnmodified() {
|
||||
i := 0
|
||||
queue := []*yaml.Node{m.Node}
|
||||
for i < len(queue) {
|
||||
q := queue[i]
|
||||
i = i + 1
|
||||
if q.Kind != yaml.MappingNode {
|
||||
continue
|
||||
}
|
||||
q.Value = ""
|
||||
queue = append(queue, q.Content...)
|
||||
}
|
||||
}
|
||||
|
||||
// IsModified traverses the map using BFS to search for any nodes that have been modified.
|
||||
func (m *Map) IsModified() bool {
|
||||
i := 0
|
||||
queue := []*yaml.Node{m.Node}
|
||||
for i < len(queue) {
|
||||
q := queue[i]
|
||||
i = i + 1
|
||||
if q.Kind != yaml.MappingNode {
|
||||
continue
|
||||
}
|
||||
if q.Value == modified {
|
||||
return true
|
||||
}
|
||||
queue = append(queue, q.Content...)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Map) String() string {
|
||||
data, err := Marshal(m)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
203
vendor/github.com/cli/go-gh/v2/pkg/auth/auth.go
generated
vendored
Normal file
203
vendor/github.com/cli/go-gh/v2/pkg/auth/auth.go
generated
vendored
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
// Package auth is a set of functions for retrieving authentication tokens
|
||||
// and authenticated hosts.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/go-gh/v2/internal/set"
|
||||
"github.com/cli/go-gh/v2/pkg/config"
|
||||
"github.com/cli/safeexec"
|
||||
)
|
||||
|
||||
const (
|
||||
codespaces = "CODESPACES"
|
||||
defaultSource = "default"
|
||||
ghEnterpriseToken = "GH_ENTERPRISE_TOKEN"
|
||||
ghHost = "GH_HOST"
|
||||
ghToken = "GH_TOKEN"
|
||||
github = "github.com"
|
||||
githubEnterpriseToken = "GITHUB_ENTERPRISE_TOKEN"
|
||||
githubToken = "GITHUB_TOKEN"
|
||||
hostsKey = "hosts"
|
||||
localhost = "github.localhost"
|
||||
oauthToken = "oauth_token"
|
||||
tenancyHost = "ghe.com" // TenancyHost is the domain suffix of a tenancy GitHub instance.
|
||||
)
|
||||
|
||||
// TokenForHost retrieves an authentication token and the source of that token for the specified
|
||||
// host. The source can be either an environment variable, configuration file, or the system
|
||||
// keyring. In the latter case, this shells out to "gh auth token" to obtain the token.
|
||||
//
|
||||
// Returns "", "default" if no applicable token is found.
|
||||
func TokenForHost(host string) (string, string) {
|
||||
if token, source := TokenFromEnvOrConfig(host); token != "" {
|
||||
return token, source
|
||||
}
|
||||
|
||||
ghExe := os.Getenv("GH_PATH")
|
||||
if ghExe == "" {
|
||||
ghExe, _ = safeexec.LookPath("gh")
|
||||
}
|
||||
|
||||
if ghExe != "" {
|
||||
if token, source := tokenFromGh(ghExe, host); token != "" {
|
||||
return token, source
|
||||
}
|
||||
}
|
||||
|
||||
return "", defaultSource
|
||||
}
|
||||
|
||||
// TokenFromEnvOrConfig retrieves an authentication token from environment variables or the config
|
||||
// file as fallback, but does not support reading the token from system keyring. Most consumers
|
||||
// should use TokenForHost.
|
||||
func TokenFromEnvOrConfig(host string) (string, string) {
|
||||
cfg, _ := config.Read(nil)
|
||||
return tokenForHost(cfg, host)
|
||||
}
|
||||
|
||||
func tokenForHost(cfg *config.Config, host string) (string, string) {
|
||||
normalizedHost := NormalizeHostname(host)
|
||||
// This code is currently the exact opposite of IsEnterprise. However, we have chosen
|
||||
// to write it separately, directly in line, because it is much clearer in the exact
|
||||
// scenarios that we expect to use GH_TOKEN and GITHUB_TOKEN.
|
||||
if normalizedHost == github || IsTenancy(normalizedHost) || normalizedHost == localhost {
|
||||
if token := os.Getenv(ghToken); token != "" {
|
||||
return token, ghToken
|
||||
}
|
||||
|
||||
if token := os.Getenv(githubToken); token != "" {
|
||||
return token, githubToken
|
||||
}
|
||||
} else {
|
||||
if token := os.Getenv(ghEnterpriseToken); token != "" {
|
||||
return token, ghEnterpriseToken
|
||||
}
|
||||
|
||||
if token := os.Getenv(githubEnterpriseToken); token != "" {
|
||||
return token, githubEnterpriseToken
|
||||
}
|
||||
}
|
||||
|
||||
// If config is nil, something has failed much earlier and it's probably
|
||||
// more correct to panic because we don't expect to support anything
|
||||
// where the config isn't available, but that would be a breaking change,
|
||||
// so it's worth thinking about carefully, if we wanted to rework this.
|
||||
if cfg == nil {
|
||||
return "", defaultSource
|
||||
}
|
||||
|
||||
token, err := cfg.Get([]string{hostsKey, normalizedHost, oauthToken})
|
||||
if err != nil {
|
||||
return "", defaultSource
|
||||
}
|
||||
|
||||
return token, oauthToken
|
||||
}
|
||||
|
||||
func tokenFromGh(path string, host string) (string, string) {
|
||||
cmd := exec.Command(path, "auth", "token", "--secure-storage", "--hostname", host)
|
||||
result, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", "gh"
|
||||
}
|
||||
return strings.TrimSpace(string(result)), "gh"
|
||||
}
|
||||
|
||||
// KnownHosts retrieves a list of hosts that have corresponding
|
||||
// authentication tokens, either from environment variables
|
||||
// or from the configuration file.
|
||||
// Returns an empty string slice if no hosts are found.
|
||||
func KnownHosts() []string {
|
||||
cfg, _ := config.Read(nil)
|
||||
return knownHosts(cfg)
|
||||
}
|
||||
|
||||
func knownHosts(cfg *config.Config) []string {
|
||||
hosts := set.NewStringSet()
|
||||
if host := os.Getenv(ghHost); host != "" {
|
||||
hosts.Add(host)
|
||||
}
|
||||
if token, _ := tokenForHost(cfg, github); token != "" {
|
||||
hosts.Add(github)
|
||||
}
|
||||
if cfg != nil {
|
||||
keys, err := cfg.Keys([]string{hostsKey})
|
||||
if err == nil {
|
||||
hosts.AddValues(keys)
|
||||
}
|
||||
}
|
||||
return hosts.ToSlice()
|
||||
}
|
||||
|
||||
// DefaultHost retrieves an authenticated host and the source of host.
|
||||
// The source can be either an environment variable or from the
|
||||
// configuration file.
|
||||
// Returns "github.com", "default" if no viable host is found.
|
||||
func DefaultHost() (string, string) {
|
||||
cfg, _ := config.Read(nil)
|
||||
return defaultHost(cfg)
|
||||
}
|
||||
|
||||
func defaultHost(cfg *config.Config) (string, string) {
|
||||
if host := os.Getenv(ghHost); host != "" {
|
||||
return host, ghHost
|
||||
}
|
||||
if cfg != nil {
|
||||
keys, err := cfg.Keys([]string{hostsKey})
|
||||
if err == nil && len(keys) == 1 {
|
||||
return keys[0], hostsKey
|
||||
}
|
||||
}
|
||||
return github, defaultSource
|
||||
}
|
||||
|
||||
// IsEnterprise determines if a provided host is a GitHub Enterprise Server instance,
|
||||
// rather than GitHub.com, a tenancy GitHub instance, or github.localhost.
|
||||
func IsEnterprise(host string) bool {
|
||||
// Note that if you are making changes here, you should also consider making the equivalent
|
||||
// in tokenForHost, which is the exact opposite of this function.
|
||||
normalizedHost := NormalizeHostname(host)
|
||||
return normalizedHost != github && normalizedHost != localhost && !IsTenancy(normalizedHost)
|
||||
}
|
||||
|
||||
// IsTenancy determines if a provided host is a tenancy GitHub instance,
|
||||
// rather than GitHub.com or a GitHub Enterprise Server instance.
|
||||
func IsTenancy(host string) bool {
|
||||
normalizedHost := NormalizeHostname(host)
|
||||
return strings.HasSuffix(normalizedHost, "."+tenancyHost)
|
||||
}
|
||||
|
||||
// NormalizeHostname ensures the host matches the values used throughout
|
||||
// the rest of the codebase with respect to hostnames. These are github,
|
||||
// localhost, and tenancyHost.
|
||||
func NormalizeHostname(host string) string {
|
||||
hostname := strings.ToLower(host)
|
||||
if strings.HasSuffix(hostname, "."+github) {
|
||||
return github
|
||||
}
|
||||
if strings.HasSuffix(hostname, "."+localhost) {
|
||||
return localhost
|
||||
}
|
||||
// This has been copied over from the cli/cli NormalizeHostname function
|
||||
// to ensure compatible behaviour but we don't fully understand when or
|
||||
// why it would be useful here. We can't see what harm will come of
|
||||
// duplicating the logic.
|
||||
if before, found := cutSuffix(hostname, "."+tenancyHost); found {
|
||||
idx := strings.LastIndex(before, ".")
|
||||
return fmt.Sprintf("%s.%s", before[idx+1:], tenancyHost)
|
||||
}
|
||||
return hostname
|
||||
}
|
||||
|
||||
// Backport strings.CutSuffix from Go 1.20.
|
||||
func cutSuffix(s, suffix string) (string, bool) {
|
||||
if !strings.HasSuffix(s, suffix) {
|
||||
return s, false
|
||||
}
|
||||
return s[:len(s)-len(suffix)], true
|
||||
}
|
||||
344
vendor/github.com/cli/go-gh/v2/pkg/config/config.go
generated
vendored
Normal file
344
vendor/github.com/cli/go-gh/v2/pkg/config/config.go
generated
vendored
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
// Package config is a set of types for interacting with the gh configuration files.
|
||||
// Note: This package is intended for use only in gh, any other use cases are subject
|
||||
// to breakage and non-backwards compatible updates.
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/cli/go-gh/v2/internal/yamlmap"
|
||||
)
|
||||
|
||||
const (
|
||||
appData = "AppData"
|
||||
ghConfigDir = "GH_CONFIG_DIR"
|
||||
localAppData = "LocalAppData"
|
||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
||||
xdgDataHome = "XDG_DATA_HOME"
|
||||
xdgStateHome = "XDG_STATE_HOME"
|
||||
xdgCacheHome = "XDG_CACHE_HOME"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg *Config
|
||||
once sync.Once
|
||||
loadErr error
|
||||
)
|
||||
|
||||
// Config is a in memory representation of the gh configuration files.
|
||||
// It can be thought of as map where entries consist of a key that
|
||||
// correspond to either a string value or a map value, allowing for
|
||||
// multi-level maps.
|
||||
type Config struct {
|
||||
entries *yamlmap.Map
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Get a string value from a Config.
|
||||
// The keys argument is a sequence of key values so that nested
|
||||
// entries can be retrieved. A undefined string will be returned
|
||||
// if trying to retrieve a key that corresponds to a map value.
|
||||
// Returns "", KeyNotFoundError if any of the keys can not be found.
|
||||
func (c *Config) Get(keys []string) (string, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
m := c.entries
|
||||
for _, key := range keys {
|
||||
var err error
|
||||
m, err = m.FindEntry(key)
|
||||
if err != nil {
|
||||
return "", &KeyNotFoundError{key}
|
||||
}
|
||||
}
|
||||
return m.Value, nil
|
||||
}
|
||||
|
||||
// Keys enumerates a Config's keys.
|
||||
// The keys argument is a sequence of key values so that nested
|
||||
// map values can be have their keys enumerated.
|
||||
// Returns nil, KeyNotFoundError if any of the keys can not be found.
|
||||
func (c *Config) Keys(keys []string) ([]string, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
m := c.entries
|
||||
for _, key := range keys {
|
||||
var err error
|
||||
m, err = m.FindEntry(key)
|
||||
if err != nil {
|
||||
return nil, &KeyNotFoundError{key}
|
||||
}
|
||||
}
|
||||
return m.Keys(), nil
|
||||
}
|
||||
|
||||
// Remove an entry from a Config.
|
||||
// The keys argument is a sequence of key values so that nested
|
||||
// entries can be removed. Removing an entry that has nested
|
||||
// entries removes those also.
|
||||
// Returns KeyNotFoundError if any of the keys can not be found.
|
||||
func (c *Config) Remove(keys []string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
m := c.entries
|
||||
for i := 0; i < len(keys)-1; i++ {
|
||||
var err error
|
||||
key := keys[i]
|
||||
m, err = m.FindEntry(key)
|
||||
if err != nil {
|
||||
return &KeyNotFoundError{key}
|
||||
}
|
||||
}
|
||||
err := m.RemoveEntry(keys[len(keys)-1])
|
||||
if err != nil {
|
||||
return &KeyNotFoundError{keys[len(keys)-1]}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set a string value in a Config.
|
||||
// The keys argument is a sequence of key values so that nested
|
||||
// entries can be set. If any of the keys do not exist they will
|
||||
// be created. If the string value to be set is empty it will be
|
||||
// represented as null not an empty string when written.
|
||||
//
|
||||
// var c *Config
|
||||
// c.Set([]string{"key"}, "")
|
||||
// Write(c) // writes `key: ` not `key: ""`
|
||||
func (c *Config) Set(keys []string, value string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
m := c.entries
|
||||
for i := 0; i < len(keys)-1; i++ {
|
||||
key := keys[i]
|
||||
entry, err := m.FindEntry(key)
|
||||
if err != nil {
|
||||
entry = yamlmap.MapValue()
|
||||
m.AddEntry(key, entry)
|
||||
}
|
||||
m = entry
|
||||
}
|
||||
val := yamlmap.StringValue(value)
|
||||
if value == "" {
|
||||
val = yamlmap.NullValue()
|
||||
}
|
||||
m.SetEntry(keys[len(keys)-1], val)
|
||||
}
|
||||
|
||||
func (c *Config) deepCopy() *Config {
|
||||
return ReadFromString(c.entries.String())
|
||||
}
|
||||
|
||||
// Read gh configuration files from the local file system and
|
||||
// returns a Config. A copy of the fallback configuration will
|
||||
// be returned when there are no configuration files to load.
|
||||
// If there are no configuration files and no fallback configuration
|
||||
// an empty configuration will be returned.
|
||||
var Read = func(fallback *Config) (*Config, error) {
|
||||
once.Do(func() {
|
||||
cfg, loadErr = load(generalConfigFile(), hostsConfigFile(), fallback)
|
||||
})
|
||||
return cfg, loadErr
|
||||
}
|
||||
|
||||
// ReadFromString takes a yaml string and returns a Config.
|
||||
func ReadFromString(str string) *Config {
|
||||
m, _ := mapFromString(str)
|
||||
if m == nil {
|
||||
m = yamlmap.MapValue()
|
||||
}
|
||||
return &Config{entries: m}
|
||||
}
|
||||
|
||||
// Write gh configuration files to the local file system.
|
||||
// It will only write gh configuration files that have been modified
|
||||
// since last being read.
|
||||
func Write(c *Config) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
hosts, err := c.entries.FindEntry("hosts")
|
||||
if err == nil && hosts.IsModified() {
|
||||
err := writeFile(hostsConfigFile(), []byte(hosts.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hosts.SetUnmodified()
|
||||
}
|
||||
|
||||
if c.entries.IsModified() {
|
||||
// Hosts gets written to a different file above so remove it
|
||||
// before writing and add it back in after writing.
|
||||
hostsMap, hostsErr := c.entries.FindEntry("hosts")
|
||||
if hostsErr == nil {
|
||||
_ = c.entries.RemoveEntry("hosts")
|
||||
}
|
||||
err := writeFile(generalConfigFile(), []byte(c.entries.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.entries.SetUnmodified()
|
||||
if hostsErr == nil {
|
||||
c.entries.AddEntry("hosts", hostsMap)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func load(generalFilePath, hostsFilePath string, fallback *Config) (*Config, error) {
|
||||
generalMap, err := mapFromFile(generalFilePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
if errors.Is(err, yamlmap.ErrInvalidYaml) ||
|
||||
errors.Is(err, yamlmap.ErrInvalidFormat) {
|
||||
return nil, &InvalidConfigFileError{Path: generalFilePath, Err: err}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if generalMap == nil {
|
||||
generalMap = yamlmap.MapValue()
|
||||
}
|
||||
|
||||
hostsMap, err := mapFromFile(hostsFilePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
if errors.Is(err, yamlmap.ErrInvalidYaml) ||
|
||||
errors.Is(err, yamlmap.ErrInvalidFormat) {
|
||||
return nil, &InvalidConfigFileError{Path: hostsFilePath, Err: err}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hostsMap != nil && !hostsMap.Empty() {
|
||||
generalMap.AddEntry("hosts", hostsMap)
|
||||
generalMap.SetUnmodified()
|
||||
}
|
||||
|
||||
if generalMap.Empty() && fallback != nil {
|
||||
return fallback.deepCopy(), nil
|
||||
}
|
||||
|
||||
return &Config{entries: generalMap}, nil
|
||||
}
|
||||
|
||||
func generalConfigFile() string {
|
||||
return filepath.Join(ConfigDir(), "config.yml")
|
||||
}
|
||||
|
||||
func hostsConfigFile() string {
|
||||
return filepath.Join(ConfigDir(), "hosts.yml")
|
||||
}
|
||||
|
||||
func mapFromFile(filename string) (*yamlmap.Map, error) {
|
||||
data, err := readFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return yamlmap.Unmarshal(data)
|
||||
}
|
||||
|
||||
func mapFromString(str string) (*yamlmap.Map, error) {
|
||||
return yamlmap.Unmarshal([]byte(str))
|
||||
}
|
||||
|
||||
// ConfigDir returns the path to the configuration directory.
|
||||
//
|
||||
// Config path precedence: GH_CONFIG_DIR, XDG_CONFIG_HOME, AppData (windows only), HOME.
|
||||
func ConfigDir() string {
|
||||
var path string
|
||||
if a := os.Getenv(ghConfigDir); a != "" {
|
||||
path = a
|
||||
} else if b := os.Getenv(xdgConfigHome); b != "" {
|
||||
path = filepath.Join(b, "gh")
|
||||
} else if c := os.Getenv(appData); runtime.GOOS == "windows" && c != "" {
|
||||
path = filepath.Join(c, "GitHub CLI")
|
||||
} else {
|
||||
d, _ := os.UserHomeDir()
|
||||
path = filepath.Join(d, ".config", "gh")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// StateDir returns the path to the state directory.
|
||||
//
|
||||
// State path precedence: XDG_STATE_HOME, LocalAppData (windows only), HOME.
|
||||
func StateDir() string {
|
||||
var path string
|
||||
if a := os.Getenv(xdgStateHome); a != "" {
|
||||
path = filepath.Join(a, "gh")
|
||||
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
|
||||
path = filepath.Join(b, "GitHub CLI")
|
||||
} else {
|
||||
c, _ := os.UserHomeDir()
|
||||
path = filepath.Join(c, ".local", "state", "gh")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// DataDir returns the path to the data directory.
|
||||
//
|
||||
// Data path precedence: XDG_DATA_HOME, LocalAppData (windows only), HOME.
|
||||
func DataDir() string {
|
||||
var path string
|
||||
if a := os.Getenv(xdgDataHome); a != "" {
|
||||
path = filepath.Join(a, "gh")
|
||||
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
|
||||
path = filepath.Join(b, "GitHub CLI")
|
||||
} else {
|
||||
c, _ := os.UserHomeDir()
|
||||
path = filepath.Join(c, ".local", "share", "gh")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// CacheDir returns the path to the cache directory.
|
||||
//
|
||||
// Cache path precedence: XDG_CACHE_HOME, LocalAppData (windows only), HOME, legacy gh-cli-cache.
|
||||
func CacheDir() string {
|
||||
if a := os.Getenv(xdgCacheHome); a != "" {
|
||||
return filepath.Join(a, "gh")
|
||||
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
|
||||
return filepath.Join(b, "GitHub CLI")
|
||||
} else if c, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(c, ".cache", "gh")
|
||||
} else {
|
||||
// Note that this has a minor security issue because /tmp is world-writeable.
|
||||
// As such, it is possible for other users on a shared system to overwrite cached data.
|
||||
// The practical risk of this is low, but it's worth calling out as a risk.
|
||||
// I've included this here for backwards compatibility but we should consider removing it.
|
||||
return filepath.Join(os.TempDir(), "gh-cli-cache")
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(filename string) ([]byte, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func writeFile(filename string, data []byte) (writeErr error) {
|
||||
if writeErr = os.MkdirAll(filepath.Dir(filename), 0771); writeErr != nil {
|
||||
return
|
||||
}
|
||||
var file *os.File
|
||||
if file, writeErr = os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600); writeErr != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := file.Close(); writeErr == nil && err != nil {
|
||||
writeErr = err
|
||||
}
|
||||
}()
|
||||
_, writeErr = file.Write(data)
|
||||
return
|
||||
}
|
||||
32
vendor/github.com/cli/go-gh/v2/pkg/config/errors.go
generated
vendored
Normal file
32
vendor/github.com/cli/go-gh/v2/pkg/config/errors.go
generated
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// InvalidConfigFileError represents an error when trying to read a config file.
|
||||
type InvalidConfigFileError struct {
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error allows InvalidConfigFileError to satisfy error interface.
|
||||
func (e *InvalidConfigFileError) Error() string {
|
||||
return fmt.Sprintf("invalid config file %s: %s", e.Path, e.Err)
|
||||
}
|
||||
|
||||
// Unwrap allows InvalidConfigFileError to be unwrapped.
|
||||
func (e *InvalidConfigFileError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// KeyNotFoundError represents an error when trying to find a config key
|
||||
// that does not exist.
|
||||
type KeyNotFoundError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
// Error allows KeyNotFoundError to satisfy error interface.
|
||||
func (e *KeyNotFoundError) Error() string {
|
||||
return fmt.Sprintf("could not find key %q", e.Key)
|
||||
}
|
||||
25
vendor/github.com/cli/safeexec/LICENSE
generated
vendored
Normal file
25
vendor/github.com/cli/safeexec/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2020, GitHub Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. 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.
|
||||
|
||||
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 HOLDER 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.
|
||||
48
vendor/github.com/cli/safeexec/README.md
generated
vendored
Normal file
48
vendor/github.com/cli/safeexec/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# safeexec
|
||||
|
||||
A Go module that provides a stabler alternative to `exec.LookPath()` that:
|
||||
- Avoids a Windows security risk of executing commands found in the current directory; and
|
||||
- Allows executing commands found in PATH, even if they come from relative PATH entries.
|
||||
|
||||
This is an alternative to [`golang.org/x/sys/execabs`](https://pkg.go.dev/golang.org/x/sys/execabs).
|
||||
|
||||
## Usage
|
||||
```go
|
||||
import (
|
||||
"os/exec"
|
||||
"github.com/cli/safeexec"
|
||||
)
|
||||
|
||||
func gitStatus() error {
|
||||
gitBin, err := safeexec.LookPath("git")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(gitBin, "status")
|
||||
return cmd.Run()
|
||||
}
|
||||
```
|
||||
|
||||
## Background
|
||||
### Windows security vulnerability with Go <= 1.18
|
||||
Go 1.18 (and older) standard library has a security vulnerability when executing programs:
|
||||
```go
|
||||
import "os/exec"
|
||||
|
||||
func gitStatus() error {
|
||||
// On Windows, this will result in `.\git.exe` or `.\git.bat` being executed
|
||||
// if either were found in the current working directory.
|
||||
cmd := exec.Command("git", "status")
|
||||
return cmd.Run()
|
||||
}
|
||||
```
|
||||
|
||||
For historic reasons, Go used to implicitly [include the current directory](https://github.com/golang/go/issues/38736) in the PATH resolution on Windows. The `safeexec` package avoids searching the current directory on Windows.
|
||||
|
||||
### Relative PATH entries with Go 1.19+
|
||||
|
||||
Go 1.19 (and newer) standard library [throws an error](https://github.com/golang/go/issues/43724) if `exec.LookPath("git")` resolved to an executable relative to the current directory. This can happen on other platforms if the PATH environment variable contains relative entries, e.g. `PATH=./bin:$PATH`. The `safeexec` package allows respecting relative PATH entries as it assumes that the responsibility for keeping PATH safe lies outside of the Go program.
|
||||
|
||||
## TODO
|
||||
|
||||
Ideally, this module would also provide `exec.Command()` and `exec.CommandContext()` equivalents that delegate to the patched version of `LookPath`. However, this doesn't seem possible since `LookPath` may return an error, while `exec.Command/CommandContext()` themselves do not return an error. In the standard library, the resulting `exec.Cmd` struct stores the LookPath error in a private field, but that functionality isn't available to us.
|
||||
17
vendor/github.com/cli/safeexec/lookpath.go
generated
vendored
Normal file
17
vendor/github.com/cli/safeexec/lookpath.go
generated
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//go:build !windows && go1.19
|
||||
// +build !windows,go1.19
|
||||
|
||||
package safeexec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func LookPath(file string) (string, error) {
|
||||
path, err := exec.LookPath(file)
|
||||
if errors.Is(err, exec.ErrDot) {
|
||||
return path, nil
|
||||
}
|
||||
return path, err
|
||||
}
|
||||
10
vendor/github.com/cli/safeexec/lookpath_1.18.go
generated
vendored
Normal file
10
vendor/github.com/cli/safeexec/lookpath_1.18.go
generated
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//go:build !windows && !go1.19
|
||||
// +build !windows,!go1.19
|
||||
|
||||
package safeexec
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func LookPath(file string) (string, error) {
|
||||
return exec.LookPath(file)
|
||||
}
|
||||
120
vendor/github.com/cli/safeexec/lookpath_windows.go
generated
vendored
Normal file
120
vendor/github.com/cli/safeexec/lookpath_windows.go
generated
vendored
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// Copyright (c) 2009 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.
|
||||
|
||||
// Package safeexec provides alternatives for exec package functions to avoid
|
||||
// accidentally executing binaries found in the current working directory on
|
||||
// Windows.
|
||||
package safeexec
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func chkStat(file string) error {
|
||||
d, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return os.ErrPermission
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasExt(file string) bool {
|
||||
i := strings.LastIndex(file, ".")
|
||||
if i < 0 {
|
||||
return false
|
||||
}
|
||||
return strings.LastIndexAny(file, `:\/`) < i
|
||||
}
|
||||
|
||||
func findExecutable(file string, exts []string) (string, error) {
|
||||
if len(exts) == 0 {
|
||||
return file, chkStat(file)
|
||||
}
|
||||
if hasExt(file) {
|
||||
if chkStat(file) == nil {
|
||||
return file, nil
|
||||
}
|
||||
}
|
||||
for _, e := range exts {
|
||||
if f := file + e; chkStat(f) == nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
|
||||
// LookPath searches for an executable named file in the
|
||||
// directories named by the PATH environment variable.
|
||||
// If file contains a slash, it is tried directly and the PATH is not consulted.
|
||||
// LookPath also uses PATHEXT environment variable to match
|
||||
// a suitable candidate.
|
||||
// The result may be an absolute path or a path relative to the current directory.
|
||||
func LookPath(file string) (string, error) {
|
||||
var exts []string
|
||||
x := os.Getenv(`PATHEXT`)
|
||||
if x != "" {
|
||||
for _, e := range strings.Split(strings.ToLower(x), `;`) {
|
||||
if e == "" {
|
||||
continue
|
||||
}
|
||||
if e[0] != '.' {
|
||||
e = "." + e
|
||||
}
|
||||
exts = append(exts, e)
|
||||
}
|
||||
} else {
|
||||
exts = []string{".com", ".exe", ".bat", ".cmd"}
|
||||
}
|
||||
|
||||
if strings.ContainsAny(file, `:\/`) {
|
||||
if f, err := findExecutable(file, exts); err == nil {
|
||||
return f, nil
|
||||
} else {
|
||||
return "", &exec.Error{file, err}
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/golang/go/issues/38736
|
||||
// if f, err := findExecutable(filepath.Join(".", file), exts); err == nil {
|
||||
// return f, nil
|
||||
// }
|
||||
|
||||
path := os.Getenv("path")
|
||||
for _, dir := range filepath.SplitList(path) {
|
||||
if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return "", &exec.Error{file, exec.ErrNotFound}
|
||||
}
|
||||
2
vendor/github.com/mattn/go-colorable/noncolorable.go
generated
vendored
2
vendor/github.com/mattn/go-colorable/noncolorable.go
generated
vendored
|
|
@ -42,7 +42,6 @@ loop:
|
|||
continue
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for {
|
||||
c, err := er.ReadByte()
|
||||
if err != nil {
|
||||
|
|
@ -51,7 +50,6 @@ loop:
|
|||
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
|
||||
break
|
||||
}
|
||||
buf.Write([]byte(string(c)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
5
vendor/github.com/mattn/go-isatty/isatty_bsd.go
generated
vendored
5
vendor/github.com/mattn/go-isatty/isatty_bsd.go
generated
vendored
|
|
@ -1,6 +1,7 @@
|
|||
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly) && !appengine
|
||||
// +build darwin freebsd openbsd netbsd dragonfly
|
||||
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly || hurd) && !appengine && !tinygo
|
||||
// +build darwin freebsd openbsd netbsd dragonfly hurd
|
||||
// +build !appengine
|
||||
// +build !tinygo
|
||||
|
||||
package isatty
|
||||
|
||||
|
|
|
|||
5
vendor/github.com/mattn/go-isatty/isatty_others.go
generated
vendored
5
vendor/github.com/mattn/go-isatty/isatty_others.go
generated
vendored
|
|
@ -1,5 +1,6 @@
|
|||
//go:build appengine || js || nacl || wasm
|
||||
// +build appengine js nacl wasm
|
||||
//go:build (appengine || js || nacl || tinygo || wasm) && !windows
|
||||
// +build appengine js nacl tinygo wasm
|
||||
// +build !windows
|
||||
|
||||
package isatty
|
||||
|
||||
|
|
|
|||
3
vendor/github.com/mattn/go-isatty/isatty_tcgets.go
generated
vendored
3
vendor/github.com/mattn/go-isatty/isatty_tcgets.go
generated
vendored
|
|
@ -1,6 +1,7 @@
|
|||
//go:build (linux || aix || zos) && !appengine
|
||||
//go:build (linux || aix || zos) && !appengine && !tinygo
|
||||
// +build linux aix zos
|
||||
// +build !appengine
|
||||
// +build !tinygo
|
||||
|
||||
package isatty
|
||||
|
||||
|
|
|
|||
2
vendor/github.com/xo/terminfo/caps.go
generated
vendored
2
vendor/github.com/xo/terminfo/caps.go
generated
vendored
|
|
@ -1,7 +1,5 @@
|
|||
package terminfo
|
||||
|
||||
//go:generate go run gen.go
|
||||
|
||||
// BoolCapName returns the bool capability name.
|
||||
func BoolCapName(i int) string {
|
||||
return boolCapNames[2*i]
|
||||
|
|
|
|||
500
vendor/github.com/xo/terminfo/capvals.go
generated
vendored
500
vendor/github.com/xo/terminfo/capvals.go
generated
vendored
File diff suppressed because it is too large
Load diff
3
vendor/github.com/xo/terminfo/color.go
generated
vendored
3
vendor/github.com/xo/terminfo/color.go
generated
vendored
|
|
@ -70,14 +70,12 @@ func ColorLevelFromEnv() (ColorLevel, error) {
|
|||
}
|
||||
return ColorLevelHundreds, nil
|
||||
}
|
||||
|
||||
// otherwise determine from TERM's max_colors capability
|
||||
if term := os.Getenv("TERM"); term != "" {
|
||||
ti, err := Load(term)
|
||||
if err != nil {
|
||||
return ColorLevelNone, err
|
||||
}
|
||||
|
||||
v, ok := ti.Nums[MaxColors]
|
||||
switch {
|
||||
case !ok || v <= 16:
|
||||
|
|
@ -86,6 +84,5 @@ func ColorLevelFromEnv() (ColorLevel, error) {
|
|||
return ColorLevelHundreds, nil
|
||||
}
|
||||
}
|
||||
|
||||
return ColorLevelBasic, nil
|
||||
}
|
||||
|
|
|
|||
45
vendor/github.com/xo/terminfo/util.go → vendor/github.com/xo/terminfo/dec.go
generated
vendored
45
vendor/github.com/xo/terminfo/util.go → vendor/github.com/xo/terminfo/dec.go
generated
vendored
|
|
@ -7,12 +7,11 @@ import (
|
|||
const (
|
||||
// maxFileLength is the max file length.
|
||||
maxFileLength = 4096
|
||||
|
||||
// magic is the file magic for terminfo files.
|
||||
magic = 0432
|
||||
|
||||
// magicExtended is the file magic for terminfo files with the extended number format.
|
||||
magicExtended = 01036
|
||||
magic = 0o432
|
||||
// magicExtended is the file magic for terminfo files with the extended
|
||||
// number format.
|
||||
magicExtended = 0o1036
|
||||
)
|
||||
|
||||
// header fields.
|
||||
|
|
@ -99,12 +98,12 @@ func readStrings(idx []int, buf []byte, n int) (map[int][]byte, int, error) {
|
|||
type decoder struct {
|
||||
buf []byte
|
||||
pos int
|
||||
len int
|
||||
n int
|
||||
}
|
||||
|
||||
// readBytes reads the next n bytes of buf, incrementing pos by n.
|
||||
func (d *decoder) readBytes(n int) ([]byte, error) {
|
||||
if d.len < d.pos+n {
|
||||
if d.n < d.pos+n {
|
||||
return nil, ErrUnexpectedFileEnd
|
||||
}
|
||||
n, d.pos = d.pos, d.pos+n
|
||||
|
|
@ -115,15 +114,12 @@ func (d *decoder) readBytes(n int) ([]byte, error) {
|
|||
func (d *decoder) readInts(n, w int) ([]int, error) {
|
||||
w /= 8
|
||||
l := n * w
|
||||
|
||||
buf, err := d.readBytes(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// align
|
||||
d.pos += d.pos % 2
|
||||
|
||||
z := make([]int, n)
|
||||
for i, j := 0, 0; i < l; i, j = i+w, j+1 {
|
||||
switch w {
|
||||
|
|
@ -135,7 +131,6 @@ func (d *decoder) readInts(n, w int) ([]int, error) {
|
|||
z[j] = int(buf[i+3])<<24 | int(buf[i+2])<<16 | int(buf[i+1])<<8 | int(buf[i])
|
||||
}
|
||||
}
|
||||
|
||||
return z, nil
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +140,6 @@ func (d *decoder) readBools(n int) (map[int]bool, map[int]bool, error) {
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// process
|
||||
bools, boolsM := make(map[int]bool), make(map[int]bool)
|
||||
for i, b := range buf {
|
||||
|
|
@ -154,7 +148,6 @@ func (d *decoder) readBools(n int) (map[int]bool, map[int]bool, error) {
|
|||
boolsM[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
return bools, boolsM, nil
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +157,6 @@ func (d *decoder) readNums(n, w int) (map[int]int, map[int]bool, error) {
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// process
|
||||
nums, numsM := make(map[int]int), make(map[int]bool)
|
||||
for i := 0; i < n; i++ {
|
||||
|
|
@ -173,7 +165,6 @@ func (d *decoder) readNums(n, w int) (map[int]int, map[int]bool, error) {
|
|||
numsM[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
return nums, numsM, nil
|
||||
}
|
||||
|
||||
|
|
@ -184,16 +175,13 @@ func (d *decoder) readStringTable(n, sz int) ([][]byte, []int, error) {
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// read string data table
|
||||
data, err := d.readBytes(sz)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// align
|
||||
d.pos += d.pos % 2
|
||||
|
||||
// process
|
||||
s := make([][]byte, n)
|
||||
var m []int
|
||||
|
|
@ -209,7 +197,6 @@ func (d *decoder) readStringTable(n, sz int) ([][]byte, []int, error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s, m, nil
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +207,6 @@ func (d *decoder) readStrings(n, sz int) (map[int][]byte, map[int]bool, error) {
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
strs := make(map[int][]byte)
|
||||
for k, v := range s {
|
||||
if k == AcsChars {
|
||||
|
|
@ -228,39 +214,32 @@ func (d *decoder) readStrings(n, sz int) (map[int][]byte, map[int]bool, error) {
|
|||
}
|
||||
strs[k] = v
|
||||
}
|
||||
|
||||
strsM := make(map[int]bool, len(m))
|
||||
for _, k := range m {
|
||||
strsM[k] = true
|
||||
}
|
||||
|
||||
return strs, strsM, nil
|
||||
}
|
||||
|
||||
// canonicalizeAscChars reorders chars to be unique, in order.
|
||||
//
|
||||
// see repair_ascc in ncurses-6.0/progs/dump_entry.c
|
||||
// see repair_ascc in ncurses-6.3/progs/dump_entry.c
|
||||
func canonicalizeAscChars(z []byte) []byte {
|
||||
var c chars
|
||||
var c []byte
|
||||
enc := make(map[byte]byte, len(z)/2)
|
||||
for i := 0; i < len(z); i += 2 {
|
||||
if _, ok := enc[z[i]]; !ok {
|
||||
a, b := z[i], z[i+1]
|
||||
//log.Printf(">>> a: %d %c, b: %d %c", a, a, b, b)
|
||||
// log.Printf(">>> a: %d %c, b: %d %c", a, a, b, b)
|
||||
c, enc[a] = append(c, b), b
|
||||
}
|
||||
}
|
||||
sort.Sort(c)
|
||||
|
||||
sort.Slice(c, func(i, j int) bool {
|
||||
return c[i] < c[j]
|
||||
})
|
||||
r := make([]byte, 2*len(c))
|
||||
for i := 0; i < len(c); i++ {
|
||||
r[i*2], r[i*2+1] = c[i], enc[c[i]]
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type chars []byte
|
||||
|
||||
func (c chars) Len() int { return len(c) }
|
||||
func (c chars) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
||||
func (c chars) Less(i, j int) bool { return c[i] < c[j] }
|
||||
8
vendor/github.com/xo/terminfo/load.go
generated
vendored
8
vendor/github.com/xo/terminfo/load.go
generated
vendored
|
|
@ -23,34 +23,27 @@ func Load(name string) (*Terminfo, error) {
|
|||
if name == "" {
|
||||
return nil, ErrEmptyTermName
|
||||
}
|
||||
|
||||
termCache.RLock()
|
||||
ti, ok := termCache.db[name]
|
||||
termCache.RUnlock()
|
||||
|
||||
if ok {
|
||||
return ti, nil
|
||||
}
|
||||
|
||||
var checkDirs []string
|
||||
|
||||
// check $TERMINFO
|
||||
if dir := os.Getenv("TERMINFO"); dir != "" {
|
||||
checkDirs = append(checkDirs, dir)
|
||||
}
|
||||
|
||||
// check $HOME/.terminfo
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checkDirs = append(checkDirs, path.Join(u.HomeDir, ".terminfo"))
|
||||
|
||||
// check $TERMINFO_DIRS
|
||||
if dirs := os.Getenv("TERMINFO_DIRS"); dirs != "" {
|
||||
checkDirs = append(checkDirs, strings.Split(dirs, ":")...)
|
||||
}
|
||||
|
||||
// check fallback directories
|
||||
checkDirs = append(checkDirs, "/etc/terminfo", "/lib/terminfo", "/usr/share/terminfo")
|
||||
for _, dir := range checkDirs {
|
||||
|
|
@ -61,7 +54,6 @@ func Load(name string) (*Terminfo, error) {
|
|||
return ti, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrDatabaseDirectoryNotFound
|
||||
}
|
||||
|
||||
|
|
|
|||
85
vendor/github.com/xo/terminfo/param.go
generated
vendored
85
vendor/github.com/xo/terminfo/param.go
generated
vendored
|
|
@ -13,25 +13,18 @@ import (
|
|||
type parametizer struct {
|
||||
// z is the string to parameterize
|
||||
z []byte
|
||||
|
||||
// pos is the current position in s.
|
||||
pos int
|
||||
|
||||
// nest is the current nest level.
|
||||
nest int
|
||||
|
||||
// s is the variable stack.
|
||||
s stack
|
||||
|
||||
// skipElse keeps the state of skipping else.
|
||||
skipElse bool
|
||||
|
||||
// buf is the result buffer.
|
||||
buf *bytes.Buffer
|
||||
|
||||
// params are the parameters to interpolate.
|
||||
params [9]interface{}
|
||||
|
||||
// vars are dynamic variables.
|
||||
vars [26]interface{}
|
||||
}
|
||||
|
|
@ -54,19 +47,15 @@ var parametizerPool = sync.Pool{
|
|||
func newParametizer(z []byte) *parametizer {
|
||||
p := parametizerPool.Get().(*parametizer)
|
||||
p.z = z
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// reset resets the parametizer.
|
||||
func (p *parametizer) reset() {
|
||||
p.pos, p.nest = 0, 0
|
||||
|
||||
p.s.reset()
|
||||
p.buf.Reset()
|
||||
|
||||
p.params, p.vars = [9]interface{}{}, [26]interface{}{}
|
||||
|
||||
parametizerPool.Put(p)
|
||||
}
|
||||
|
||||
|
|
@ -106,13 +95,11 @@ func (p *parametizer) scanTextFn() stateFn {
|
|||
p.writeFrom(ppos)
|
||||
return nil
|
||||
}
|
||||
|
||||
if ch == '%' {
|
||||
p.writeFrom(ppos)
|
||||
p.pos++
|
||||
return p.scanCodeFn
|
||||
}
|
||||
|
||||
p.pos++
|
||||
}
|
||||
}
|
||||
|
|
@ -122,11 +109,9 @@ func (p *parametizer) scanCodeFn() stateFn {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case '%':
|
||||
p.buf.WriteByte('%')
|
||||
|
||||
case ':':
|
||||
// this character is used to avoid interpreting "%-" and "%+" as operators.
|
||||
// the next character is where the format really begins.
|
||||
|
|
@ -136,71 +121,52 @@ func (p *parametizer) scanCodeFn() stateFn {
|
|||
return nil
|
||||
}
|
||||
return p.scanFormatFn
|
||||
|
||||
case '#', ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.':
|
||||
return p.scanFormatFn
|
||||
|
||||
case 'o':
|
||||
p.buf.WriteString(strconv.FormatInt(int64(p.s.popInt()), 8))
|
||||
|
||||
case 'd':
|
||||
p.buf.WriteString(strconv.Itoa(p.s.popInt()))
|
||||
|
||||
case 'x':
|
||||
p.buf.WriteString(strconv.FormatInt(int64(p.s.popInt()), 16))
|
||||
|
||||
case 'X':
|
||||
p.buf.WriteString(strings.ToUpper(strconv.FormatInt(int64(p.s.popInt()), 16)))
|
||||
|
||||
case 's':
|
||||
p.buf.WriteString(p.s.popString())
|
||||
|
||||
case 'c':
|
||||
p.buf.WriteByte(p.s.popByte())
|
||||
|
||||
case 'p':
|
||||
p.pos++
|
||||
return p.pushParamFn
|
||||
|
||||
case 'P':
|
||||
p.pos++
|
||||
return p.setDsVarFn
|
||||
|
||||
case 'g':
|
||||
p.pos++
|
||||
return p.getDsVarFn
|
||||
|
||||
case '\'':
|
||||
p.pos++
|
||||
ch, err = p.peek()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.s.push(ch)
|
||||
|
||||
// skip the '\''
|
||||
p.pos++
|
||||
|
||||
case '{':
|
||||
p.pos++
|
||||
return p.pushIntfn
|
||||
|
||||
case 'l':
|
||||
p.s.push(len(p.s.popString()))
|
||||
|
||||
case '+':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai + bi)
|
||||
|
||||
case '-':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai - bi)
|
||||
|
||||
case '*':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai * bi)
|
||||
|
||||
case '/':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
if bi != 0 {
|
||||
|
|
@ -208,7 +174,6 @@ func (p *parametizer) scanCodeFn() stateFn {
|
|||
} else {
|
||||
p.s.push(0)
|
||||
}
|
||||
|
||||
case 'm':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
if bi != 0 {
|
||||
|
|
@ -216,101 +181,77 @@ func (p *parametizer) scanCodeFn() stateFn {
|
|||
} else {
|
||||
p.s.push(0)
|
||||
}
|
||||
|
||||
case '&':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai & bi)
|
||||
|
||||
case '|':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai | bi)
|
||||
|
||||
case '^':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai ^ bi)
|
||||
|
||||
case '=':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai == bi)
|
||||
|
||||
case '>':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai > bi)
|
||||
|
||||
case '<':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai < bi)
|
||||
|
||||
case 'A':
|
||||
bi, ai := p.s.popBool(), p.s.popBool()
|
||||
p.s.push(ai && bi)
|
||||
|
||||
case 'O':
|
||||
bi, ai := p.s.popBool(), p.s.popBool()
|
||||
p.s.push(ai || bi)
|
||||
|
||||
case '!':
|
||||
p.s.push(!p.s.popBool())
|
||||
|
||||
case '~':
|
||||
p.s.push(^p.s.popInt())
|
||||
|
||||
case 'i':
|
||||
for i := range p.params[:2] {
|
||||
if n, ok := p.params[i].(int); ok {
|
||||
p.params[i] = n + 1
|
||||
}
|
||||
}
|
||||
|
||||
case '?', ';':
|
||||
|
||||
case 't':
|
||||
return p.scanThenFn
|
||||
|
||||
case 'e':
|
||||
p.skipElse = true
|
||||
return p.skipTextFn
|
||||
}
|
||||
|
||||
p.pos++
|
||||
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
func (p *parametizer) scanFormatFn() stateFn {
|
||||
// the character was already read, so no need to check the error.
|
||||
ch, _ := p.peek()
|
||||
|
||||
// 6 should be the maximum length of a format string, for example "%:-9.9d".
|
||||
f := []byte{'%', ch, 0, 0, 0, 0}
|
||||
|
||||
var err error
|
||||
|
||||
for {
|
||||
p.pos++
|
||||
ch, err = p.peek()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
f = append(f, ch)
|
||||
switch ch {
|
||||
case 'o', 'd', 'x', 'X':
|
||||
fmt.Fprintf(p.buf, string(f), p.s.popInt())
|
||||
break
|
||||
|
||||
case 's':
|
||||
fmt.Fprintf(p.buf, string(f), p.s.popString())
|
||||
break
|
||||
|
||||
case 'c':
|
||||
fmt.Fprintf(p.buf, string(f), p.s.popByte())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
p.pos++
|
||||
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
|
|
@ -319,16 +260,13 @@ func (p *parametizer) pushParamFn() stateFn {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ai := int(ch - '1'); ai >= 0 && ai < len(p.params) {
|
||||
p.s.push(p.params[ai])
|
||||
} else {
|
||||
p.s.push(0)
|
||||
}
|
||||
|
||||
// skip the '}'
|
||||
p.pos++
|
||||
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
|
|
@ -337,7 +275,6 @@ func (p *parametizer) setDsVarFn() stateFn {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ch >= 'A' && ch <= 'Z' {
|
||||
staticVars.Lock()
|
||||
staticVars.vars[int(ch-'A')] = p.s.pop()
|
||||
|
|
@ -345,7 +282,6 @@ func (p *parametizer) setDsVarFn() stateFn {
|
|||
} else if ch >= 'a' && ch <= 'z' {
|
||||
p.vars[int(ch-'a')] = p.s.pop()
|
||||
}
|
||||
|
||||
p.pos++
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
|
@ -355,20 +291,16 @@ func (p *parametizer) getDsVarFn() stateFn {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var a byte
|
||||
if ch >= 'A' && ch <= 'Z' {
|
||||
a = 'A'
|
||||
} else if ch >= 'a' && ch <= 'z' {
|
||||
a = 'a'
|
||||
}
|
||||
|
||||
staticVars.Lock()
|
||||
p.s.push(staticVars.vars[int(ch-a)])
|
||||
staticVars.Unlock()
|
||||
|
||||
p.pos++
|
||||
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
|
|
@ -379,26 +311,21 @@ func (p *parametizer) pushIntfn() stateFn {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.pos++
|
||||
if ch < '0' || ch > '9' {
|
||||
p.s.push(ai)
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
ai = (ai * 10) + int(ch-'0')
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parametizer) scanThenFn() stateFn {
|
||||
p.pos++
|
||||
|
||||
if p.s.popBool() {
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
p.skipElse = false
|
||||
|
||||
return p.skipTextFn
|
||||
}
|
||||
|
||||
|
|
@ -408,17 +335,14 @@ func (p *parametizer) skipTextFn() stateFn {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.pos++
|
||||
if ch == '%' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if p.skipElse {
|
||||
return p.skipElseFn
|
||||
}
|
||||
|
||||
return p.skipThenFn
|
||||
}
|
||||
|
||||
|
|
@ -427,7 +351,6 @@ func (p *parametizer) skipThenFn() stateFn {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.pos++
|
||||
switch ch {
|
||||
case ';':
|
||||
|
|
@ -435,16 +358,13 @@ func (p *parametizer) skipThenFn() stateFn {
|
|||
return p.scanTextFn
|
||||
}
|
||||
p.nest--
|
||||
|
||||
case '?':
|
||||
p.nest++
|
||||
|
||||
case 'e':
|
||||
if p.nest == 0 {
|
||||
return p.scanTextFn
|
||||
}
|
||||
}
|
||||
|
||||
return p.skipTextFn
|
||||
}
|
||||
|
||||
|
|
@ -453,7 +373,6 @@ func (p *parametizer) skipElseFn() stateFn {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.pos++
|
||||
switch ch {
|
||||
case ';':
|
||||
|
|
@ -461,11 +380,9 @@ func (p *parametizer) skipElseFn() stateFn {
|
|||
return p.scanTextFn
|
||||
}
|
||||
p.nest--
|
||||
|
||||
case '?':
|
||||
p.nest++
|
||||
}
|
||||
|
||||
return p.skipTextFn
|
||||
}
|
||||
|
||||
|
|
@ -473,13 +390,11 @@ func (p *parametizer) skipElseFn() stateFn {
|
|||
func Printf(z []byte, params ...interface{}) string {
|
||||
p := newParametizer(z)
|
||||
defer p.reset()
|
||||
|
||||
// make sure we always have 9 parameters -- makes it easier
|
||||
// later to skip checks and its faster
|
||||
for i := 0; i < len(p.params) && i < len(params); i++ {
|
||||
p.params[i] = params[i]
|
||||
}
|
||||
|
||||
return p.exec()
|
||||
}
|
||||
|
||||
|
|
|
|||
83
vendor/github.com/xo/terminfo/terminfo.go
generated
vendored
83
vendor/github.com/xo/terminfo/terminfo.go
generated
vendored
|
|
@ -1,6 +1,8 @@
|
|||
// Package terminfo implements reading terminfo files in pure go.
|
||||
package terminfo
|
||||
|
||||
//go:generate go run gen.go
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
|
@ -20,34 +22,24 @@ func (err Error) Error() string {
|
|||
const (
|
||||
// ErrInvalidFileSize is the invalid file size error.
|
||||
ErrInvalidFileSize Error = "invalid file size"
|
||||
|
||||
// ErrUnexpectedFileEnd is the unexpected file end error.
|
||||
ErrUnexpectedFileEnd Error = "unexpected file end"
|
||||
|
||||
// ErrInvalidStringTable is the invalid string table error.
|
||||
ErrInvalidStringTable Error = "invalid string table"
|
||||
|
||||
// ErrInvalidMagic is the invalid magic error.
|
||||
ErrInvalidMagic Error = "invalid magic"
|
||||
|
||||
// ErrInvalidHeader is the invalid header error.
|
||||
ErrInvalidHeader Error = "invalid header"
|
||||
|
||||
// ErrInvalidNames is the invalid names error.
|
||||
ErrInvalidNames Error = "invalid names"
|
||||
|
||||
// ErrInvalidExtendedHeader is the invalid extended header error.
|
||||
ErrInvalidExtendedHeader Error = "invalid extended header"
|
||||
|
||||
// ErrEmptyTermName is the empty term name error.
|
||||
ErrEmptyTermName Error = "empty term name"
|
||||
|
||||
// ErrDatabaseDirectoryNotFound is the database directory not found error.
|
||||
ErrDatabaseDirectoryNotFound Error = "database directory not found"
|
||||
|
||||
// ErrFileNotFound is the file not found error.
|
||||
ErrFileNotFound Error = "file not found"
|
||||
|
||||
// ErrInvalidTermProgramVersion is the invalid TERM_PROGRAM_VERSION error.
|
||||
ErrInvalidTermProgramVersion Error = "invalid TERM_PROGRAM_VERSION"
|
||||
)
|
||||
|
|
@ -56,43 +48,30 @@ const (
|
|||
type Terminfo struct {
|
||||
// File is the original source file.
|
||||
File string
|
||||
|
||||
// Names are the provided cap names.
|
||||
Names []string
|
||||
|
||||
// Bools are the bool capabilities.
|
||||
Bools map[int]bool
|
||||
|
||||
// BoolsM are the missing bool capabilities.
|
||||
BoolsM map[int]bool
|
||||
|
||||
// Nums are the num capabilities.
|
||||
Nums map[int]int
|
||||
|
||||
// NumsM are the missing num capabilities.
|
||||
NumsM map[int]bool
|
||||
|
||||
// Strings are the string capabilities.
|
||||
Strings map[int][]byte
|
||||
|
||||
// StringsM are the missing string capabilities.
|
||||
StringsM map[int]bool
|
||||
|
||||
// ExtBools are the extended bool capabilities.
|
||||
ExtBools map[int]bool
|
||||
|
||||
// ExtBoolsNames is the map of extended bool capabilities to their index.
|
||||
ExtBoolNames map[int][]byte
|
||||
|
||||
// ExtNums are the extended num capabilities.
|
||||
ExtNums map[int]int
|
||||
|
||||
// ExtNumsNames is the map of extended num capabilities to their index.
|
||||
ExtNumNames map[int][]byte
|
||||
|
||||
// ExtStrings are the extended string capabilities.
|
||||
ExtStrings map[int][]byte
|
||||
|
||||
// ExtStringsNames is the map of extended string capabilities to their index.
|
||||
ExtStringNames map[int][]byte
|
||||
}
|
||||
|
|
@ -100,75 +79,63 @@ type Terminfo struct {
|
|||
// Decode decodes the terminfo data contained in buf.
|
||||
func Decode(buf []byte) (*Terminfo, error) {
|
||||
var err error
|
||||
|
||||
// check max file length
|
||||
if len(buf) >= maxFileLength {
|
||||
return nil, ErrInvalidFileSize
|
||||
}
|
||||
|
||||
d := &decoder{
|
||||
buf: buf,
|
||||
len: len(buf),
|
||||
n: len(buf),
|
||||
}
|
||||
|
||||
// read header
|
||||
h, err := d.readInts(6, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var numWidth int
|
||||
|
||||
// check magic
|
||||
if h[fieldMagic] == magic {
|
||||
switch {
|
||||
case h[fieldMagic] == magic:
|
||||
numWidth = 16
|
||||
} else if h[fieldMagic] == magicExtended {
|
||||
case h[fieldMagic] == magicExtended:
|
||||
numWidth = 32
|
||||
} else {
|
||||
default:
|
||||
return nil, ErrInvalidMagic
|
||||
}
|
||||
|
||||
// check header
|
||||
if hasInvalidCaps(h) {
|
||||
return nil, ErrInvalidHeader
|
||||
}
|
||||
|
||||
// check remaining length
|
||||
if d.len-d.pos < capLength(h) {
|
||||
if d.n-d.pos < capLength(h) {
|
||||
return nil, ErrUnexpectedFileEnd
|
||||
}
|
||||
|
||||
// read names
|
||||
names, err := d.readBytes(h[fieldNameSize])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check name is terminated properly
|
||||
i := findNull(names, 0)
|
||||
if i == -1 {
|
||||
return nil, ErrInvalidNames
|
||||
}
|
||||
names = names[:i]
|
||||
|
||||
// read bool caps
|
||||
bools, boolsM, err := d.readBools(h[fieldBoolCount])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read num caps
|
||||
nums, numsM, err := d.readNums(h[fieldNumCount], numWidth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read string caps
|
||||
strs, strsM, err := d.readStrings(h[fieldStringCount], h[fieldTableSize])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ti := &Terminfo{
|
||||
Names: strings.Split(string(names), "|"),
|
||||
Bools: bools,
|
||||
|
|
@ -178,57 +145,47 @@ func Decode(buf []byte) (*Terminfo, error) {
|
|||
Strings: strs,
|
||||
StringsM: strsM,
|
||||
}
|
||||
|
||||
// at the end of file, so no extended caps
|
||||
if d.pos >= d.len {
|
||||
if d.pos >= d.n {
|
||||
return ti, nil
|
||||
}
|
||||
|
||||
// decode extended header
|
||||
eh, err := d.readInts(5, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check extended offset field
|
||||
if hasInvalidExtOffset(eh) {
|
||||
return nil, ErrInvalidExtendedHeader
|
||||
}
|
||||
|
||||
// check extended cap lengths
|
||||
if d.len-d.pos != extCapLength(eh, numWidth) {
|
||||
if d.n-d.pos != extCapLength(eh, numWidth) {
|
||||
return nil, ErrInvalidExtendedHeader
|
||||
}
|
||||
|
||||
// read extended bool caps
|
||||
ti.ExtBools, _, err = d.readBools(eh[fieldExtBoolCount])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read extended num caps
|
||||
ti.ExtNums, _, err = d.readNums(eh[fieldExtNumCount], numWidth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read extended string data table indexes
|
||||
extIndexes, err := d.readInts(eh[fieldExtOffsetCount], 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read string data table
|
||||
extData, err := d.readBytes(eh[fieldExtTableSize])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// precautionary check that exactly at end of file
|
||||
if d.pos != d.len {
|
||||
if d.pos != d.n {
|
||||
return nil, ErrUnexpectedFileEnd
|
||||
}
|
||||
|
||||
var last int
|
||||
// read extended string caps
|
||||
ti.ExtStrings, last, err = readStrings(extIndexes, extData, eh[fieldExtStringCount])
|
||||
|
|
@ -236,28 +193,24 @@ func Decode(buf []byte) (*Terminfo, error) {
|
|||
return nil, err
|
||||
}
|
||||
extIndexes, extData = extIndexes[eh[fieldExtStringCount]:], extData[last:]
|
||||
|
||||
// read extended bool names
|
||||
ti.ExtBoolNames, _, err = readStrings(extIndexes, extData, eh[fieldExtBoolCount])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extIndexes = extIndexes[eh[fieldExtBoolCount]:]
|
||||
|
||||
// read extended num names
|
||||
ti.ExtNumNames, _, err = readStrings(extIndexes, extData, eh[fieldExtNumCount])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extIndexes = extIndexes[eh[fieldExtNumCount]:]
|
||||
|
||||
// read extended string names
|
||||
ti.ExtStringNames, _, err = readStrings(extIndexes, extData, eh[fieldExtStringCount])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//extIndexes = extIndexes[eh[fieldExtStringCount]:]
|
||||
|
||||
// extIndexes = extIndexes[eh[fieldExtStringCount]:]
|
||||
return ti, nil
|
||||
}
|
||||
|
||||
|
|
@ -279,23 +232,19 @@ func Open(dir, name string) (*Terminfo, error) {
|
|||
if buf == nil {
|
||||
return nil, ErrFileNotFound
|
||||
}
|
||||
|
||||
// decode
|
||||
ti, err := Decode(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// save original file name
|
||||
ti.File = filename
|
||||
|
||||
// add to cache
|
||||
termCache.Lock()
|
||||
for _, n := range ti.Names {
|
||||
termCache.db[n] = ti
|
||||
}
|
||||
termCache.Unlock()
|
||||
|
||||
return ti, nil
|
||||
}
|
||||
|
||||
|
|
@ -441,7 +390,6 @@ func (ti *Terminfo) Fprintf(w io.Writer, i int, v ...interface{}) {
|
|||
// them for this terminal.
|
||||
func (ti *Terminfo) Colorf(fg, bg int, str string) string {
|
||||
maxColors := int(ti.Nums[MaxColors])
|
||||
|
||||
// map bright colors to lower versions if the color table only holds 8.
|
||||
if maxColors == 8 {
|
||||
if fg > 7 && fg < 16 {
|
||||
|
|
@ -451,7 +399,6 @@ func (ti *Terminfo) Colorf(fg, bg int, str string) string {
|
|||
bg -= 8
|
||||
}
|
||||
}
|
||||
|
||||
var s string
|
||||
if maxColors > fg && fg >= 0 {
|
||||
s += ti.Printf(SetAForeground, fg)
|
||||
|
|
@ -480,20 +427,17 @@ func (ti *Terminfo) Goto(row, col int) string {
|
|||
// most strings don't need padding, which is good news!
|
||||
return io.WriteString(w, s)
|
||||
}
|
||||
|
||||
end := strings.Index(s, ">")
|
||||
if end == -1 {
|
||||
// unterminated... just emit bytes unadulterated.
|
||||
return io.WriteString(w, "$<"+s)
|
||||
}
|
||||
|
||||
var c int
|
||||
c, err = io.WriteString(w, s[:start])
|
||||
if err != nil {
|
||||
return n + c, err
|
||||
}
|
||||
n += c
|
||||
|
||||
s = s[start+2:]
|
||||
val := s[:end]
|
||||
s = s[end+1:]
|
||||
|
|
@ -518,13 +462,11 @@ func (ti *Terminfo) Goto(row, col int) string {
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
z, pad := ((baud/8)/unit)*ms, ti.Strings[PadChar]
|
||||
b := make([]byte, len(pad)*z)
|
||||
for bp := copy(b, pad); bp < len(b); bp *= 2 {
|
||||
copy(b[bp:], b[:bp])
|
||||
}
|
||||
|
||||
if (!ti.Bools[XonXoff] && baud > int(ti.Nums[PaddingBaudRate])) || mandatory {
|
||||
c, err = w.Write(b)
|
||||
if err != nil {
|
||||
|
|
@ -533,6 +475,5 @@ func (ti *Terminfo) Goto(row, col int) string {
|
|||
n += c
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}*/
|
||||
|
|
|
|||
21
vendor/modules.txt
vendored
21
vendor/modules.txt
vendored
|
|
@ -17,6 +17,15 @@ github.com/bahlo/generic-list-go
|
|||
# github.com/buger/jsonparser v1.1.2
|
||||
## explicit; go 1.13
|
||||
github.com/buger/jsonparser
|
||||
# github.com/cli/go-gh/v2 v2.13.0
|
||||
## explicit; go 1.25.0
|
||||
github.com/cli/go-gh/v2/internal/set
|
||||
github.com/cli/go-gh/v2/internal/yamlmap
|
||||
github.com/cli/go-gh/v2/pkg/auth
|
||||
github.com/cli/go-gh/v2/pkg/config
|
||||
# github.com/cli/safeexec v1.0.1
|
||||
## explicit; go 1.15
|
||||
github.com/cli/safeexec
|
||||
# github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
## explicit
|
||||
github.com/cloudfoundry/jibber_jabber
|
||||
|
|
@ -119,11 +128,11 @@ github.com/lucasb-eyer/go-colorful
|
|||
## explicit; go 1.12
|
||||
github.com/mailru/easyjson/buffer
|
||||
github.com/mailru/easyjson/jwriter
|
||||
# github.com/mattn/go-colorable v0.1.11
|
||||
## explicit; go 1.13
|
||||
# github.com/mattn/go-colorable v0.1.13
|
||||
## explicit; go 1.15
|
||||
github.com/mattn/go-colorable
|
||||
# github.com/mattn/go-isatty v0.0.14
|
||||
## explicit; go 1.12
|
||||
# github.com/mattn/go-isatty v0.0.20
|
||||
## explicit; go 1.15
|
||||
github.com/mattn/go-isatty
|
||||
# github.com/mgutz/str v1.2.0
|
||||
## explicit
|
||||
|
|
@ -179,8 +188,8 @@ github.com/stretchr/testify/assert/yaml
|
|||
# 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-20210125001918-ca9a967f8778
|
||||
## explicit; go 1.15
|
||||
# github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
|
||||
## explicit; go 1.19
|
||||
github.com/xo/terminfo
|
||||
# golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
## explicit; go 1.20
|
||||
|
|
|
|||
Loading…
Reference in a new issue