1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-03-19 21:28:28 +02:00

Merge branch 'refactor-better-encapsulation' into test-refactor

This commit is contained in:
Jesse Duffield 2023-04-30 14:00:37 +10:00
commit 8d68ab41b6
184 changed files with 6791 additions and 6159 deletions

View File

@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>ctrl+r</kbd>: switch to a recent repo <kbd>ctrl+r</kbd>: switch to a recent repo
<kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k) <kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k)
<kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j) <kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>R</kbd>: refresh
<kbd>?</kbd>: open menu
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu <kbd>@</kbd>: open command log menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view <kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view <kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: execute custom command <kbd>:</kbd>: execute custom command
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>m</kbd>: view merge/rebase options
<kbd>R</kbd>: refresh
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>?</kbd>: open menu
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>z</kbd>: undo (via reflog) (experimental) <kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental) <kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>P</kbd>: push <kbd>P</kbd>: push
@ -56,6 +56,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>`</kbd>: toggle file tree view <kbd>`</kbd>: toggle file tree view
</pre> </pre>
## Commit Message
<pre>
<kbd>enter</kbd>: confirm
<kbd>esc</kbd>: close
</pre>
## Commits ## Commits
<pre> <pre>
@ -89,6 +96,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>enter</kbd>: view selected item's files <kbd>enter</kbd>: view selected item's files
</pre> </pre>
## Confirmation Panel
<pre>
<kbd>enter</kbd>: confirm
<kbd>esc</kbd>: close/cancel
</pre>
## Files ## Files
<pre> <pre>
@ -197,6 +211,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>C</kbd>: commit changes using git editor <kbd>C</kbd>: commit changes using git editor
</pre> </pre>
## Menu
<pre>
<kbd>enter</kbd>: execute
<kbd>esc</kbd>: close
</pre>
## Reflog ## Reflog
<pre> <pre>
@ -250,8 +271,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Status ## Status
<pre> <pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file <kbd>o</kbd>: open config file
<kbd>e</kbd>: edit config file
<kbd>u</kbd>: check for update <kbd>u</kbd>: check for update
<kbd>enter</kbd>: switch to a recent repo <kbd>enter</kbd>: switch to a recent repo
<kbd>a</kbd>: show all branch logs <kbd>a</kbd>: show all branch logs

View File

@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>ctrl+r</kbd>: 最近使用したリポジトリに切り替え <kbd>ctrl+r</kbd>: 最近使用したリポジトリに切り替え
<kbd>pgup</kbd>: メインパネルを上にスクロール (fn+up/shift+k) <kbd>pgup</kbd>: メインパネルを上にスクロール (fn+up/shift+k)
<kbd>pgdown</kbd>: メインパネルを下にスクロール (fn+down/shift+j) <kbd>pgdown</kbd>: メインパネルを下にスクロール (fn+down/shift+j)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>R</kbd>: リフレッシュ
<kbd>?</kbd>: メニューを開く
<kbd>+</kbd>: 次のスクリーンモード (normal/half/fullscreen)
<kbd>_</kbd>: 前のスクリーンモード
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: 差分メニューを開く
<kbd>ctrl+e</kbd>: 差分メニューを開く
<kbd>@</kbd>: コマンドログメニューを開く <kbd>@</kbd>: コマンドログメニューを開く
<kbd>ctrl+w</kbd>: 空白文字の差分の表示有無を切り替え
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view <kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view <kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: カスタムコマンドを実行 <kbd>:</kbd>: カスタムコマンドを実行
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>m</kbd>: view merge/rebase options
<kbd>R</kbd>: リフレッシュ
<kbd>+</kbd>: 次のスクリーンモード (normal/half/fullscreen)
<kbd>_</kbd>: 前のスクリーンモード
<kbd>?</kbd>: メニューを開く
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: 差分メニューを開く
<kbd>ctrl+e</kbd>: 差分メニューを開く
<kbd>ctrl+w</kbd>: 空白文字の差分の表示有無を切り替え
<kbd>z</kbd>: アンドゥ (via reflog) (experimental) <kbd>z</kbd>: アンドゥ (via reflog) (experimental)
<kbd>ctrl+z</kbd>: リドゥ (via reflog) (experimental) <kbd>ctrl+z</kbd>: リドゥ (via reflog) (experimental)
<kbd>P</kbd>: push <kbd>P</kbd>: push
@ -115,6 +115,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>`</kbd>: ファイルツリーの表示を切り替え <kbd>`</kbd>: ファイルツリーの表示を切り替え
</pre> </pre>
## コミットメッセージ
<pre>
<kbd>enter</kbd>: 確認
<kbd>esc</kbd>: 閉じる
</pre>
## サブモジュール ## サブモジュール
<pre> <pre>
@ -131,8 +138,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## ステータス ## ステータス
<pre> <pre>
<kbd>e</kbd>: 設定ファイルを編集
<kbd>o</kbd>: 設定ファイルを開く <kbd>o</kbd>: 設定ファイルを開く
<kbd>e</kbd>: 設定ファイルを編集
<kbd>u</kbd>: 更新を確認 <kbd>u</kbd>: 更新を確認
<kbd>enter</kbd>: 最近使用したリポジトリに切り替え <kbd>enter</kbd>: 最近使用したリポジトリに切り替え
<kbd>a</kbd>: すべてのブランチログを表示 <kbd>a</kbd>: すべてのブランチログを表示
@ -257,6 +264,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>C</kbd>: gitエディタを使用して変更をコミット <kbd>C</kbd>: gitエディタを使用して変更をコミット
</pre> </pre>
## メニュー
<pre>
<kbd>enter</kbd>: 実行
<kbd>esc</kbd>: 閉じる
</pre>
## リモート ## リモート
<pre> <pre>
@ -295,3 +309,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection <kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>enter</kbd>: コミットを閲覧 <kbd>enter</kbd>: コミットを閲覧
</pre> </pre>
## 確認パネル
<pre>
<kbd>enter</kbd>: 確認
<kbd>esc</kbd>: 閉じる/キャンセル
</pre>

View File

@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>ctrl+r</kbd>: 최근에 사용한 저장소로 전환 <kbd>ctrl+r</kbd>: 최근에 사용한 저장소로 전환
<kbd>pgup</kbd>: 메인 패널을 위로 스크롤 (fn+up/shift+k) <kbd>pgup</kbd>: 메인 패널을 위로 스크롤 (fn+up/shift+k)
<kbd>pgdown</kbd>: 메인 패널을 아래로로 스크롤 (fn+down/shift+j) <kbd>pgdown</kbd>: 메인 패널을 아래로로 스크롤 (fn+down/shift+j)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: 커스텀 Patch 옵션 보기
<kbd>R</kbd>: 새로고침
<kbd>?</kbd>: 매뉴 열기
<kbd>+</kbd>: 다음 스크린 모드 (normal/half/fullscreen)
<kbd>_</kbd>: 이전 스크린 모드
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: Diff 메뉴 열기
<kbd>ctrl+e</kbd>: Diff 메뉴 열기
<kbd>@</kbd>: 명령어 로그 메뉴 열기 <kbd>@</kbd>: 명령어 로그 메뉴 열기
<kbd>ctrl+w</kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
<kbd>}</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기 <kbd>}</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기
<kbd>{</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트 크기 줄이기 <kbd>{</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트 크기 줄이기
<kbd>:</kbd>: execute custom command <kbd>:</kbd>: execute custom command
<kbd>ctrl+p</kbd>: 커스텀 Patch 옵션 보기
<kbd>m</kbd>: view merge/rebase options
<kbd>R</kbd>: 새로고침
<kbd>+</kbd>: 다음 스크린 모드 (normal/half/fullscreen)
<kbd>_</kbd>: 이전 스크린 모드
<kbd>?</kbd>: 매뉴 열기
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: Diff 메뉴 열기
<kbd>ctrl+e</kbd>: Diff 메뉴 열기
<kbd>ctrl+w</kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
<kbd>z</kbd>: 되돌리기 (reflog) (실험적) <kbd>z</kbd>: 되돌리기 (reflog) (실험적)
<kbd>ctrl+z</kbd>: 다시 실행 (reflog) (실험적) <kbd>ctrl+z</kbd>: 다시 실행 (reflog) (실험적)
<kbd>P</kbd>: 푸시 <kbd>P</kbd>: 푸시
@ -83,6 +83,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>enter</kbd>: view selected item's files <kbd>enter</kbd>: view selected item's files
</pre> </pre>
## 메뉴
<pre>
<kbd>enter</kbd>: 실행
<kbd>esc</kbd>: 닫기
</pre>
## 메인 패널 (Merging) ## 메인 패널 (Merging)
<pre> <pre>
@ -168,8 +175,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 상태 ## 상태
<pre> <pre>
<kbd>e</kbd>: 설정 파일 수정
<kbd>o</kbd>: 설정 파일 열기 <kbd>o</kbd>: 설정 파일 열기
<kbd>e</kbd>: 설정 파일 수정
<kbd>u</kbd>: 업데이트 확인 <kbd>u</kbd>: 업데이트 확인
<kbd>enter</kbd>: 최근에 사용한 저장소로 전환 <kbd>enter</kbd>: 최근에 사용한 저장소로 전환
<kbd>a</kbd>: 모든 브랜치 로그 표시 <kbd>a</kbd>: 모든 브랜치 로그 표시
@ -259,6 +266,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>`</kbd>: 파일 트리뷰로 전환 <kbd>`</kbd>: 파일 트리뷰로 전환
</pre> </pre>
## 커밋메시지
<pre>
<kbd>enter</kbd>: 확인
<kbd>esc</kbd>: 닫기
</pre>
## 태그 ## 태그
<pre> <pre>
@ -295,3 +309,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>M</kbd>: git mergetool를 열기 <kbd>M</kbd>: git mergetool를 열기
<kbd>f</kbd>: fetch <kbd>f</kbd>: fetch
</pre> </pre>
## 확인 패널
<pre>
<kbd>enter</kbd>: 확인
<kbd>esc</kbd>: 닫기/취소
</pre>

View File

@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>ctrl+r</kbd>: wissel naar een recente repo <kbd>ctrl+r</kbd>: wissel naar een recente repo
<kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up/shift+k) <kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up/shift+k)
<kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down/shift+j) <kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down/shift+j)
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
<kbd>R</kbd>: verversen
<kbd>?</kbd>: open menu
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
<kbd>_</kbd>: vorige scherm modus
<kbd>ctrl+s</kbd>: bekijk scoping opties
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu <kbd>@</kbd>: open command log menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view <kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view <kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: voer aangepaste commando uit <kbd>:</kbd>: voer aangepaste commando uit
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>R</kbd>: verversen
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
<kbd>_</kbd>: vorige scherm modus
<kbd>?</kbd>: open menu
<kbd>ctrl+s</kbd>: bekijk scoping opties
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel) <kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel) <kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
<kbd>P</kbd>: push <kbd>P</kbd>: push
@ -68,6 +68,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>f</kbd>: fetch <kbd>f</kbd>: fetch
</pre> </pre>
## Bevestigingspaneel
<pre>
<kbd>enter</kbd>: bevestig
<kbd>esc</kbd>: sluiten
</pre>
## Branches ## Branches
<pre> <pre>
@ -91,6 +98,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>enter</kbd>: bekijk commits <kbd>enter</kbd>: bekijk commits
</pre> </pre>
## Commit Bericht
<pre>
<kbd>enter</kbd>: bevestig
<kbd>esc</kbd>: sluiten
</pre>
## Commit bestanden ## Commit bestanden
<pre> <pre>
@ -138,6 +152,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>enter</kbd>: bekijk gecommite bestanden <kbd>enter</kbd>: bekijk gecommite bestanden
</pre> </pre>
## Menu
<pre>
<kbd>enter</kbd>: uitvoeren
<kbd>esc</kbd>: sluiten
</pre>
## Mergen ## Mergen
<pre> <pre>
@ -250,8 +271,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Status ## Status
<pre> <pre>
<kbd>e</kbd>: verander config bestand
<kbd>o</kbd>: open config bestand <kbd>o</kbd>: open config bestand
<kbd>e</kbd>: verander config bestand
<kbd>u</kbd>: check voor updates <kbd>u</kbd>: check voor updates
<kbd>enter</kbd>: wissel naar een recente repo <kbd>enter</kbd>: wissel naar een recente repo
<kbd>a</kbd>: alle logs van de branch laten zien <kbd>a</kbd>: alle logs van de branch laten zien

View File

@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>ctrl+r</kbd>: switch to a recent repo <kbd>ctrl+r</kbd>: switch to a recent repo
<kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k) <kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k)
<kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j) <kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j)
<kbd>m</kbd>: widok scalenia/opcje zmiany bazy
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>R</kbd>: odśwież
<kbd>?</kbd>: open menu
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu <kbd>@</kbd>: open command log menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view <kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view <kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: wykonaj własną komendę <kbd>:</kbd>: wykonaj własną komendę
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>m</kbd>: widok scalenia/opcje zmiany bazy
<kbd>R</kbd>: odśwież
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>?</kbd>: open menu
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>z</kbd>: undo (via reflog) (experimental) <kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental) <kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>P</kbd>: push <kbd>P</kbd>: push
@ -42,6 +42,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>[</kbd>: previous tab <kbd>[</kbd>: previous tab
</pre> </pre>
## Commit Message
<pre>
<kbd>enter</kbd>: potwierdź
<kbd>esc</kbd>: zamknij
</pre>
## Commity ## Commity
<pre> <pre>
@ -75,6 +82,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>enter</kbd>: przeglądaj pliki commita <kbd>enter</kbd>: przeglądaj pliki commita
</pre> </pre>
## Confirmation Panel
<pre>
<kbd>enter</kbd>: potwierdź
<kbd>esc</kbd>: zamknij
</pre>
## Local Branches ## Local Branches
<pre> <pre>
@ -113,6 +127,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>esc</kbd>: wyście z trybu "linia po linii" <kbd>esc</kbd>: wyście z trybu "linia po linii"
</pre> </pre>
## Menu
<pre>
<kbd>enter</kbd>: wykonaj
<kbd>esc</kbd>: zamknij
</pre>
## Pliki ## Pliki
<pre> <pre>
@ -243,8 +264,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Status ## Status
<pre> <pre>
<kbd>e</kbd>: edytuj konfigurację
<kbd>o</kbd>: otwórz konfigurację <kbd>o</kbd>: otwórz konfigurację
<kbd>e</kbd>: edytuj konfigurację
<kbd>u</kbd>: sprawdź aktualizacje <kbd>u</kbd>: sprawdź aktualizacje
<kbd>enter</kbd>: switch to a recent repo <kbd>enter</kbd>: switch to a recent repo
<kbd>a</kbd>: pokaż wszystkie logi gałęzi <kbd>a</kbd>: pokaż wszystkie logi gałęzi

View File

@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>ctrl+r</kbd>: 切换到最近的仓库 <kbd>ctrl+r</kbd>: 切换到最近的仓库
<kbd>pgup</kbd>: 向上滚动主面板 (fn+up/shift+k) <kbd>pgup</kbd>: 向上滚动主面板 (fn+up/shift+k)
<kbd>pgdown</kbd>: 向下滚动主面板 (fn+down/shift+j) <kbd>pgdown</kbd>: 向下滚动主面板 (fn+down/shift+j)
<kbd>m</kbd>: 查看 合并/变基 选项
<kbd>ctrl+p</kbd>: 查看自定义补丁选项
<kbd>R</kbd>: 刷新
<kbd>?</kbd>: 打开菜单
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
<kbd>_</kbd>: 上一屏模式
<kbd>ctrl+s</kbd>: 查看按路径过滤选项
<kbd>W</kbd>: 打开 diff 菜单
<kbd>ctrl+e</kbd>: 打开 diff 菜单
<kbd>@</kbd>: 打开命令日志菜单 <kbd>@</kbd>: 打开命令日志菜单
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白字符差异
<kbd>}</kbd>: 扩大差异视图中显示的上下文范围 <kbd>}</kbd>: 扩大差异视图中显示的上下文范围
<kbd>{</kbd>: 缩小差异视图中显示的上下文范围 <kbd>{</kbd>: 缩小差异视图中显示的上下文范围
<kbd>:</kbd>: 执行自定义命令 <kbd>:</kbd>: 执行自定义命令
<kbd>ctrl+p</kbd>: 查看自定义补丁选项
<kbd>m</kbd>: 查看 合并/变基 选项
<kbd>R</kbd>: 刷新
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
<kbd>_</kbd>: 上一屏模式
<kbd>?</kbd>: 打开菜单
<kbd>ctrl+s</kbd>: 查看按路径过滤选项
<kbd>W</kbd>: 打开 diff 菜单
<kbd>ctrl+e</kbd>: 打开 diff 菜单
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白字符差异
<kbd>z</kbd>: (通过 reflog)撤销「实验功能」 <kbd>z</kbd>: (通过 reflog)撤销「实验功能」
<kbd>ctrl+z</kbd>: (通过 reflog)重做「实验功能」 <kbd>ctrl+z</kbd>: (通过 reflog)重做「实验功能」
<kbd>P</kbd>: 推送 <kbd>P</kbd>: 推送
@ -155,6 +155,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>`</kbd>: 切换文件树视图 <kbd>`</kbd>: 切换文件树视图
</pre> </pre>
## 提交讯息
<pre>
<kbd>enter</kbd>: 确认
<kbd>esc</kbd>: 关闭
</pre>
## 文件 ## 文件
<pre> <pre>
@ -254,13 +261,27 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 状态 ## 状态
<pre> <pre>
<kbd>e</kbd>: 编辑配置文件
<kbd>o</kbd>: 打开配置文件 <kbd>o</kbd>: 打开配置文件
<kbd>e</kbd>: 编辑配置文件
<kbd>u</kbd>: 检查更新 <kbd>u</kbd>: 检查更新
<kbd>enter</kbd>: 切换到最近的仓库 <kbd>enter</kbd>: 切换到最近的仓库
<kbd>a</kbd>: 显示所有分支的日志 <kbd>a</kbd>: 显示所有分支的日志
</pre> </pre>
## 确认面板
<pre>
<kbd>enter</kbd>: 确认
<kbd>esc</kbd>: 关闭
</pre>
## 菜单
<pre>
<kbd>enter</kbd>: 执行
<kbd>esc</kbd>: 关闭
</pre>
## 贮藏 ## 贮藏
<pre> <pre>

View File

@ -33,7 +33,6 @@ type App struct {
Config config.AppConfigurer Config config.AppConfigurer
OSCommand *oscommands.OSCommand OSCommand *oscommands.OSCommand
Gui *gui.Gui Gui *gui.Gui
Updater *updates.Updater // may only need this on the Gui
} }
func Run( func Run(
@ -87,8 +86,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
app.OSCommand = oscommands.NewOSCommand(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log)) app.OSCommand = oscommands.NewOSCommand(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log))
var err error updater, err := updates.NewUpdater(common, config, app.OSCommand)
app.Updater, err = updates.NewUpdater(common, config, app.OSCommand)
if err != nil { if err != nil {
return app, err return app, err
} }
@ -108,7 +106,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
return app, err return app, err
} }
app.Gui, err = gui.NewGui(common, config, gitVersion, app.Updater, showRecentRepos, dirName) app.Gui, err = gui.NewGui(common, config, gitVersion, updater, showRecentRepos, dirName)
if err != nil { if err != nil {
return app, err return app, err
} }

View File

@ -1,131 +0,0 @@
package gui
import (
"time"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock"
)
// statusManager's job is to handle rendering of loading states and toast notifications
// that you see at the bottom left of the screen.
type statusManager struct {
statuses []appStatus
nextId int
mutex deadlock.Mutex
}
type appStatus struct {
message string
statusType string
id int
}
func (m *statusManager) removeStatus(id int) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.statuses = slices.Filter(m.statuses, func(status appStatus) bool {
return status.id != id
})
}
func (m *statusManager) addWaitingStatus(message string) int {
m.mutex.Lock()
defer m.mutex.Unlock()
m.nextId += 1
id := m.nextId
newStatus := appStatus{
message: message,
statusType: "waiting",
id: id,
}
m.statuses = append([]appStatus{newStatus}, m.statuses...)
return id
}
func (m *statusManager) addToastStatus(message string) int {
m.mutex.Lock()
defer m.mutex.Unlock()
m.nextId++
id := m.nextId
newStatus := appStatus{
message: message,
statusType: "toast",
id: id,
}
m.statuses = append([]appStatus{newStatus}, m.statuses...)
go func() {
time.Sleep(time.Second * 2)
m.removeStatus(id)
}()
return id
}
func (m *statusManager) getStatusString() string {
if len(m.statuses) == 0 {
return ""
}
topStatus := m.statuses[0]
if topStatus.statusType == "waiting" {
return topStatus.message + " " + utils.Loader()
}
return topStatus.message
}
func (m *statusManager) showStatus() bool {
return len(m.statuses) > 0
}
func (gui *Gui) toast(message string) {
gui.statusManager.addToastStatus(message)
gui.renderAppStatus()
}
func (gui *Gui) renderAppStatus() {
go utils.Safe(func() {
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for range ticker.C {
appStatus := gui.statusManager.getStatusString()
gui.c.OnUIThread(func() error {
return gui.renderString(gui.Views.AppStatus, appStatus)
})
if appStatus == "" {
return
}
}
})
}
// withWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (gui *Gui) withWaitingStatus(message string, f func() error) error {
go utils.Safe(func() {
id := gui.statusManager.addWaitingStatus(message)
defer func() {
gui.statusManager.removeStatus(id)
}()
gui.renderAppStatus()
if err := f(); err != nil {
gui.c.OnUIThread(func() error {
return gui.c.Error(err)
})
}
})
return nil
}

View File

@ -4,18 +4,33 @@ import (
"strings" "strings"
"time" "time"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
func (gui *Gui) startBackgroundRoutines() { type BackgroundRoutineMgr struct {
userConfig := gui.UserConfig gui *Gui
// if we've suspended the gui (e.g. because we've switched to a subprocess)
// we typically want to pause some things that are running like background
// file refreshes
pauseBackgroundThreads bool
}
func (self *BackgroundRoutineMgr) PauseBackgroundThreads(pause bool) {
self.pauseBackgroundThreads = pause
}
func (self *BackgroundRoutineMgr) startBackgroundRoutines() {
userConfig := self.gui.UserConfig
if userConfig.Git.AutoFetch { if userConfig.Git.AutoFetch {
fetchInterval := userConfig.Refresher.FetchInterval fetchInterval := userConfig.Refresher.FetchInterval
if fetchInterval > 0 { if fetchInterval > 0 {
go utils.Safe(gui.startBackgroundFetch) go utils.Safe(self.startBackgroundFetch)
} else { } else {
gui.c.Log.Errorf( self.gui.c.Log.Errorf(
"Value of config option 'refresher.fetchInterval' (%d) is invalid, disabling auto-fetch", "Value of config option 'refresher.fetchInterval' (%d) is invalid, disabling auto-fetch",
fetchInterval) fetchInterval)
} }
@ -24,42 +39,44 @@ func (gui *Gui) startBackgroundRoutines() {
if userConfig.Git.AutoRefresh { if userConfig.Git.AutoRefresh {
refreshInterval := userConfig.Refresher.RefreshInterval refreshInterval := userConfig.Refresher.RefreshInterval
if refreshInterval > 0 { if refreshInterval > 0 {
gui.goEvery(time.Second*time.Duration(refreshInterval), gui.stopChan, gui.refreshFilesAndSubmodules) self.goEvery(time.Second*time.Duration(refreshInterval), self.gui.stopChan, func() error {
return self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
})
} else { } else {
gui.c.Log.Errorf( self.gui.c.Log.Errorf(
"Value of config option 'refresher.refreshInterval' (%d) is invalid, disabling auto-refresh", "Value of config option 'refresher.refreshInterval' (%d) is invalid, disabling auto-refresh",
refreshInterval) refreshInterval)
} }
} }
} }
func (gui *Gui) startBackgroundFetch() { func (self *BackgroundRoutineMgr) startBackgroundFetch() {
gui.waitForIntro.Wait() self.gui.waitForIntro.Wait()
isNew := gui.IsNewRepo isNew := self.gui.IsNewRepo
userConfig := gui.UserConfig userConfig := self.gui.UserConfig
if !isNew { if !isNew {
time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second) time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second)
} }
err := gui.backgroundFetch() err := self.backgroundFetch()
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew { if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
_ = gui.c.Alert(gui.c.Tr.NoAutomaticGitFetchTitle, gui.c.Tr.NoAutomaticGitFetchBody) _ = self.gui.c.Alert(self.gui.c.Tr.NoAutomaticGitFetchTitle, self.gui.c.Tr.NoAutomaticGitFetchBody)
} else { } else {
gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error { self.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), self.gui.stopChan, func() error {
err := gui.backgroundFetch() err := self.backgroundFetch()
gui.render() self.gui.c.Render()
return err return err
}) })
} }
} }
func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function func() error) { func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan struct{}, function func() error) {
go utils.Safe(func() { go utils.Safe(func() {
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
if gui.PauseBackgroundThreads { if self.pauseBackgroundThreads {
continue continue
} }
_ = function() _ = function()
@ -69,3 +86,11 @@ func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function fun
} }
}) })
} }
func (self *BackgroundRoutineMgr) backgroundFetch() (err error) {
err = self.gui.git.Sync.Fetch(git_commands.FetchOptions{Background: true})
_ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
return err
}

View File

@ -1,23 +0,0 @@
package gui
import "github.com/jesseduffield/lazygit/pkg/gui/types"
func (gui *Gui) branchesRenderToMain() error {
var task types.UpdateTask
branch := gui.State.Contexts.Branches.GetSelected()
if branch == nil {
task = types.NewRenderStringTask(gui.c.Tr.NoBranchesThisRepo)
} else {
cmdObj := gui.git.Branch.GetGraphCmdObj(branch.FullRefName())
task = types.NewRunPtyTask(cmdObj.GetCmd())
}
return gui.c.RenderToMainViews(types.RefreshMainOpts{
Pair: gui.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: gui.c.Tr.LogTitle,
Task: task,
},
})
}

View File

@ -1,52 +0,0 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
func (gui *Gui) commitFilesRenderToMain() error {
node := gui.State.Contexts.CommitFiles.GetSelected()
if node == nil {
return nil
}
ref := gui.State.Contexts.CommitFiles.GetRef()
to := ref.RefName()
from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
cmdObj := gui.git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false,
gui.IgnoreWhitespaceInDiffView)
task := types.NewRunPtyTask(cmdObj.GetCmd())
pair := gui.c.MainViewPairs().Normal
if node.File != nil {
pair = gui.c.MainViewPairs().PatchBuilding
}
return gui.c.RenderToMainViews(types.RefreshMainOpts{
Pair: pair,
Main: &types.ViewUpdateOpts{
Title: gui.Tr.Patch,
Task: task,
},
Secondary: gui.secondaryPatchPanelUpdateOpts(),
})
}
func (gui *Gui) SwitchToCommitFilesContext(opts controllers.SwitchToCommitFilesContextOpts) error {
gui.State.Contexts.CommitFiles.SetSelectedLineIdx(0)
gui.State.Contexts.CommitFiles.SetRef(opts.Ref)
gui.State.Contexts.CommitFiles.SetTitleRef(opts.Ref.Description())
gui.State.Contexts.CommitFiles.SetCanRebase(opts.CanRebase)
gui.State.Contexts.CommitFiles.SetParentContext(opts.Context)
gui.State.Contexts.CommitFiles.SetWindowName(opts.Context.GetWindowName())
if err := gui.c.Refresh(types.RefreshOptions{
Scope: []types.RefreshableView{types.COMMIT_FILES},
}); err != nil {
return err
}
return gui.c.PushContext(gui.State.Contexts.CommitFiles)
}

View File

@ -1,36 +0,0 @@
package gui
import (
"strconv"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) handleCommitMessageFocused() error {
message := utils.ResolvePlaceholderString(
gui.c.Tr.CommitMessageConfirm,
map[string]string{
"keyBindClose": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Return),
"keyBindConfirm": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Confirm),
},
)
gui.RenderCommitLength()
return gui.renderString(gui.Views.Options, message)
}
func (gui *Gui) RenderCommitLength() {
if !gui.c.UserConfig.Gui.CommitLength.Show {
return
}
gui.Views.CommitMessage.Subtitle = getBufferLength(gui.Views.CommitMessage)
}
func getBufferLength(view *gocui.View) string {
return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " "
}

View File

@ -1,88 +0,0 @@
package gui
import (
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// after selecting the 200th commit, we'll load in all the rest
const COMMIT_THRESHOLD = 200
// list panel functions
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
return gui.State.Contexts.LocalCommits.GetSelected()
}
func (gui *Gui) onCommitFocus() error {
context := gui.State.Contexts.LocalCommits
if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() {
context.SetLimitCommits(false)
go utils.Safe(func() {
if err := gui.refreshCommitsWithLimit(); err != nil {
_ = gui.c.Error(err)
}
})
}
return nil
}
func (gui *Gui) branchCommitsRenderToMain() error {
var task types.UpdateTask
commit := gui.State.Contexts.LocalCommits.GetSelected()
if commit == nil {
task = types.NewRenderStringTask(gui.c.Tr.NoCommitsThisBranch)
} else if commit.Action == todo.UpdateRef {
task = types.NewRenderStringTask(
utils.ResolvePlaceholderString(
gui.c.Tr.UpdateRefHere,
map[string]string{
"ref": commit.Name,
}))
} else {
cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath(),
gui.IgnoreWhitespaceInDiffView)
task = types.NewRunPtyTask(cmdObj.GetCmd())
}
return gui.c.RenderToMainViews(types.RefreshMainOpts{
Pair: gui.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Patch",
Task: task,
},
Secondary: gui.secondaryPatchPanelUpdateOpts(),
})
}
func (gui *Gui) secondaryPatchPanelUpdateOpts() *types.ViewUpdateOpts {
if gui.git.Patch.PatchBuilder.Active() {
patch := gui.git.Patch.PatchBuilder.RenderAggregatedPatch(false)
return &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(patch),
Title: gui.Tr.CustomPatch,
}
}
return nil
}
func (gui *Gui) refForLog() string {
bisectInfo := gui.git.Bisect.GetInfo()
gui.State.Model.BisectInfo = bisectInfo
if !bisectInfo.Started() {
return "HEAD"
}
// need to see if our bisect's current commit is reachable from our 'new' ref.
if bisectInfo.Bisecting() && !gui.git.Bisect.ReachableFromStart(bisectInfo) {
return bisectInfo.GetNewSha()
}
return bisectInfo.GetStartSha()
}

View File

@ -1,311 +0,0 @@
package gui
import (
"context"
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mattn/go-runewidth"
)
// This file is for the rendering of confirmation panels along with setting and handling associated
// keybindings.
func (gui *Gui) wrappedConfirmationFunction(cancel context.CancelFunc, function func() error) func() error {
return func() error {
cancel()
if err := gui.c.PopContext(); err != nil {
return err
}
if function != nil {
if err := function(); err != nil {
return gui.c.Error(err)
}
}
return nil
}
}
func (gui *Gui) wrappedPromptConfirmationFunction(cancel context.CancelFunc, function func(string) error, getResponse func() string) func() error {
return func() error {
cancel()
if err := gui.c.PopContext(); err != nil {
return err
}
if function != nil {
if err := function(getResponse()); err != nil {
return gui.c.Error(err)
}
}
return nil
}
}
func (gui *Gui) deactivateConfirmationPrompt() {
gui.Mutexes.PopupMutex.Lock()
gui.State.CurrentPopupOpts = nil
gui.Mutexes.PopupMutex.Unlock()
gui.Views.Confirmation.Visible = false
gui.Views.Suggestions.Visible = false
gui.clearConfirmationViewKeyBindings()
}
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
// if we need to wrap, calculate height to fit content within view's width
if wrap {
for _, line := range lines {
lineCount += runewidth.StringWidth(line)/width + 1
}
} else {
lineCount = len(lines)
}
return lineCount
}
func (gui *Gui) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
return gui.getPopupPanelDimensionsAux(panelWidth, contentHeight)
}
func (gui *Gui) getPopupPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) {
width, height := gui.g.Size()
if panelHeight > height*3/4 {
panelHeight = height * 3 / 4
}
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
func (gui *Gui) getConfirmationPanelWidth() int {
width, _ := gui.g.Size()
// we want a minimum width up to a point, then we do it based on ratio.
panelWidth := 4 * width / 7
minWidth := 80
if panelWidth < minWidth {
if width-2 < minWidth {
panelWidth = width - 2
} else {
panelWidth = minWidth
}
}
return panelWidth
}
func (gui *Gui) prepareConfirmationPanel(
ctx context.Context,
opts types.ConfirmOpts,
) error {
gui.Views.Confirmation.HasLoader = opts.HasLoader
if opts.HasLoader {
gui.g.StartTicking(ctx)
}
gui.Views.Confirmation.Title = opts.Title
// for now we do not support wrapping in our editor
gui.Views.Confirmation.Wrap = !opts.Editable
gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor
gui.Views.Confirmation.Mask = runeForMask(opts.Mask)
_ = gui.Views.Confirmation.SetOrigin(0, 0)
gui.findSuggestions = opts.FindSuggestionsFunc
if opts.FindSuggestionsFunc != nil {
suggestionsView := gui.Views.Suggestions
suggestionsView.Wrap = false
suggestionsView.FgColor = theme.GocuiDefaultTextColor
gui.setSuggestions(opts.FindSuggestionsFunc(""))
suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(gui.c.Tr.SuggestionsTitle, gui.c.UserConfig.Keybinding.Universal.TogglePanel)
}
gui.resizeConfirmationPanel()
return nil
}
func runeForMask(mask bool) rune {
if mask {
return '*'
}
return 0
}
func (gui *Gui) createPopupPanel(ctx context.Context, opts types.CreatePopupPanelOpts) error {
gui.Mutexes.PopupMutex.Lock()
defer gui.Mutexes.PopupMutex.Unlock()
ctx, cancel := context.WithCancel(ctx)
// we don't allow interruptions of non-loader popups in case we get stuck somehow
// e.g. a credentials popup never gets its required user input so a process hangs
// forever.
// The proper solution is to have a queue of popup options
if gui.State.CurrentPopupOpts != nil && !gui.State.CurrentPopupOpts.HasLoader {
gui.Log.Error("ignoring create popup panel because a popup panel is already open")
cancel()
return nil
}
// remove any previous keybindings
gui.clearConfirmationViewKeyBindings()
err := gui.prepareConfirmationPanel(
ctx,
types.ConfirmOpts{
Title: opts.Title,
Prompt: opts.Prompt,
HasLoader: opts.HasLoader,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
Editable: opts.Editable,
Mask: opts.Mask,
})
if err != nil {
cancel()
return err
}
confirmationView := gui.Views.Confirmation
confirmationView.Editable = opts.Editable
confirmationView.Editor = gocui.EditorFunc(gui.promptEditor)
if opts.Editable {
textArea := confirmationView.TextArea
textArea.Clear()
textArea.TypeString(opts.Prompt)
gui.resizeConfirmationPanel()
confirmationView.RenderTextArea()
} else {
if err := gui.renderString(confirmationView, style.AttrBold.Sprint(opts.Prompt)); err != nil {
cancel()
return err
}
}
if err := gui.setKeyBindings(cancel, opts); err != nil {
cancel()
return err
}
gui.State.CurrentPopupOpts = &opts
return gui.c.PushContext(gui.State.Contexts.Confirmation)
}
func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopupPanelOpts) error {
actions := utils.ResolvePlaceholderString(
gui.c.Tr.CloseConfirm,
map[string]string{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
},
)
_ = gui.renderString(gui.Views.Options, actions)
var onConfirm func() error
if opts.HandleConfirmPrompt != nil {
onConfirm = gui.wrappedPromptConfirmationFunction(cancel, opts.HandleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() })
} else {
onConfirm = gui.wrappedConfirmationFunction(cancel, opts.HandleConfirm)
}
keybindingConfig := gui.c.UserConfig.Keybinding
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
cancel,
opts.HandleConfirmPrompt,
gui.getSelectedSuggestionValue,
)
bindings := []*types.Binding{
{
ViewName: "confirmation",
Key: keybindings.GetKey(keybindingConfig.Universal.Confirm),
Handler: onConfirm,
},
{
ViewName: "confirmation",
Key: keybindings.GetKey(keybindingConfig.Universal.Return),
Handler: gui.wrappedConfirmationFunction(cancel, opts.HandleClose),
},
{
ViewName: "confirmation",
Key: keybindings.GetKey(keybindingConfig.Universal.TogglePanel),
Handler: func() error {
if len(gui.State.Suggestions) > 0 {
return gui.replaceContext(gui.State.Contexts.Suggestions)
}
return nil
},
},
{
ViewName: "suggestions",
Key: keybindings.GetKey(keybindingConfig.Universal.Confirm),
Handler: onSuggestionConfirm,
},
{
ViewName: "suggestions",
Key: keybindings.GetKey(keybindingConfig.Universal.Return),
Handler: gui.wrappedConfirmationFunction(cancel, opts.HandleClose),
},
{
ViewName: "suggestions",
Key: keybindings.GetKey(keybindingConfig.Universal.TogglePanel),
Handler: func() error { return gui.replaceContext(gui.State.Contexts.Confirmation) },
},
}
for _, binding := range bindings {
if err := gui.SetKeybinding(binding); err != nil {
return err
}
}
return nil
}
func (gui *Gui) clearConfirmationViewKeyBindings() {
keybindingConfig := gui.c.UserConfig.Keybinding
_ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone)
}
func (gui *Gui) refreshSuggestions() {
gui.suggestionsAsyncHandler.Do(func() func() {
findSuggestionsFn := gui.findSuggestions
if findSuggestionsFn != nil {
suggestions := gui.findSuggestions(gui.c.GetPromptInput())
return func() { gui.setSuggestions(suggestions) }
} else {
return func() {}
}
})
}
func (gui *Gui) handleAskFocused() error {
keybindingConfig := gui.c.UserConfig.Keybinding
message := utils.ResolvePlaceholderString(
gui.c.Tr.CloseConfirm,
map[string]string{
"keyBindClose": keybindings.Label(keybindingConfig.Universal.Return),
"keyBindConfirm": keybindings.Label(keybindingConfig.Universal.Confirm),
},
)
return gui.renderString(gui.Views.Options, message)
}

View File

@ -0,0 +1,3 @@
package constants
const SEARCH_PREFIX = "search: "

View File

@ -1,12 +1,10 @@
package gui package gui
import ( import (
"sort" "errors"
"strings" "sync"
"github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo" "github.com/samber/lo"
@ -17,46 +15,66 @@ import (
// you in the menu context. When contexts are activated/deactivated certain things need // you in the menu context. When contexts are activated/deactivated certain things need
// to happen like showing/hiding views and rendering content. // to happen like showing/hiding views and rendering content.
func (gui *Gui) popupViewNames() []string { type ContextMgr struct {
popups := slices.Filter(gui.State.Contexts.Flatten(), func(c types.Context) bool { ContextStack []types.Context
return c.GetKind() == types.PERSISTENT_POPUP || c.GetKind() == types.TEMPORARY_POPUP sync.RWMutex
}) gui *Gui
return slices.Map(popups, func(c types.Context) string { allContexts *context.ContextTree
return c.GetViewName()
})
} }
// use replaceContext when you don't want to return to the original context upon func NewContextMgr(
gui *Gui,
allContexts *context.ContextTree,
) *ContextMgr {
return &ContextMgr{
ContextStack: []types.Context{},
RWMutex: sync.RWMutex{},
gui: gui,
allContexts: allContexts,
}
}
// use when you don't want to return to the original context upon
// hitting escape: you want to go that context's parent instead. // hitting escape: you want to go that context's parent instead.
func (gui *Gui) replaceContext(c types.Context) error { func (self *ContextMgr) Replace(c types.Context) error {
if !c.IsFocusable() { if !c.IsFocusable() {
return nil return nil
} }
gui.State.ContextManager.Lock() self.Lock()
if len(gui.State.ContextManager.ContextStack) == 0 { if len(self.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = []types.Context{c} self.ContextStack = []types.Context{c}
} else { } else {
// replace the last item with the given item // replace the last item with the given item
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack[0:len(gui.State.ContextManager.ContextStack)-1], c) self.ContextStack = append(self.ContextStack[0:len(self.ContextStack)-1], c)
} }
defer gui.State.ContextManager.Unlock() defer self.Unlock()
return gui.activateContext(c, types.OnFocusOpts{}) return self.activateContext(c, types.OnFocusOpts{})
} }
func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error { func (self *ContextMgr) Push(c types.Context, opts ...types.OnFocusOpts) error {
if len(opts) > 1 {
return errors.New("cannot pass multiple opts to Push")
}
singleOpts := types.OnFocusOpts{}
if len(opts) > 0 {
// using triple dot but you should only ever pass one of these opt structs
singleOpts = opts[0]
}
if !c.IsFocusable() { if !c.IsFocusable() {
return nil return nil
} }
contextsToDeactivate, contextToActivate := gui.pushToContextStack(c) contextsToDeactivate, contextToActivate := self.pushToContextStack(c)
for _, contextToDeactivate := range contextsToDeactivate { for _, contextToDeactivate := range contextsToDeactivate {
if err := gui.deactivateContext(contextToDeactivate, types.OnFocusLostOpts{NewContextKey: c.GetKey()}); err != nil { if err := self.deactivateContext(contextToDeactivate, types.OnFocusLostOpts{NewContextKey: c.GetKey()}); err != nil {
return err return err
} }
} }
@ -65,43 +83,43 @@ func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error {
return nil return nil
} }
return gui.activateContext(contextToActivate, opts) return self.activateContext(contextToActivate, singleOpts)
} }
// Adjusts the context stack based on the context that's being pushed and // Adjusts the context stack based on the context that's being pushed and
// returns (contexts to deactivate, context to activate) // returns (contexts to deactivate, context to activate)
func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Context) { func (self *ContextMgr) pushToContextStack(c types.Context) ([]types.Context, types.Context) {
contextsToDeactivate := []types.Context{} contextsToDeactivate := []types.Context{}
gui.State.ContextManager.Lock() self.Lock()
defer gui.State.ContextManager.Unlock() defer self.Unlock()
if len(gui.State.ContextManager.ContextStack) > 0 && if len(self.ContextStack) > 0 &&
c == gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] { c == self.ContextStack[len(self.ContextStack)-1] {
// Context being pushed is already on top of the stack: nothing to // Context being pushed is already on top of the stack: nothing to
// deactivate or activate // deactivate or activate
return contextsToDeactivate, nil return contextsToDeactivate, nil
} }
if len(gui.State.ContextManager.ContextStack) == 0 { if len(self.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c) self.ContextStack = append(self.ContextStack, c)
} else if c.GetKind() == types.SIDE_CONTEXT { } else if c.GetKind() == types.SIDE_CONTEXT {
// if we are switching to a side context, remove all other contexts in the stack // if we are switching to a side context, remove all other contexts in the stack
contextsToDeactivate = gui.State.ContextManager.ContextStack contextsToDeactivate = self.ContextStack
gui.State.ContextManager.ContextStack = []types.Context{c} self.ContextStack = []types.Context{c}
} else if c.GetKind() == types.MAIN_CONTEXT { } else if c.GetKind() == types.MAIN_CONTEXT {
// if we're switching to a main context, remove all other main contexts in the stack // if we're switching to a main context, remove all other main contexts in the stack
contextsToKeep := []types.Context{} contextsToKeep := []types.Context{}
for _, stackContext := range gui.State.ContextManager.ContextStack { for _, stackContext := range self.ContextStack {
if stackContext.GetKind() == types.MAIN_CONTEXT { if stackContext.GetKind() == types.MAIN_CONTEXT {
contextsToDeactivate = append(contextsToDeactivate, stackContext) contextsToDeactivate = append(contextsToDeactivate, stackContext)
} else { } else {
contextsToKeep = append(contextsToKeep, stackContext) contextsToKeep = append(contextsToKeep, stackContext)
} }
} }
gui.State.ContextManager.ContextStack = append(contextsToKeep, c) self.ContextStack = append(contextsToKeep, c)
} else { } else {
topContext := gui.currentContextWithoutLock() topContext := self.currentContextWithoutLock()
// if we're pushing the same context on, we do nothing. // if we're pushing the same context on, we do nothing.
if topContext.GetKey() != c.GetKey() { if topContext.GetKey() != c.GetKey() {
@ -114,48 +132,48 @@ func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Cont
(topContext.GetKind() == types.MAIN_CONTEXT && c.GetKind() == types.MAIN_CONTEXT) { (topContext.GetKind() == types.MAIN_CONTEXT && c.GetKind() == types.MAIN_CONTEXT) {
contextsToDeactivate = append(contextsToDeactivate, topContext) contextsToDeactivate = append(contextsToDeactivate, topContext)
_, gui.State.ContextManager.ContextStack = slices.Pop(gui.State.ContextManager.ContextStack) _, self.ContextStack = slices.Pop(self.ContextStack)
} }
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c) self.ContextStack = append(self.ContextStack, c)
} }
} }
return contextsToDeactivate, c return contextsToDeactivate, c
} }
func (gui *Gui) popContext() error { func (self *ContextMgr) Pop() error {
gui.State.ContextManager.Lock() self.Lock()
if len(gui.State.ContextManager.ContextStack) == 1 { if len(self.ContextStack) == 1 {
// cannot escape from bottommost context // cannot escape from bottommost context
gui.State.ContextManager.Unlock() self.Unlock()
return nil return nil
} }
var currentContext types.Context var currentContext types.Context
currentContext, gui.State.ContextManager.ContextStack = slices.Pop(gui.State.ContextManager.ContextStack) currentContext, self.ContextStack = slices.Pop(self.ContextStack)
newContext := gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] newContext := self.ContextStack[len(self.ContextStack)-1]
gui.State.ContextManager.Unlock() self.Unlock()
if err := gui.deactivateContext(currentContext, types.OnFocusLostOpts{NewContextKey: newContext.GetKey()}); err != nil { if err := self.deactivateContext(currentContext, types.OnFocusLostOpts{NewContextKey: newContext.GetKey()}); err != nil {
return err return err
} }
return gui.activateContext(newContext, types.OnFocusOpts{}) return self.activateContext(newContext, types.OnFocusOpts{})
} }
func (gui *Gui) removeContexts(contextsToRemove []types.Context) error { func (self *ContextMgr) RemoveContexts(contextsToRemove []types.Context) error {
gui.State.ContextManager.Lock() self.Lock()
if len(gui.State.ContextManager.ContextStack) == 1 { if len(self.ContextStack) == 1 {
gui.State.ContextManager.Unlock() self.Unlock()
return nil return nil
} }
rest := lo.Filter(gui.State.ContextManager.ContextStack, func(context types.Context, _ int) bool { rest := lo.Filter(self.ContextStack, func(context types.Context, _ int) bool {
for _, contextToRemove := range contextsToRemove { for _, contextToRemove := range contextsToRemove {
if context.GetKey() == contextToRemove.GetKey() { if context.GetKey() == contextToRemove.GetKey() {
return false return false
@ -163,25 +181,25 @@ func (gui *Gui) removeContexts(contextsToRemove []types.Context) error {
} }
return true return true
}) })
gui.State.ContextManager.ContextStack = rest self.ContextStack = rest
contextToActivate := rest[len(rest)-1] contextToActivate := rest[len(rest)-1]
gui.State.ContextManager.Unlock() self.Unlock()
for _, context := range contextsToRemove { for _, context := range contextsToRemove {
if err := gui.deactivateContext(context, types.OnFocusLostOpts{NewContextKey: contextToActivate.GetKey()}); err != nil { if err := self.deactivateContext(context, types.OnFocusLostOpts{NewContextKey: contextToActivate.GetKey()}); err != nil {
return err return err
} }
} }
// activate the item at the top of the stack // activate the item at the top of the stack
return gui.activateContext(contextToActivate, types.OnFocusOpts{}) return self.activateContext(contextToActivate, types.OnFocusOpts{})
} }
func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error { func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
view, _ := gui.g.View(c.GetViewName()) view, _ := self.gui.c.GocuiGui().View(c.GetViewName())
if view != nil && view.IsSearching() { if view != nil && view.IsSearching() {
if err := gui.onSearchEscape(); err != nil { if err := self.gui.onSearchEscape(); err != nil {
return err return err
} }
} }
@ -200,34 +218,17 @@ func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) e
return nil return nil
} }
// postRefreshUpdate is to be called on a context after the state that it depends on has been refreshed func (self *ContextMgr) activateContext(c types.Context, opts types.OnFocusOpts) error {
// if the context's view is set to another context we do nothing.
// if the context's view is the current view we trigger a focus; re-selecting the current item.
func (gui *Gui) postRefreshUpdate(c types.Context) error {
if err := c.HandleRender(); err != nil {
return err
}
if gui.currentViewName() == c.GetViewName() {
if err := c.HandleFocus(types.OnFocusOpts{}); err != nil {
return err
}
}
return nil
}
func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error {
viewName := c.GetViewName() viewName := c.GetViewName()
v, err := gui.g.View(viewName) v, err := self.gui.c.GocuiGui().View(viewName)
if err != nil { if err != nil {
return err return err
} }
gui.setWindowContext(c) self.gui.helpers.Window.SetWindowContext(c)
gui.moveToTopOfWindow(c) self.gui.helpers.Window.MoveToTopOfWindow(c)
if _, err := gui.g.SetCurrentView(viewName); err != nil { if _, err := self.gui.c.GocuiGui().SetCurrentView(viewName); err != nil {
return err return err
} }
@ -238,14 +239,9 @@ func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error {
v.Visible = true v.Visible = true
gui.g.Cursor = v.Editable self.gui.c.GocuiGui().Cursor = v.Editable
// render the options available for the current context at the bottom of the screen self.gui.renderContextOptionsMap(c)
optionsMap := c.GetOptionsMap()
if optionsMap == nil {
optionsMap = gui.globalOptionsMap()
}
gui.renderOptionsMap(optionsMap)
if err := c.HandleFocus(opts); err != nil { if err := c.HandleFocus(opts); err != nil {
return err return err
@ -254,63 +250,27 @@ func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error {
return nil return nil
} }
func (gui *Gui) optionsMapToString(optionsMap map[string]string) string { func (self *ContextMgr) Current() types.Context {
options := maps.MapToSlice(optionsMap, func(key string, description string) string { self.RLock()
return key + ": " + description defer self.RUnlock()
})
sort.Strings(options) return self.currentContextWithoutLock()
return strings.Join(options, ", ")
} }
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) { func (self *ContextMgr) currentContextWithoutLock() types.Context {
_ = gui.renderString(gui.Views.Options, gui.optionsMapToString(optionsMap)) if len(self.ContextStack) == 0 {
} return self.gui.defaultSideContext()
// // currently unused
// func (gui *Gui) renderContextStack() string {
// result := ""
// for _, context := range gui.State.ContextManager.ContextStack {
// result += string(context.GetKey()) + "\n"
// }
// return result
// }
func (gui *Gui) currentContext() types.Context {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
return gui.currentContextWithoutLock()
}
func (gui *Gui) currentContextWithoutLock() types.Context {
if len(gui.State.ContextManager.ContextStack) == 0 {
return gui.defaultSideContext()
} }
return gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] return self.ContextStack[len(self.ContextStack)-1]
} }
// the status panel is not yet a list context (and may never be), so this method is not // Note that this could return the 'status' context which is not itself a list context.
// quite the same as currentSideContext() func (self *ContextMgr) CurrentSide() types.Context {
func (gui *Gui) currentSideListContext() types.IListContext { self.RLock()
context := gui.currentSideContext() defer self.RUnlock()
listContext, ok := context.(types.IListContext)
if !ok {
return nil
}
return listContext
}
func (gui *Gui) currentSideContext() types.Context { stack := self.ContextStack
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
stack := gui.State.ContextManager.ContextStack
// on startup the stack can be empty so we'll return an empty string in that case
if len(stack) == 0 {
return gui.defaultSideContext()
}
// find the first context in the stack with the type of types.SIDE_CONTEXT // find the first context in the stack with the type of types.SIDE_CONTEXT
for i := range stack { for i := range stack {
@ -321,22 +281,22 @@ func (gui *Gui) currentSideContext() types.Context {
} }
} }
return gui.defaultSideContext() return self.gui.defaultSideContext()
} }
// static as opposed to popup // static as opposed to popup
func (gui *Gui) currentStaticContext() types.Context { func (self *ContextMgr) CurrentStatic() types.Context {
gui.State.ContextManager.RLock() self.RLock()
defer gui.State.ContextManager.RUnlock() defer self.RUnlock()
return gui.currentStaticContextWithoutLock() return self.currentStaticContextWithoutLock()
} }
func (gui *Gui) currentStaticContextWithoutLock() types.Context { func (self *ContextMgr) currentStaticContextWithoutLock() types.Context {
stack := gui.State.ContextManager.ContextStack stack := self.ContextStack
if len(stack) == 0 { if len(stack) == 0 {
return gui.defaultSideContext() return self.gui.defaultSideContext()
} }
// find the first context in the stack without a popup type // find the first context in the stack without a popup type
@ -348,88 +308,43 @@ func (gui *Gui) currentStaticContextWithoutLock() types.Context {
} }
} }
return gui.defaultSideContext() return self.gui.defaultSideContext()
} }
func (gui *Gui) defaultSideContext() types.Context { func (self *ContextMgr) ForEach(f func(types.Context)) {
if gui.State.Modes.Filtering.Active() { self.RLock()
return gui.State.Contexts.LocalCommits defer self.RUnlock()
} else {
return gui.State.Contexts.Files for _, context := range self.gui.State.ContextMgr.ContextStack {
f(context)
} }
} }
// getFocusLayout returns a manager function for when view gain and lose focus func (self *ContextMgr) IsCurrent(c types.Context) bool {
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error { return self.Current().GetKey() == c.GetKey()
var previousView *gocui.View }
return func(g *gocui.Gui) error {
newView := gui.g.CurrentView()
// for now we don't consider losing focus to a popup panel as actually losing focus
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
if err := gui.onViewFocusLost(previousView); err != nil {
return err
}
previousView = newView // all list contexts
func (self *ContextMgr) AllList() []types.IListContext {
var listContexts []types.IListContext
for _, context := range self.allContexts.Flatten() {
if listContext, ok := context.(types.IListContext); ok {
listContexts = append(listContexts, listContext)
} }
return nil
}
}
func (gui *Gui) onViewFocusLost(oldView *gocui.View) error {
if oldView == nil {
return nil
} }
oldView.Highlight = false return listContexts
_ = oldView.SetOriginX(0)
return nil
} }
func (gui *Gui) TransientContexts() []types.Context { func (self *ContextMgr) AllPatchExplorer() []types.IPatchExplorerContext {
return slices.Filter(gui.State.Contexts.Flatten(), func(context types.Context) bool { var listContexts []types.IPatchExplorerContext
return context.IsTransient()
})
}
func (gui *Gui) rerenderView(view *gocui.View) error { for _, context := range self.allContexts.Flatten() {
context, ok := gui.contextForView(view.Name()) if listContext, ok := context.(types.IPatchExplorerContext); ok {
if !ok { listContexts = append(listContexts, listContext)
gui.Log.Errorf("no context found for view %s", view.Name()) }
return nil
} }
return context.HandleRender() return listContexts
} }
func (gui *Gui) getSideContextSelectedItemId() string {
currentSideContext := gui.currentSideListContext()
if currentSideContext == nil {
return ""
}
return currentSideContext.GetSelectedItemId()
}
// currently unused
// func (gui *Gui) getCurrentSideView() *gocui.View {
// currentSideContext := gui.currentSideContext()
// if currentSideContext == nil {
// return nil
// }
// view, _ := gui.g.View(currentSideContext.GetViewName())
// return view
// }
// currently unused
// func (gui *Gui) renderContextStack() string {
// result := ""
// for _, context := range gui.State.ContextManager.ContextStack {
// result += context.GetViewName() + "\n"
// }
// return result
// }

View File

@ -16,6 +16,9 @@ type BaseContext struct {
keybindingsFns []types.KeybindingsFn keybindingsFns []types.KeybindingsFn
mouseKeybindingsFns []types.MouseKeybindingsFn mouseKeybindingsFns []types.MouseKeybindingsFn
onClickFn func() error onClickFn func() error
onRenderToMainFn func() error
onFocusFn onFocusFn
onFocusLostFn onFocusLostFn
focusable bool focusable bool
transient bool transient bool
@ -25,6 +28,11 @@ type BaseContext struct {
*ParentContextMgr *ParentContextMgr
} }
type (
onFocusFn = func(types.OnFocusOpts) error
onFocusLostFn = func(types.OnFocusLostOpts) error
)
var _ types.IBaseContext = &BaseContext{} var _ types.IBaseContext = &BaseContext{}
type NewBaseContextOpts struct { type NewBaseContextOpts struct {
@ -129,6 +137,36 @@ func (self *BaseContext) GetOnClick() func() error {
return self.onClickFn return self.onClickFn
} }
func (self *BaseContext) AddOnRenderToMainFn(fn func() error) {
if fn != nil {
self.onRenderToMainFn = fn
}
}
func (self *BaseContext) GetOnRenderToMain() func() error {
return self.onRenderToMainFn
}
func (self *BaseContext) AddOnFocusFn(fn onFocusFn) {
if fn != nil {
self.onFocusFn = fn
}
}
func (self *BaseContext) GetOnFocus() onFocusFn {
return self.onFocusFn
}
func (self *BaseContext) AddOnFocusLostFn(fn onFocusLostFn) {
if fn != nil {
self.onFocusLostFn = fn
}
}
func (self *BaseContext) GetOnFocusLost() onFocusLostFn {
return self.onFocusLostFn
}
func (self *BaseContext) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding { func (self *BaseContext) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
bindings := []*gocui.ViewMouseBinding{} bindings := []*gocui.ViewMouseBinding{}
for i := range self.mouseKeybindingsFns { for i := range self.mouseKeybindingsFns {

View File

@ -1,8 +1,8 @@
package context package context
import ( import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -11,40 +11,40 @@ type BranchesContext struct {
*ListContextTrait *ListContextTrait
} }
var _ types.IListContext = (*BranchesContext)(nil) var (
_ types.IListContext = (*BranchesContext)(nil)
_ types.DiffableContext = (*BranchesContext)(nil)
)
func NewBranchesContext( func NewBranchesContext(c *ContextCommon) *BranchesContext {
getModel func() []*models.Branch, viewModel := NewBasicViewModel(func() []*models.Branch { return c.Model().Branches })
view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error, getDisplayStrings := func(startIdx int, length int) [][]string {
onRenderToMain func() error, return presentation.GetBranchListDisplayStrings(
onFocusLost func(opts types.OnFocusLostOpts) error, c.Model().Branches,
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().Diffing.Ref,
c.Tr,
)
}
c *types.HelperCommon, self := &BranchesContext{
) *BranchesContext {
viewModel := NewBasicViewModel(getModel)
return &BranchesContext{
BasicViewModel: viewModel, BasicViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().Branches,
WindowName: "branches", WindowName: "branches",
Key: LOCAL_BRANCHES_CONTEXT_KEY, Key: LOCAL_BRANCHES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
}, },
} }
return self
} }
func (self *BranchesContext) GetSelectedItemId() string { func (self *BranchesContext) GetSelectedItemId() string {
@ -63,3 +63,16 @@ func (self *BranchesContext) GetSelectedRef() types.Ref {
} }
return branch return branch
} }
func (self *BranchesContext) GetDiffTerminals() []string {
// for our local branches we want to include both the branch and its upstream
branch := self.GetSelected()
if branch != nil {
names := []string{branch.ID()}
if branch.IsTrackingRemote() {
names = append(names, branch.ID()+"@{u}")
}
return names
}
return nil
}

View File

@ -1,9 +1,11 @@
package context package context
import ( import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/gui/filetree"
"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/gui/types"
) )
@ -13,20 +15,28 @@ type CommitFilesContext struct {
*DynamicTitleBuilder *DynamicTitleBuilder
} }
var _ types.IListContext = (*CommitFilesContext)(nil) var (
_ types.IListContext = (*CommitFilesContext)(nil)
_ types.DiffableContext = (*CommitFilesContext)(nil)
)
func NewCommitFilesContext( func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
getModel func() []*models.CommitFile, viewModel := filetree.NewCommitFileTreeViewModel(
view *gocui.View, func() []*models.CommitFile { return c.Model().CommitFiles },
getDisplayStrings func(startIdx int, length int) [][]string, c.Log,
c.UserConfig.Gui.ShowFileTree,
)
onFocus func(types.OnFocusOpts) error, getDisplayStrings := func(startIdx int, length int) [][]string {
onRenderToMain func() error, if viewModel.Len() == 0 {
onFocusLost func(opts types.OnFocusLostOpts) error, return [][]string{{style.FgRed.Sprint("(none)")}}
}
c *types.HelperCommon, lines := presentation.RenderCommitFileTree(viewModel, c.Modes().Diffing.Ref, c.Git().Patch.PatchBuilder)
) *CommitFilesContext { return slices.Map(lines, func(line string) []string {
viewModel := filetree.NewCommitFileTreeViewModel(getModel, c.Log, c.UserConfig.Gui.ShowFileTree) return []string{line}
})
}
return &CommitFilesContext{ return &CommitFilesContext{
CommitFileTreeViewModel: viewModel, CommitFileTreeViewModel: viewModel,
@ -34,18 +44,14 @@ func NewCommitFilesContext(
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext( Context: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{ NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().CommitFiles,
WindowName: "commits", WindowName: "commits",
Key: COMMIT_FILES_CONTEXT_KEY, Key: COMMIT_FILES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
Transient: true, Transient: true,
}), }),
ContextCallbackOpts{ ),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -61,3 +67,7 @@ func (self *CommitFilesContext) GetSelectedItemId() string {
return item.ID() return item.ID()
} }
func (self *CommitFilesContext) GetDiffTerminals() []string {
return []string{self.GetRef().RefName()}
}

View File

@ -1,15 +1,21 @@
package context package context
import ( import (
"strconv"
"strings"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
type CommitMessageContext struct { type CommitMessageContext struct {
c *ContextCommon
types.Context types.Context
viewModel *CommitMessageViewModel viewModel *CommitMessageViewModel
} }
var _ types.Context = (*CommitMessageContext)(nil)
// when selectedIndex (see below) is set to this value, it means that we're not // when selectedIndex (see below) is set to this value, it means that we're not
// currently viewing a commit message of an existing commit: instead we're making our own // currently viewing a commit message of an existing commit: instead we're making our own
// new commit message // new commit message
@ -35,22 +41,21 @@ type CommitMessageViewModel struct {
} }
func NewCommitMessageContext( func NewCommitMessageContext(
view *gocui.View, c *ContextCommon,
opts ContextCallbackOpts,
) *CommitMessageContext { ) *CommitMessageContext {
viewModel := &CommitMessageViewModel{} viewModel := &CommitMessageViewModel{}
return &CommitMessageContext{ return &CommitMessageContext{
c: c,
viewModel: viewModel, viewModel: viewModel,
Context: NewSimpleContext( Context: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{ NewBaseContext(NewBaseContextOpts{
Kind: types.PERSISTENT_POPUP, Kind: types.PERSISTENT_POPUP,
View: view, View: c.Views().CommitMessage,
WindowName: "commitMessage", WindowName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY, Key: COMMIT_MESSAGE_CONTEXT_KEY,
Focusable: true, Focusable: true,
HasUncontrolledBounds: true, HasUncontrolledBounds: true,
}), }),
opts,
), ),
} }
} }
@ -93,3 +98,15 @@ func (self *CommitMessageContext) SetPanelState(index int, title string, preserv
self.viewModel.onConfirm = onConfirm self.viewModel.onConfirm = onConfirm
self.GetView().Title = title self.GetView().Title = title
} }
func (self *CommitMessageContext) RenderCommitLength() {
if !self.c.UserConfig.Gui.CommitLength.Show {
return
}
self.c.Views().CommitMessage.Subtitle = getBufferLength(self.c.Views().CommitMessage)
}
func getBufferLength(view *gocui.View) string {
return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " "
}

View File

@ -0,0 +1,35 @@
package context
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ConfirmationContext struct {
*SimpleContext
c *ContextCommon
State ConfirmationContextState
}
type ConfirmationContextState struct {
OnConfirm func() error
OnClose func() error
}
var _ types.Context = (*ConfirmationContext)(nil)
func NewConfirmationContext(
c *ContextCommon,
) *ConfirmationContext {
return &ConfirmationContext{
c: c,
SimpleContext: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Confirmation,
WindowName: "confirmation",
Key: CONFIRMATION_CONTEXT_KEY,
Kind: types.TEMPORARY_POPUP,
Focusable: true,
HasUncontrolledBounds: true,
})),
}
}

View File

@ -97,7 +97,7 @@ type ContextTree struct {
CustomPatchBuilder *PatchExplorerContext CustomPatchBuilder *PatchExplorerContext
CustomPatchBuilderSecondary types.Context CustomPatchBuilderSecondary types.Context
MergeConflicts *MergeConflictsContext MergeConflicts *MergeConflictsContext
Confirmation types.Context Confirmation *ConfirmationContext
CommitMessage *CommitMessageContext CommitMessage *CommitMessageContext
CommitDescription types.Context CommitDescription types.Context
CommandLog types.Context CommandLog types.Context

View File

@ -0,0 +1,11 @@
package context
import (
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ContextCommon struct {
*common.Common
types.IGuiCommon
}

View File

@ -10,11 +10,13 @@ import (
type ListContextTrait struct { type ListContextTrait struct {
types.Context types.Context
c *types.HelperCommon c *ContextCommon
list types.IList list types.IList
getDisplayStrings func(startIdx int, length int) [][]string getDisplayStrings func(startIdx int, length int) [][]string
} }
func (self *ListContextTrait) IsListContext() {}
func (self *ListContextTrait) GetList() types.IList { func (self *ListContextTrait) GetList() types.IList {
return self.list return self.list
} }

View File

@ -1,8 +1,11 @@
package context package context
import ( import (
"github.com/jesseduffield/gocui" "log"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -11,36 +14,57 @@ type LocalCommitsContext struct {
*ViewportListContextTrait *ViewportListContextTrait
} }
var _ types.IListContext = (*LocalCommitsContext)(nil) var (
_ types.IListContext = (*LocalCommitsContext)(nil)
_ types.DiffableContext = (*LocalCommitsContext)(nil)
)
func NewLocalCommitsContext( func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
getModel func() []*models.Commit, viewModel := NewLocalCommitsViewModel(
view *gocui.View, func() []*models.Commit { return c.Model().Commits },
getDisplayStrings func(startIdx int, length int) [][]string, c,
)
onFocus func(types.OnFocusOpts) error, getDisplayStrings := func(startIdx int, length int) [][]string {
onRenderToMain func() error, selectedCommitSha := ""
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon, if c.CurrentContext().GetKey() == LOCAL_COMMITS_CONTEXT_KEY {
) *LocalCommitsContext { selectedCommit := viewModel.GetSelected()
viewModel := NewLocalCommitsViewModel(getModel, c) if selectedCommit != nil {
selectedCommitSha = selectedCommit.Sha
}
}
showYouAreHereLabel := c.Model().WorkingTreeStateAtLastCommitRefresh == enums.REBASE_MODE_REBASING
return presentation.GetCommitListDisplayStrings(
c.Common,
c.Model().Commits,
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().CherryPicking.SelectedShaSet(),
c.Modes().Diffing.Ref,
c.UserConfig.Gui.TimeFormat,
c.UserConfig.Git.ParseEmoji,
selectedCommitSha,
startIdx,
length,
shouldShowGraph(c),
c.Model().BisectInfo,
showYouAreHereLabel,
)
}
return &LocalCommitsContext{ return &LocalCommitsContext{
LocalCommitsViewModel: viewModel, LocalCommitsViewModel: viewModel,
ViewportListContextTrait: &ViewportListContextTrait{ ViewportListContextTrait: &ViewportListContextTrait{
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().Commits,
WindowName: "commits", WindowName: "commits",
Key: LOCAL_COMMITS_CONTEXT_KEY, Key: LOCAL_COMMITS_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -69,7 +93,7 @@ type LocalCommitsViewModel struct {
showWholeGitGraph bool showWholeGitGraph bool
} }
func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *types.HelperCommon) *LocalCommitsViewModel { func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *ContextCommon) *LocalCommitsViewModel {
self := &LocalCommitsViewModel{ self := &LocalCommitsViewModel{
BasicViewModel: NewBasicViewModel(getModel), BasicViewModel: NewBasicViewModel(getModel),
limitCommits: true, limitCommits: true,
@ -91,6 +115,12 @@ func (self *LocalCommitsContext) GetSelectedRef() types.Ref {
return commit return commit
} }
func (self *LocalCommitsContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}
func (self *LocalCommitsViewModel) SetLimitCommits(value bool) { func (self *LocalCommitsViewModel) SetLimitCommits(value bool) {
self.limitCommits = value self.limitCommits = value
} }
@ -110,3 +140,22 @@ func (self *LocalCommitsViewModel) GetShowWholeGitGraph() bool {
func (self *LocalCommitsViewModel) GetCommits() []*models.Commit { func (self *LocalCommitsViewModel) GetCommits() []*models.Commit {
return self.getModel() return self.getModel()
} }
func shouldShowGraph(c *ContextCommon) bool {
if c.Modes().Filtering.Active() {
return false
}
value := c.UserConfig.Git.Log.ShowGraph
switch value {
case "always":
return true
case "never":
return false
case "when-maximised":
return c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL
}
log.Fatalf("Unknown value for git.log.showGraph: %s. Expected one of: 'always', 'never', 'when-maximised'", value)
return false
}

View File

@ -2,7 +2,6 @@ package context
import ( import (
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
@ -16,34 +15,21 @@ type MenuContext struct {
var _ types.IListContext = (*MenuContext)(nil) var _ types.IListContext = (*MenuContext)(nil)
func NewMenuContext( func NewMenuContext(
view *gocui.View, c *ContextCommon,
c *types.HelperCommon,
getOptionsMap func() map[string]string,
renderToDescriptionView func(string),
) *MenuContext { ) *MenuContext {
viewModel := NewMenuViewModel() viewModel := NewMenuViewModel()
onFocus := func(types.OnFocusOpts) error {
selectedMenuItem := viewModel.GetSelected()
renderToDescriptionView(selectedMenuItem.Tooltip)
return nil
}
return &MenuContext{ return &MenuContext{
MenuViewModel: viewModel, MenuViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().Menu,
WindowName: "menu", WindowName: "menu",
Key: "menu", Key: "menu",
Kind: types.TEMPORARY_POPUP, Kind: types.TEMPORARY_POPUP,
OnGetOptionsMap: getOptionsMap,
Focusable: true, Focusable: true,
HasUncontrolledBounds: true, HasUncontrolledBounds: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
}),
getDisplayStrings: viewModel.GetDisplayStrings, getDisplayStrings: viewModel.GetDisplayStrings,
list: viewModel, list: viewModel,
c: c, c: c,

View File

@ -3,7 +3,6 @@ package context
import ( import (
"math" "math"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/sasha-s/go-deadlock" "github.com/sasha-s/go-deadlock"
@ -12,7 +11,7 @@ import (
type MergeConflictsContext struct { type MergeConflictsContext struct {
types.Context types.Context
viewModel *ConflictsViewModel viewModel *ConflictsViewModel
c *types.HelperCommon c *ContextCommon
mutex *deadlock.Mutex mutex *deadlock.Mutex
} }
@ -25,12 +24,7 @@ type ConflictsViewModel struct {
} }
func NewMergeConflictsContext( func NewMergeConflictsContext(
view *gocui.View, c *ContextCommon,
opts ContextCallbackOpts,
c *types.HelperCommon,
getOptionsMap func() map[string]string,
) *MergeConflictsContext { ) *MergeConflictsContext {
viewModel := &ConflictsViewModel{ viewModel := &ConflictsViewModel{
state: mergeconflicts.NewState(), state: mergeconflicts.NewState(),
@ -43,14 +37,12 @@ func NewMergeConflictsContext(
Context: NewSimpleContext( Context: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{ NewBaseContext(NewBaseContextOpts{
Kind: types.MAIN_CONTEXT, Kind: types.MAIN_CONTEXT,
View: view, View: c.Views().MergeConflicts,
WindowName: "main", WindowName: "main",
Key: MERGE_CONFLICTS_CONTEXT_KEY, Key: MERGE_CONFLICTS_CONTEXT_KEY,
OnGetOptionsMap: getOptionsMap,
Focusable: true, Focusable: true,
HighlightOnFocus: true, HighlightOnFocus: true,
}), }),
opts,
), ),
c: c, c: c,
} }

View File

@ -13,7 +13,7 @@ type PatchExplorerContext struct {
state *patch_exploring.State state *patch_exploring.State
viewTrait *ViewTrait viewTrait *ViewTrait
getIncludedLineIndices func() []int getIncludedLineIndices func() []int
c *types.HelperCommon c *ContextCommon
mutex *deadlock.Mutex mutex *deadlock.Mutex
} }
@ -24,11 +24,9 @@ func NewPatchExplorerContext(
windowName string, windowName string,
key types.ContextKey, key types.ContextKey,
onFocus func(types.OnFocusOpts) error,
onFocusLost func(opts types.OnFocusLostOpts) error,
getIncludedLineIndices func() []int, getIncludedLineIndices func() []int,
c *types.HelperCommon, c *ContextCommon,
) *PatchExplorerContext { ) *PatchExplorerContext {
return &PatchExplorerContext{ return &PatchExplorerContext{
state: nil, state: nil,
@ -43,13 +41,12 @@ func NewPatchExplorerContext(
Kind: types.MAIN_CONTEXT, Kind: types.MAIN_CONTEXT,
Focusable: true, Focusable: true,
HighlightOnFocus: true, HighlightOnFocus: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
}),
} }
} }
func (self *PatchExplorerContext) IsPatchExplorerContext() {}
func (self *PatchExplorerContext) GetState() *patch_exploring.State { func (self *PatchExplorerContext) GetState() *patch_exploring.State {
return self.state return self.state
} }

View File

@ -1,8 +1,8 @@
package context package context
import ( import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -11,35 +11,35 @@ type ReflogCommitsContext struct {
*ListContextTrait *ListContextTrait
} }
var _ types.IListContext = (*ReflogCommitsContext)(nil) var (
_ types.IListContext = (*ReflogCommitsContext)(nil)
_ types.DiffableContext = (*ReflogCommitsContext)(nil)
)
func NewReflogCommitsContext( func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
getModel func() []*models.Commit, viewModel := NewBasicViewModel(func() []*models.Commit { return c.Model().FilteredReflogCommits })
view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error, getDisplayStrings := func(startIdx int, length int) [][]string {
onRenderToMain func() error, return presentation.GetReflogCommitListDisplayStrings(
onFocusLost func(opts types.OnFocusLostOpts) error, c.Model().FilteredReflogCommits,
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c *types.HelperCommon, c.Modes().CherryPicking.SelectedShaSet(),
) *ReflogCommitsContext { c.Modes().Diffing.Ref,
viewModel := NewBasicViewModel(getModel) c.UserConfig.Gui.TimeFormat,
c.UserConfig.Git.ParseEmoji,
)
}
return &ReflogCommitsContext{ return &ReflogCommitsContext{
BasicViewModel: viewModel, BasicViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().ReflogCommits,
WindowName: "commits", WindowName: "commits",
Key: REFLOG_COMMITS_CONTEXT_KEY, Key: REFLOG_COMMITS_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -71,3 +71,9 @@ func (self *ReflogCommitsContext) GetSelectedRef() types.Ref {
func (self *ReflogCommitsContext) GetCommits() []*models.Commit { func (self *ReflogCommitsContext) GetCommits() []*models.Commit {
return self.getModel() return self.getModel()
} }
func (self *ReflogCommitsContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

View File

@ -1,8 +1,8 @@
package context package context
import ( import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -12,37 +12,32 @@ type RemoteBranchesContext struct {
*DynamicTitleBuilder *DynamicTitleBuilder
} }
var _ types.IListContext = (*RemoteBranchesContext)(nil) var (
_ types.IListContext = (*RemoteBranchesContext)(nil)
_ types.DiffableContext = (*RemoteBranchesContext)(nil)
)
func NewRemoteBranchesContext( func NewRemoteBranchesContext(
getModel func() []*models.RemoteBranch, c *ContextCommon,
view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon,
) *RemoteBranchesContext { ) *RemoteBranchesContext {
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(func() []*models.RemoteBranch { return c.Model().RemoteBranches })
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetRemoteBranchListDisplayStrings(c.Model().RemoteBranches, c.Modes().Diffing.Ref)
}
return &RemoteBranchesContext{ return &RemoteBranchesContext{
BasicViewModel: viewModel, BasicViewModel: viewModel,
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle), DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle),
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().RemoteBranches,
WindowName: "branches", WindowName: "branches",
Key: REMOTE_BRANCHES_CONTEXT_KEY, Key: REMOTE_BRANCHES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
Transient: true, Transient: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -66,3 +61,9 @@ func (self *RemoteBranchesContext) GetSelectedRef() types.Ref {
} }
return remoteBranch return remoteBranch
} }
func (self *RemoteBranchesContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

View File

@ -1,8 +1,8 @@
package context package context
import ( import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -11,35 +11,28 @@ type RemotesContext struct {
*ListContextTrait *ListContextTrait
} }
var _ types.IListContext = (*RemotesContext)(nil) var (
_ types.IListContext = (*RemotesContext)(nil)
_ types.DiffableContext = (*RemotesContext)(nil)
)
func NewRemotesContext( func NewRemotesContext(c *ContextCommon) *RemotesContext {
getModel func() []*models.Remote, viewModel := NewBasicViewModel(func() []*models.Remote { return c.Model().Remotes })
view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error, getDisplayStrings := func(startIdx int, length int) [][]string {
onRenderToMain func() error, return presentation.GetRemoteListDisplayStrings(c.Model().Remotes, c.Modes().Diffing.Ref)
onFocusLost func(opts types.OnFocusLostOpts) error, }
c *types.HelperCommon,
) *RemotesContext {
viewModel := NewBasicViewModel(getModel)
return &RemotesContext{ return &RemotesContext{
BasicViewModel: viewModel, BasicViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().Remotes,
WindowName: "branches", WindowName: "branches",
Key: REMOTES_CONTEXT_KEY, Key: REMOTES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -55,3 +48,9 @@ func (self *RemotesContext) GetSelectedItemId() string {
return item.ID() return item.ID()
} }
func (self *RemotesContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

146
pkg/gui/context/setup.go Normal file
View File

@ -0,0 +1,146 @@
package context
import "github.com/jesseduffield/lazygit/pkg/gui/types"
func NewContextTree(c *ContextCommon) *ContextTree {
commitFilesContext := NewCommitFilesContext(c)
return &ContextTree{
Global: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.GLOBAL_CONTEXT,
View: nil, // TODO: see if this breaks anything
WindowName: "",
Key: GLOBAL_CONTEXT_KEY,
Focusable: false,
HasUncontrolledBounds: true, // setting to true because the global context doesn't even have a view
}),
),
Status: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.SIDE_CONTEXT,
View: c.Views().Status,
WindowName: "status",
Key: STATUS_CONTEXT_KEY,
Focusable: true,
}),
),
Files: NewWorkingTreeContext(c),
Submodules: NewSubmodulesContext(c),
Menu: NewMenuContext(c),
Remotes: NewRemotesContext(c),
RemoteBranches: NewRemoteBranchesContext(c),
LocalCommits: NewLocalCommitsContext(c),
CommitFiles: commitFilesContext,
ReflogCommits: NewReflogCommitsContext(c),
SubCommits: NewSubCommitsContext(c),
Branches: NewBranchesContext(c),
Tags: NewTagsContext(c),
Stash: NewStashContext(c),
Suggestions: NewSuggestionsContext(c),
Normal: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.MAIN_CONTEXT,
View: c.Views().Main,
WindowName: "main",
Key: NORMAL_MAIN_CONTEXT_KEY,
Focusable: false,
}),
),
NormalSecondary: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.MAIN_CONTEXT,
View: c.Views().Secondary,
WindowName: "secondary",
Key: NORMAL_SECONDARY_CONTEXT_KEY,
Focusable: false,
}),
),
Staging: NewPatchExplorerContext(
c.Views().Staging,
"main",
STAGING_MAIN_CONTEXT_KEY,
func() []int { return nil },
c,
),
StagingSecondary: NewPatchExplorerContext(
c.Views().StagingSecondary,
"secondary",
STAGING_SECONDARY_CONTEXT_KEY,
func() []int { return nil },
c,
),
CustomPatchBuilder: NewPatchExplorerContext(
c.Views().PatchBuilding,
"main",
PATCH_BUILDING_MAIN_CONTEXT_KEY,
func() []int {
filename := commitFilesContext.GetSelectedPath()
includedLineIndices, err := c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename)
if err != nil {
c.Log.Error(err)
return nil
}
return includedLineIndices
},
c,
),
CustomPatchBuilderSecondary: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.MAIN_CONTEXT,
View: c.Views().PatchBuildingSecondary,
WindowName: "secondary",
Key: PATCH_BUILDING_SECONDARY_CONTEXT_KEY,
Focusable: false,
}),
),
MergeConflicts: NewMergeConflictsContext(
c,
),
Confirmation: NewConfirmationContext(c),
CommitMessage: NewCommitMessageContext(c),
CommitDescription: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.PERSISTENT_POPUP,
View: c.Views().CommitDescription,
WindowName: "commitDescription",
Key: COMMIT_DESCRIPTION_CONTEXT_KEY,
Focusable: true,
HasUncontrolledBounds: true,
}),
),
Search: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.PERSISTENT_POPUP,
View: c.Views().Search,
WindowName: "search",
Key: SEARCH_CONTEXT_KEY,
Focusable: true,
}),
),
CommandLog: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.EXTRAS_CONTEXT,
View: c.Views().Extras,
WindowName: "extras",
Key: COMMAND_LOG_CONTEXT_KEY,
Focusable: true,
}),
),
Snake: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.SIDE_CONTEXT,
View: c.Views().Snake,
WindowName: "files",
Key: SNAKE_CONTEXT_KEY,
Focusable: true,
}),
),
Options: NewDisplayContext(OPTIONS_CONTEXT_KEY, c.Views().Options, "options"),
AppStatus: NewDisplayContext(APP_STATUS_CONTEXT_KEY, c.Views().AppStatus, "appStatus"),
SearchPrefix: NewDisplayContext(SEARCH_PREFIX_CONTEXT_KEY, c.Views().SearchPrefix, "searchPrefix"),
Information: NewDisplayContext(INFORMATION_CONTEXT_KEY, c.Views().Information, "information"),
Limit: NewDisplayContext(LIMIT_CONTEXT_KEY, c.Views().Limit, "limit"),
}
}

View File

@ -6,29 +6,12 @@ import (
) )
type SimpleContext struct { type SimpleContext struct {
OnFocus func(opts types.OnFocusOpts) error
OnFocusLost func(opts types.OnFocusLostOpts) error
OnRender func() error
// this is for pushing some content to the main view
OnRenderToMain func() error
*BaseContext *BaseContext
} }
type ContextCallbackOpts struct { func NewSimpleContext(baseContext *BaseContext) *SimpleContext {
OnFocus func(opts types.OnFocusOpts) error
OnFocusLost func(opts types.OnFocusLostOpts) error
OnRender func() error
OnRenderToMain func() error
}
func NewSimpleContext(baseContext *BaseContext, opts ContextCallbackOpts) *SimpleContext {
return &SimpleContext{ return &SimpleContext{
OnFocus: opts.OnFocus, BaseContext: baseContext,
OnFocusLost: opts.OnFocusLost,
OnRender: opts.OnRender,
OnRenderToMain: opts.OnRenderToMain,
BaseContext: baseContext,
} }
} }
@ -45,7 +28,6 @@ func NewDisplayContext(key types.ContextKey, view *gocui.View, windowName string
Focusable: false, Focusable: false,
Transient: false, Transient: false,
}), }),
ContextCallbackOpts{},
) )
} }
@ -54,14 +36,14 @@ func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) error {
self.GetViewTrait().SetHighlight(true) self.GetViewTrait().SetHighlight(true)
} }
if self.OnFocus != nil { if self.onFocusFn != nil {
if err := self.OnFocus(opts); err != nil { if err := self.onFocusFn(opts); err != nil {
return err return err
} }
} }
if self.OnRenderToMain != nil { if self.onRenderToMainFn != nil {
if err := self.OnRenderToMain(); err != nil { if err := self.onRenderToMainFn(); err != nil {
return err return err
} }
} }
@ -70,22 +52,19 @@ func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) error {
} }
func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) error { func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) error {
if self.OnFocusLost != nil { if self.onFocusLostFn != nil {
return self.OnFocusLost(opts) return self.onFocusLostFn(opts)
} }
return nil return nil
} }
func (self *SimpleContext) HandleRender() error { func (self *SimpleContext) HandleRender() error {
if self.OnRender != nil {
return self.OnRender()
}
return nil return nil
} }
func (self *SimpleContext) HandleRenderToMain() error { func (self *SimpleContext) HandleRenderToMain() error {
if self.OnRenderToMain != nil { if self.onRenderToMainFn != nil {
return self.OnRenderToMain() return self.onRenderToMainFn()
} }
return nil return nil

View File

@ -1,8 +1,8 @@
package context package context
import ( import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -11,35 +11,30 @@ type StashContext struct {
*ListContextTrait *ListContextTrait
} }
var _ types.IListContext = (*StashContext)(nil) var (
_ types.IListContext = (*StashContext)(nil)
_ types.DiffableContext = (*StashContext)(nil)
)
func NewStashContext( func NewStashContext(
getModel func() []*models.StashEntry, c *ContextCommon,
view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon,
) *StashContext { ) *StashContext {
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(func() []*models.StashEntry { return c.Model().StashEntries })
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetStashEntryListDisplayStrings(c.Model().StashEntries, c.Modes().Diffing.Ref)
}
return &StashContext{ return &StashContext{
BasicViewModel: viewModel, BasicViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().Stash,
WindowName: "stash", WindowName: "stash",
Key: STASH_CONTEXT_KEY, Key: STASH_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -67,3 +62,9 @@ func (self *StashContext) GetSelectedRef() types.Ref {
} }
return stash return stash
} }
func (self *StashContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

View File

@ -3,8 +3,9 @@ package context
import ( import (
"fmt" "fmt"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
@ -15,23 +16,45 @@ type SubCommitsContext struct {
*DynamicTitleBuilder *DynamicTitleBuilder
} }
var _ types.IListContext = (*SubCommitsContext)(nil) var (
_ types.IListContext = (*SubCommitsContext)(nil)
_ types.DiffableContext = (*SubCommitsContext)(nil)
)
func NewSubCommitsContext( func NewSubCommitsContext(
getModel func() []*models.Commit, c *ContextCommon,
view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon,
) *SubCommitsContext { ) *SubCommitsContext {
viewModel := &SubCommitsViewModel{ viewModel := &SubCommitsViewModel{
BasicViewModel: NewBasicViewModel(getModel), BasicViewModel: NewBasicViewModel(
ref: nil, func() []*models.Commit { return c.Model().SubCommits },
limitCommits: true, ),
ref: nil,
limitCommits: true,
}
getDisplayStrings := func(startIdx int, length int) [][]string {
selectedCommitSha := ""
if c.CurrentContext().GetKey() == SUB_COMMITS_CONTEXT_KEY {
selectedCommit := viewModel.GetSelected()
if selectedCommit != nil {
selectedCommitSha = selectedCommit.Sha
}
}
return presentation.GetCommitListDisplayStrings(
c.Common,
c.Model().SubCommits,
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().CherryPicking.SelectedShaSet(),
c.Modes().Diffing.Ref,
c.UserConfig.Gui.TimeFormat,
c.UserConfig.Git.ParseEmoji,
selectedCommitSha,
startIdx,
length,
shouldShowGraph(c),
git_commands.NewNullBisectInfo(),
false,
)
} }
return &SubCommitsContext{ return &SubCommitsContext{
@ -40,17 +63,13 @@ func NewSubCommitsContext(
ViewportListContextTrait: &ViewportListContextTrait{ ViewportListContextTrait: &ViewportListContextTrait{
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().SubCommits,
WindowName: "branches", WindowName: "branches",
Key: SUB_COMMITS_CONTEXT_KEY, Key: SUB_COMMITS_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
Transient: true, Transient: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -111,3 +130,9 @@ func (self *SubCommitsContext) SetLimitCommits(value bool) {
func (self *SubCommitsContext) GetLimitCommits() bool { func (self *SubCommitsContext) GetLimitCommits() bool {
return self.limitCommits return self.limitCommits
} }
func (self *SubCommitsContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

View File

@ -1,8 +1,8 @@
package context package context
import ( import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -13,33 +13,23 @@ type SubmodulesContext struct {
var _ types.IListContext = (*SubmodulesContext)(nil) var _ types.IListContext = (*SubmodulesContext)(nil)
func NewSubmodulesContext( func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext {
getModel func() []*models.SubmoduleConfig, viewModel := NewBasicViewModel(func() []*models.SubmoduleConfig { return c.Model().Submodules })
view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error, getDisplayStrings := func(startIdx int, length int) [][]string {
onRenderToMain func() error, return presentation.GetSubmoduleListDisplayStrings(c.Model().Submodules)
onFocusLost func(opts types.OnFocusLostOpts) error, }
c *types.HelperCommon,
) *SubmodulesContext {
viewModel := NewBasicViewModel(getModel)
return &SubmodulesContext{ return &SubmodulesContext{
BasicViewModel: viewModel, BasicViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().Submodules,
WindowName: "files", WindowName: "files",
Key: SUBMODULES_CONTEXT_KEY, Key: SUBMODULES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,

View File

@ -1,45 +1,59 @@
package context package context
import ( import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/tasks"
) )
type SuggestionsContext struct { type SuggestionsContext struct {
*BasicViewModel[*types.Suggestion] *BasicViewModel[*types.Suggestion]
*ListContextTrait *ListContextTrait
State *SuggestionsContextState
}
type SuggestionsContextState struct {
Suggestions []*types.Suggestion
OnConfirm func() error
OnClose func() error
AsyncHandler *tasks.AsyncHandler
// FindSuggestions will take a string that the user has typed into a prompt
// and return a slice of suggestions which match that string.
FindSuggestions func(string) []*types.Suggestion
} }
var _ types.IListContext = (*SuggestionsContext)(nil) var _ types.IListContext = (*SuggestionsContext)(nil)
func NewSuggestionsContext( func NewSuggestionsContext(
getModel func() []*types.Suggestion, c *ContextCommon,
view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon,
) *SuggestionsContext { ) *SuggestionsContext {
state := &SuggestionsContextState{
AsyncHandler: tasks.NewAsyncHandler(),
}
getModel := func() []*types.Suggestion {
return state.Suggestions
}
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetSuggestionListDisplayStrings(state.Suggestions)
}
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(getModel)
return &SuggestionsContext{ return &SuggestionsContext{
State: state,
BasicViewModel: viewModel, BasicViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().Suggestions,
WindowName: "suggestions", WindowName: "suggestions",
Key: SUGGESTIONS_CONTEXT_KEY, Key: SUGGESTIONS_CONTEXT_KEY,
Kind: types.PERSISTENT_POPUP, Kind: types.PERSISTENT_POPUP,
Focusable: true, Focusable: true,
HasUncontrolledBounds: true, HasUncontrolledBounds: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -55,3 +69,22 @@ func (self *SuggestionsContext) GetSelectedItemId() string {
return item.Value return item.Value
} }
func (self *SuggestionsContext) SetSuggestions(suggestions []*types.Suggestion) {
self.State.Suggestions = suggestions
self.SetSelectedLineIdx(0)
self.c.ResetViewOrigin(self.GetView())
_ = self.HandleRender()
}
func (self *SuggestionsContext) RefreshSuggestions() {
self.State.AsyncHandler.Do(func() func() {
findSuggestionsFn := self.State.FindSuggestions
if findSuggestionsFn != nil {
suggestions := findSuggestionsFn(self.c.GetPromptInput())
return func() { self.SetSuggestions(suggestions) }
} else {
return func() {}
}
})
}

View File

@ -1,8 +1,8 @@
package context package context
import ( import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -11,35 +11,30 @@ type TagsContext struct {
*ListContextTrait *ListContextTrait
} }
var _ types.IListContext = (*TagsContext)(nil) var (
_ types.IListContext = (*TagsContext)(nil)
_ types.DiffableContext = (*TagsContext)(nil)
)
func NewTagsContext( func NewTagsContext(
getModel func() []*models.Tag, c *ContextCommon,
view *gocui.View,
getDisplayStrings func(startIdx int, length int) [][]string,
onFocus func(types.OnFocusOpts) error,
onRenderToMain func() error,
onFocusLost func(opts types.OnFocusLostOpts) error,
c *types.HelperCommon,
) *TagsContext { ) *TagsContext {
viewModel := NewBasicViewModel(getModel) viewModel := NewBasicViewModel(func() []*models.Tag { return c.Model().Tags })
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetTagListDisplayStrings(c.Model().Tags, c.Modes().Diffing.Ref)
}
return &TagsContext{ return &TagsContext{
BasicViewModel: viewModel, BasicViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().Tags,
WindowName: "branches", WindowName: "branches",
Key: TAGS_CONTEXT_KEY, Key: TAGS_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,
@ -63,3 +58,9 @@ func (self *TagsContext) GetSelectedRef() types.Ref {
} }
return tag return tag
} }
func (self *TagsContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()
return []string{itemId}
}

View File

@ -1,9 +1,10 @@
package context package context
import ( import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -14,33 +15,30 @@ type WorkingTreeContext struct {
var _ types.IListContext = (*WorkingTreeContext)(nil) var _ types.IListContext = (*WorkingTreeContext)(nil)
func NewWorkingTreeContext( func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
getModel func() []*models.File, viewModel := filetree.NewFileTreeViewModel(
view *gocui.View, func() []*models.File { return c.Model().Files },
getDisplayStrings func(startIdx int, length int) [][]string, c.Log,
c.UserConfig.Gui.ShowFileTree,
)
onFocus func(types.OnFocusOpts) error, getDisplayStrings := func(startIdx int, length int) [][]string {
onRenderToMain func() error, lines := presentation.RenderFileTree(viewModel, c.Modes().Diffing.Ref, c.Model().Submodules)
onFocusLost func(opts types.OnFocusLostOpts) error, return slices.Map(lines, func(line string) []string {
return []string{line}
c *types.HelperCommon, })
) *WorkingTreeContext { }
viewModel := filetree.NewFileTreeViewModel(getModel, c.Log, c.UserConfig.Gui.ShowFileTree)
return &WorkingTreeContext{ return &WorkingTreeContext{
FileTreeViewModel: viewModel, FileTreeViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view, View: c.Views().Files,
WindowName: "files", WindowName: "files",
Key: FILES_CONTEXT_KEY, Key: FILES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ })),
OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}),
list: viewModel, list: viewModel,
getDisplayStrings: getDisplayStrings, getDisplayStrings: getDisplayStrings,
c: c, c: c,

View File

@ -6,273 +6,11 @@ import (
) )
func (gui *Gui) contextTree() *context.ContextTree { func (gui *Gui) contextTree() *context.ContextTree {
return &context.ContextTree{ contextCommon := &context.ContextCommon{
Global: context.NewSimpleContext( IGuiCommon: gui.c.IGuiCommon,
context.NewBaseContext(context.NewBaseContextOpts{ Common: gui.c.Common,
Kind: types.GLOBAL_CONTEXT,
View: nil, // TODO: see if this breaks anything
WindowName: "",
Key: context.GLOBAL_CONTEXT_KEY,
Focusable: false,
HasUncontrolledBounds: true, // setting to true because the global context doesn't even have a view
}),
context.ContextCallbackOpts{
OnRenderToMain: gui.statusRenderToMain,
},
),
Status: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.SIDE_CONTEXT,
View: gui.Views.Status,
WindowName: "status",
Key: context.STATUS_CONTEXT_KEY,
Focusable: true,
}),
context.ContextCallbackOpts{
OnRenderToMain: gui.statusRenderToMain,
},
),
Snake: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.SIDE_CONTEXT,
View: gui.Views.Snake,
WindowName: "files",
Key: context.SNAKE_CONTEXT_KEY,
Focusable: true,
}),
context.ContextCallbackOpts{
OnFocus: func(opts types.OnFocusOpts) error {
gui.startSnake()
return nil
},
OnFocusLost: func(opts types.OnFocusLostOpts) error {
gui.snakeGame.Exit()
gui.moveToTopOfWindow(gui.State.Contexts.Submodules)
return nil
},
},
),
Files: gui.filesListContext(),
Submodules: gui.submodulesListContext(),
Menu: gui.menuListContext(),
Remotes: gui.remotesListContext(),
RemoteBranches: gui.remoteBranchesListContext(),
LocalCommits: gui.branchCommitsListContext(),
CommitFiles: gui.commitFilesListContext(),
ReflogCommits: gui.reflogCommitsListContext(),
SubCommits: gui.subCommitsListContext(),
Branches: gui.branchesListContext(),
Tags: gui.tagsListContext(),
Stash: gui.stashListContext(),
Suggestions: gui.suggestionsListContext(),
Normal: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.MAIN_CONTEXT,
View: gui.Views.Main,
WindowName: "main",
Key: context.NORMAL_MAIN_CONTEXT_KEY,
Focusable: false,
}),
context.ContextCallbackOpts{
OnFocus: func(opts types.OnFocusOpts) error {
return nil // TODO: should we do something here? We should allow for scrolling the panel
},
},
),
NormalSecondary: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.MAIN_CONTEXT,
View: gui.Views.Secondary,
WindowName: "secondary",
Key: context.NORMAL_SECONDARY_CONTEXT_KEY,
Focusable: false,
}),
context.ContextCallbackOpts{},
),
Staging: context.NewPatchExplorerContext(
gui.Views.Staging,
"main",
context.STAGING_MAIN_CONTEXT_KEY,
func(opts types.OnFocusOpts) error {
gui.Views.Staging.Wrap = false
gui.Views.StagingSecondary.Wrap = false
return gui.refreshStagingPanel(opts)
},
func(opts types.OnFocusLostOpts) error {
gui.State.Contexts.Staging.SetState(nil)
if opts.NewContextKey != context.STAGING_SECONDARY_CONTEXT_KEY {
gui.Views.Staging.Wrap = true
gui.Views.StagingSecondary.Wrap = true
_ = gui.State.Contexts.Staging.Render(false)
_ = gui.State.Contexts.StagingSecondary.Render(false)
}
return nil
},
func() []int { return nil },
gui.c,
),
StagingSecondary: context.NewPatchExplorerContext(
gui.Views.StagingSecondary,
"secondary",
context.STAGING_SECONDARY_CONTEXT_KEY,
func(opts types.OnFocusOpts) error {
gui.Views.Staging.Wrap = false
gui.Views.StagingSecondary.Wrap = false
return gui.refreshStagingPanel(opts)
},
func(opts types.OnFocusLostOpts) error {
gui.State.Contexts.StagingSecondary.SetState(nil)
if opts.NewContextKey != context.STAGING_MAIN_CONTEXT_KEY {
gui.Views.Staging.Wrap = true
gui.Views.StagingSecondary.Wrap = true
_ = gui.State.Contexts.Staging.Render(false)
_ = gui.State.Contexts.StagingSecondary.Render(false)
}
return nil
},
func() []int { return nil },
gui.c,
),
CustomPatchBuilder: context.NewPatchExplorerContext(
gui.Views.PatchBuilding,
"main",
context.PATCH_BUILDING_MAIN_CONTEXT_KEY,
func(opts types.OnFocusOpts) error {
// no need to change wrap on the secondary view because it can't be interacted with
gui.Views.PatchBuilding.Wrap = false
return gui.refreshPatchBuildingPanel(opts)
},
func(opts types.OnFocusLostOpts) error {
gui.Views.PatchBuilding.Wrap = true
if gui.git.Patch.PatchBuilder.IsEmpty() {
gui.git.Patch.PatchBuilder.Reset()
}
return nil
},
func() []int {
filename := gui.State.Contexts.CommitFiles.GetSelectedPath()
includedLineIndices, err := gui.git.Patch.PatchBuilder.GetFileIncLineIndices(filename)
if err != nil {
gui.Log.Error(err)
return nil
}
return includedLineIndices
},
gui.c,
),
CustomPatchBuilderSecondary: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.MAIN_CONTEXT,
View: gui.Views.PatchBuildingSecondary,
WindowName: "secondary",
Key: context.PATCH_BUILDING_SECONDARY_CONTEXT_KEY,
Focusable: false,
}),
context.ContextCallbackOpts{},
),
MergeConflicts: context.NewMergeConflictsContext(
gui.Views.MergeConflicts,
context.ContextCallbackOpts{
OnFocus: OnFocusWrapper(func() error {
gui.Views.MergeConflicts.Wrap = false
return gui.refreshMergePanel(true)
}),
OnFocusLost: func(opts types.OnFocusLostOpts) error {
gui.State.Contexts.MergeConflicts.SetUserScrolling(false)
gui.State.Contexts.MergeConflicts.GetState().ResetConflictSelection()
gui.Views.MergeConflicts.Wrap = true
return nil
},
},
gui.c,
func() map[string]string {
// wrapping in a function because contexts are initialized before helpers
return gui.helpers.MergeConflicts.GetMergingOptions()
},
),
Confirmation: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.TEMPORARY_POPUP,
View: gui.Views.Confirmation,
WindowName: "confirmation",
Key: context.CONFIRMATION_CONTEXT_KEY,
Focusable: true,
HasUncontrolledBounds: true,
}),
context.ContextCallbackOpts{
OnFocus: OnFocusWrapper(gui.handleAskFocused),
OnFocusLost: func(types.OnFocusLostOpts) error {
gui.deactivateConfirmationPrompt()
return nil
},
},
),
CommitMessage: context.NewCommitMessageContext(
gui.Views.CommitMessage,
context.ContextCallbackOpts{
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
},
),
CommitDescription: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.PERSISTENT_POPUP,
View: gui.Views.CommitDescription,
WindowName: "commitDescription",
Key: context.COMMIT_DESCRIPTION_CONTEXT_KEY,
Focusable: true,
HasUncontrolledBounds: true,
}),
context.ContextCallbackOpts{
OnFocus: func(opts types.OnFocusOpts) error {
_, err := gui.g.SetViewBeneath("commitDescription", "commitMessage", 10)
if err != nil {
return err
}
return nil
},
},
),
Search: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.PERSISTENT_POPUP,
View: gui.Views.Search,
WindowName: "search",
Key: context.SEARCH_CONTEXT_KEY,
Focusable: true,
}),
context.ContextCallbackOpts{},
),
CommandLog: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.EXTRAS_CONTEXT,
View: gui.Views.Extras,
WindowName: "extras",
Key: context.COMMAND_LOG_CONTEXT_KEY,
Focusable: true,
}),
context.ContextCallbackOpts{
OnFocusLost: func(opts types.OnFocusLostOpts) error {
gui.Views.Extras.Autoscroll = true
return nil
},
},
),
Options: context.NewDisplayContext(context.OPTIONS_CONTEXT_KEY, gui.Views.Options, "options"),
AppStatus: context.NewDisplayContext(context.APP_STATUS_CONTEXT_KEY, gui.Views.AppStatus, "appStatus"),
SearchPrefix: context.NewDisplayContext(context.SEARCH_PREFIX_CONTEXT_KEY, gui.Views.SearchPrefix, "searchPrefix"),
Information: context.NewDisplayContext(context.INFORMATION_CONTEXT_KEY, gui.Views.Information, "information"),
Limit: context.NewDisplayContext(context.LIMIT_CONTEXT_KEY, gui.Views.Limit, "limit"),
} }
return context.NewContextTree(contextCommon)
} }
// using this wrapper for when an onFocus function doesn't care about any potential // using this wrapper for when an onFocus function doesn't care about any potential
@ -283,10 +21,10 @@ func OnFocusWrapper(f func() error) func(opts types.OnFocusOpts) error {
} }
} }
func (gui *Gui) getPatchExplorerContexts() []types.IPatchExplorerContext { func (gui *Gui) defaultSideContext() types.Context {
return []types.IPatchExplorerContext{ if gui.State.Modes.Filtering.Active() {
gui.State.Contexts.Staging, return gui.State.Contexts.LocalCommits
gui.State.Contexts.StagingSecondary, } else {
gui.State.Contexts.CustomPatchBuilder, return gui.State.Contexts.Files
} }
} }

View File

@ -7,24 +7,22 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/controllers" "github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
"github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands" "github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands"
"github.com/jesseduffield/lazygit/pkg/snake" "github.com/jesseduffield/lazygit/pkg/gui/status"
"github.com/jesseduffield/lazygit/pkg/gui/types"
) )
func (gui *Gui) resetControllers() { func (gui *Gui) Helpers() *helpers.Helpers {
helperCommon := gui.c return gui.helpers
osCommand := gui.os }
model := gui.State.Model
refsHelper := helpers.NewRefsHelper( func (gui *Gui) resetHelpersAndControllers() {
helperCommon, helperCommon := gui.c
gui.git, refsHelper := helpers.NewRefsHelper(helperCommon)
gui.State.Contexts,
model, rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper)
) suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon)
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, gui.State.Contexts, gui.git, refsHelper)
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon, model, gui.refreshSuggestions)
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage }) setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription }) setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
getCommitSummary := func() string { getCommitSummary := func() string {
@ -35,66 +33,86 @@ func (gui *Gui) resetControllers() {
return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetContent()) return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetContent())
} }
commitsHelper := helpers.NewCommitsHelper(helperCommon, commitsHelper := helpers.NewCommitsHelper(helperCommon,
gui.State.Model,
gui.State.Contexts,
getCommitSummary, getCommitSummary,
setCommitSummary, setCommitSummary,
getCommitDescription, getCommitDescription,
setCommitDescription, setCommitDescription,
gui.RenderCommitLength,
) )
gpgHelper := helpers.NewGpgHelper(helperCommon, gui.os, gui.git)
gpgHelper := helpers.NewGpgHelper(helperCommon)
viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts)
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon)
stagingHelper := helpers.NewStagingHelper(helperCommon)
mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon)
refreshHelper := helpers.NewRefreshHelper(helperCommon, refsHelper, rebaseHelper, patchBuildingHelper, stagingHelper, mergeConflictsHelper, gui.fileWatcher)
diffHelper := helpers.NewDiffHelper(helperCommon)
cherryPickHelper := helpers.NewCherryPickHelper(
helperCommon,
rebaseHelper,
)
bisectHelper := helpers.NewBisectHelper(helperCommon)
windowHelper := helpers.NewWindowHelper(helperCommon, viewHelper)
modeHelper := helpers.NewModeHelper(
helperCommon,
diffHelper,
patchBuildingHelper,
cherryPickHelper,
rebaseHelper,
bisectHelper,
)
appStatusHelper := helpers.NewAppStatusHelper(
helperCommon,
func() *status.StatusManager { return gui.statusManager },
)
gui.helpers = &helpers.Helpers{ gui.helpers = &helpers.Helpers{
Refs: refsHelper, Refs: refsHelper,
Host: helpers.NewHostHelper(helperCommon, gui.git), Host: helpers.NewHostHelper(helperCommon),
PatchBuilding: helpers.NewPatchBuildingHelper(helperCommon, gui.git, gui.State.Contexts), PatchBuilding: patchBuildingHelper,
Bisect: helpers.NewBisectHelper(helperCommon, gui.git), Staging: stagingHelper,
Suggestions: suggestionsHelper, Bisect: bisectHelper,
Files: helpers.NewFilesHelper(helperCommon, gui.git, osCommand), Suggestions: suggestionsHelper,
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, gui.git, gui.State.Contexts, refsHelper, model, setCommitSummary, commitsHelper, gpgHelper), Files: helpers.NewFilesHelper(helperCommon),
Tags: helpers.NewTagsHelper(helperCommon, gui.git), WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper),
GPG: gpgHelper, Tags: helpers.NewTagsHelper(helperCommon),
MergeAndRebase: rebaseHelper, GPG: helpers.NewGpgHelper(helperCommon),
MergeConflicts: helpers.NewMergeConflictsHelper(helperCommon, gui.State.Contexts, gui.git), MergeAndRebase: rebaseHelper,
CherryPick: helpers.NewCherryPickHelper( MergeConflicts: mergeConflictsHelper,
helperCommon, CherryPick: cherryPickHelper,
gui.git, Upstream: helpers.NewUpstreamHelper(helperCommon, suggestionsHelper.GetRemoteBranchesSuggestionsFunc),
gui.State.Contexts, AmendHelper: helpers.NewAmendHelper(helperCommon, gpgHelper),
func() *cherrypicking.CherryPicking { return gui.State.Modes.CherryPicking }, Commits: commitsHelper,
rebaseHelper, Snake: helpers.NewSnakeHelper(helperCommon),
Diff: diffHelper,
Repos: helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo),
RecordDirectory: recordDirectoryHelper,
Update: helpers.NewUpdateHelper(helperCommon, gui.Updater),
Window: windowHelper,
View: viewHelper,
Refresh: refreshHelper,
Confirmation: helpers.NewConfirmationHelper(helperCommon),
Mode: modeHelper,
AppStatus: appStatusHelper,
WindowArrangement: helpers.NewWindowArrangementHelper(
gui.c,
windowHelper,
modeHelper,
appStatusHelper,
), ),
Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc),
AmendHelper: helpers.NewAmendHelper(helperCommon, gui.git, gpgHelper),
Commits: commitsHelper,
} }
gui.CustomCommandsClient = custom_commands.NewClient( gui.CustomCommandsClient = custom_commands.NewClient(
helperCommon, helperCommon,
gui.os,
gui.git,
gui.State.Contexts,
gui.helpers, gui.helpers,
) )
common := controllers.NewControllerCommon( common := controllers.NewControllerCommon(helperCommon, gui)
helperCommon,
osCommand,
gui.git,
gui.helpers,
model,
gui.State.Contexts,
gui.State.Modes,
&gui.Mutexes,
)
syncController := controllers.NewSyncController( syncController := controllers.NewSyncController(
common, common,
) )
submodulesController := controllers.NewSubmodulesController( submodulesController := controllers.NewSubmodulesController(common)
common,
gui.enterSubmodule,
)
bisectController := controllers.NewBisectController(common) bisectController := controllers.NewBisectController(common)
@ -113,8 +131,6 @@ func (gui *Gui) resetControllers() {
tagsController := controllers.NewTagsController(common) tagsController := controllers.NewTagsController(common)
filesController := controllers.NewFilesController( filesController := controllers.NewFilesController(
common, common,
gui.enterSubmodule,
setCommitSummary,
) )
mergeConflictsController := controllers.NewMergeConflictsController(common) mergeConflictsController := controllers.NewMergeConflictsController(common)
remotesController := controllers.NewRemotesController( remotesController := controllers.NewRemotesController(
@ -135,7 +151,34 @@ func (gui *Gui) resetControllers() {
stagingController := controllers.NewStagingController(common, gui.State.Contexts.Staging, gui.State.Contexts.StagingSecondary, false) stagingController := controllers.NewStagingController(common, gui.State.Contexts.Staging, gui.State.Contexts.StagingSecondary, false)
stagingSecondaryController := controllers.NewStagingController(common, gui.State.Contexts.StagingSecondary, gui.State.Contexts.Staging, true) stagingSecondaryController := controllers.NewStagingController(common, gui.State.Contexts.StagingSecondary, gui.State.Contexts.Staging, true)
patchBuildingController := controllers.NewPatchBuildingController(common) patchBuildingController := controllers.NewPatchBuildingController(common)
snakeController := controllers.NewSnakeController(common, func() *snake.Game { return gui.snakeGame }) snakeController := controllers.NewSnakeController(common)
reflogCommitsController := controllers.NewReflogCommitsController(common)
subCommitsController := controllers.NewSubCommitsController(common)
statusController := controllers.NewStatusController(common)
commandLogController := controllers.NewCommandLogController(common)
confirmationController := controllers.NewConfirmationController(common)
suggestionsController := controllers.NewSuggestionsController(common)
jumpToSideWindowController := controllers.NewJumpToSideWindowController(common)
sideWindowControllerFactory := controllers.NewSideWindowControllerFactory(common)
// allow for navigating between side window contexts
for _, context := range []types.Context{
gui.State.Contexts.Status,
gui.State.Contexts.Remotes,
gui.State.Contexts.Tags,
gui.State.Contexts.Branches,
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Files,
gui.State.Contexts.Submodules,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.LocalCommits,
gui.State.Contexts.CommitFiles,
gui.State.Contexts.SubCommits,
gui.State.Contexts.Stash,
} {
controllers.AttachControllers(context, sideWindowControllerFactory.Create(context))
}
setSubCommits := func(commits []*models.Commit) { setSubCommits := func(commits []*models.Commit) {
gui.Mutexes.SubCommitsMutex.Lock() gui.Mutexes.SubCommitsMutex.Lock()
@ -161,7 +204,7 @@ func (gui *Gui) resetControllers() {
gui.State.Contexts.Stash, gui.State.Contexts.Stash,
} { } {
controllers.AttachControllers(context, controllers.NewSwitchToDiffFilesController( controllers.AttachControllers(context, controllers.NewSwitchToDiffFilesController(
common, gui.SwitchToCommitFilesContext, context, common, context, gui.State.Contexts.CommitFiles,
)) ))
} }
@ -173,6 +216,14 @@ func (gui *Gui) resetControllers() {
controllers.AttachControllers(context, controllers.NewBasicCommitsController(common, context)) controllers.AttachControllers(context, controllers.NewBasicCommitsController(common, context))
} }
controllers.AttachControllers(gui.State.Contexts.ReflogCommits,
reflogCommitsController,
)
controllers.AttachControllers(gui.State.Contexts.SubCommits,
subCommitsController,
)
// TODO: add scroll controllers for main panels (need to bring some more functionality across for that e.g. reading more from the currently displayed git command) // TODO: add scroll controllers for main panels (need to bring some more functionality across for that e.g. reading more from the currently displayed git command)
controllers.AttachControllers(gui.State.Contexts.Staging, controllers.AttachControllers(gui.State.Contexts.Staging,
stagingController, stagingController,
@ -256,11 +307,28 @@ func (gui *Gui) resetControllers() {
remoteBranchesController, remoteBranchesController,
) )
controllers.AttachControllers(gui.State.Contexts.Status,
statusController,
)
controllers.AttachControllers(gui.State.Contexts.CommandLog,
commandLogController,
)
controllers.AttachControllers(gui.State.Contexts.Confirmation,
confirmationController,
)
controllers.AttachControllers(gui.State.Contexts.Suggestions,
suggestionsController,
)
controllers.AttachControllers(gui.State.Contexts.Global, controllers.AttachControllers(gui.State.Contexts.Global,
syncController, syncController,
undoController, undoController,
globalController, globalController,
contextLinesController, contextLinesController,
jumpToSideWindowController,
) )
controllers.AttachControllers(gui.State.Contexts.Snake, controllers.AttachControllers(gui.State.Contexts.Snake,
@ -268,8 +336,19 @@ func (gui *Gui) resetControllers() {
) )
// this must come last so that we've got our click handlers defined against the context // this must come last so that we've got our click handlers defined against the context
listControllerFactory := controllers.NewListControllerFactory(gui.c) listControllerFactory := controllers.NewListControllerFactory(common)
for _, context := range gui.getListContexts() { for _, context := range gui.c.Context().AllList() {
controllers.AttachControllers(context, listControllerFactory.Create(context)) controllers.AttachControllers(context, listControllerFactory.Create(context))
} }
} }
func (gui *Gui) getCommitMessageSetTextareaTextFn(getView func() *gocui.View) func(string) {
return func(text string) {
// using a getView function so that we don't need to worry about when the view is created
view := getView()
view.ClearTextArea()
view.TextArea.TypeString(text)
gui.helpers.Confirmation.ResizeCommitMessagePanels()
view.RenderTextArea()
}
}

View File

@ -7,5 +7,8 @@ func AttachControllers(context types.Context, controllers ...types.IController)
context.AddKeybindingsFn(controller.GetKeybindings) context.AddKeybindingsFn(controller.GetKeybindings)
context.AddMouseKeybindingsFn(controller.GetMouseKeybindings) context.AddMouseKeybindingsFn(controller.GetMouseKeybindings)
context.AddOnClickFn(controller.GetOnClick()) context.AddOnClickFn(controller.GetOnClick())
context.AddOnRenderToMainFn(controller.GetOnRenderToMain())
context.AddOnFocusFn(controller.GetOnFocus())
context.AddOnFocusLostFn(controller.GetOnFocusLost())
} }
} }

View File

@ -18,3 +18,15 @@ func (self *baseController) GetMouseKeybindings(opts types.KeybindingsOpts) []*g
func (self *baseController) GetOnClick() func() error { func (self *baseController) GetOnClick() func() error {
return nil return nil
} }
func (self *baseController) GetOnRenderToMain() func() error {
return nil
}
func (self *baseController) GetOnFocus() func(types.OnFocusOpts) error {
return nil
}
func (self *baseController) GetOnFocusLost() func(types.OnFocusLostOpts) error {
return nil
}

View File

@ -20,15 +20,15 @@ type ContainsCommits interface {
type BasicCommitsController struct { type BasicCommitsController struct {
baseController baseController
*controllerCommon c *ControllerCommon
context ContainsCommits context ContainsCommits
} }
func NewBasicCommitsController(controllerCommon *controllerCommon, context ContainsCommits) *BasicCommitsController { func NewBasicCommitsController(controllerCommon *ControllerCommon, context ContainsCommits) *BasicCommitsController {
return &BasicCommitsController{ return &BasicCommitsController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: controllerCommon, c: controllerCommon,
context: context, context: context,
} }
} }
@ -73,7 +73,7 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.ResetCherryPick), Key: opts.GetKey(opts.Config.Commits.ResetCherryPick),
Handler: self.helpers.CherryPick.Reset, Handler: self.c.Helpers().CherryPick.Reset,
Description: self.c.Tr.LcResetCherryPick, Description: self.c.Tr.LcResetCherryPick,
}, },
} }
@ -141,7 +141,7 @@ func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) e
func (self *BasicCommitsController) copyCommitSHAToClipboard(commit *models.Commit) error { func (self *BasicCommitsController) copyCommitSHAToClipboard(commit *models.Commit) error {
self.c.LogAction(self.c.Tr.Actions.CopyCommitSHAToClipboard) self.c.LogAction(self.c.Tr.Actions.CopyCommitSHAToClipboard)
if err := self.os.CopyToClipboard(commit.Sha); err != nil { if err := self.c.OS().CopyToClipboard(commit.Sha); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -150,13 +150,13 @@ func (self *BasicCommitsController) copyCommitSHAToClipboard(commit *models.Comm
} }
func (self *BasicCommitsController) copyCommitURLToClipboard(commit *models.Commit) error { func (self *BasicCommitsController) copyCommitURLToClipboard(commit *models.Commit) error {
url, err := self.helpers.Host.GetCommitURL(commit.Sha) url, err := self.c.Helpers().Host.GetCommitURL(commit.Sha)
if err != nil { if err != nil {
return err return err
} }
self.c.LogAction(self.c.Tr.Actions.CopyCommitURLToClipboard) self.c.LogAction(self.c.Tr.Actions.CopyCommitURLToClipboard)
if err := self.os.CopyToClipboard(url); err != nil { if err := self.c.OS().CopyToClipboard(url); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -165,13 +165,13 @@ func (self *BasicCommitsController) copyCommitURLToClipboard(commit *models.Comm
} }
func (self *BasicCommitsController) copyCommitDiffToClipboard(commit *models.Commit) error { func (self *BasicCommitsController) copyCommitDiffToClipboard(commit *models.Commit) error {
diff, err := self.git.Commit.GetCommitDiff(commit.Sha) diff, err := self.c.Git().Commit.GetCommitDiff(commit.Sha)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
self.c.LogAction(self.c.Tr.Actions.CopyCommitDiffToClipboard) self.c.LogAction(self.c.Tr.Actions.CopyCommitDiffToClipboard)
if err := self.os.CopyToClipboard(diff); err != nil { if err := self.c.OS().CopyToClipboard(diff); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -180,7 +180,7 @@ func (self *BasicCommitsController) copyCommitDiffToClipboard(commit *models.Com
} }
func (self *BasicCommitsController) copyAuthorToClipboard(commit *models.Commit) error { func (self *BasicCommitsController) copyAuthorToClipboard(commit *models.Commit) error {
author, err := self.git.Commit.GetCommitAuthor(commit.Sha) author, err := self.c.Git().Commit.GetCommitAuthor(commit.Sha)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -188,7 +188,7 @@ func (self *BasicCommitsController) copyAuthorToClipboard(commit *models.Commit)
formattedAuthor := fmt.Sprintf("%s <%s>", author.Name, author.Email) formattedAuthor := fmt.Sprintf("%s <%s>", author.Name, author.Email)
self.c.LogAction(self.c.Tr.Actions.CopyCommitAuthorToClipboard) self.c.LogAction(self.c.Tr.Actions.CopyCommitAuthorToClipboard)
if err := self.os.CopyToClipboard(formattedAuthor); err != nil { if err := self.c.OS().CopyToClipboard(formattedAuthor); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -197,13 +197,13 @@ func (self *BasicCommitsController) copyAuthorToClipboard(commit *models.Commit)
} }
func (self *BasicCommitsController) copyCommitMessageToClipboard(commit *models.Commit) error { func (self *BasicCommitsController) copyCommitMessageToClipboard(commit *models.Commit) error {
message, err := self.git.Commit.GetCommitMessage(commit.Sha) message, err := self.c.Git().Commit.GetCommitMessage(commit.Sha)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
self.c.LogAction(self.c.Tr.Actions.CopyCommitMessageToClipboard) self.c.LogAction(self.c.Tr.Actions.CopyCommitMessageToClipboard)
if err := self.os.CopyToClipboard(message); err != nil { if err := self.c.OS().CopyToClipboard(message); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -212,13 +212,13 @@ func (self *BasicCommitsController) copyCommitMessageToClipboard(commit *models.
} }
func (self *BasicCommitsController) openInBrowser(commit *models.Commit) error { func (self *BasicCommitsController) openInBrowser(commit *models.Commit) error {
url, err := self.helpers.Host.GetCommitURL(commit.Sha) url, err := self.c.Helpers().Host.GetCommitURL(commit.Sha)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
self.c.LogAction(self.c.Tr.Actions.OpenCommitInBrowser) self.c.LogAction(self.c.Tr.Actions.OpenCommitInBrowser)
if err := self.os.OpenLink(url); err != nil { if err := self.c.OS().OpenLink(url); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -226,11 +226,11 @@ func (self *BasicCommitsController) openInBrowser(commit *models.Commit) error {
} }
func (self *BasicCommitsController) newBranch(commit *models.Commit) error { func (self *BasicCommitsController) newBranch(commit *models.Commit) error {
return self.helpers.Refs.NewBranch(commit.RefName(), commit.Description(), "") return self.c.Helpers().Refs.NewBranch(commit.RefName(), commit.Description(), "")
} }
func (self *BasicCommitsController) createResetMenu(commit *models.Commit) error { func (self *BasicCommitsController) createResetMenu(commit *models.Commit) error {
return self.helpers.Refs.CreateGitResetMenu(commit.Sha) return self.c.Helpers().Refs.CreateGitResetMenu(commit.Sha)
} }
func (self *BasicCommitsController) checkout(commit *models.Commit) error { func (self *BasicCommitsController) checkout(commit *models.Commit) error {
@ -239,15 +239,15 @@ func (self *BasicCommitsController) checkout(commit *models.Commit) error {
Prompt: self.c.Tr.SureCheckoutThisCommit, Prompt: self.c.Tr.SureCheckoutThisCommit,
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.CheckoutCommit) self.c.LogAction(self.c.Tr.Actions.CheckoutCommit)
return self.helpers.Refs.CheckoutRef(commit.Sha, types.CheckoutRefOptions{}) return self.c.Helpers().Refs.CheckoutRef(commit.Sha, types.CheckoutRefOptions{})
}, },
}) })
} }
func (self *BasicCommitsController) copy(commit *models.Commit) error { func (self *BasicCommitsController) copy(commit *models.Commit) error {
return self.helpers.CherryPick.Copy(commit, self.context.GetCommits(), self.context) return self.c.Helpers().CherryPick.Copy(commit, self.context.GetCommits(), self.context)
} }
func (self *BasicCommitsController) copyRange(*models.Commit) error { func (self *BasicCommitsController) copyRange(*models.Commit) error {
return self.helpers.CherryPick.CopyRange(self.context.GetSelectedLineIdx(), self.context.GetCommits(), self.context) return self.c.Helpers().CherryPick.CopyRange(self.context.GetSelectedLineIdx(), self.context.GetCommits(), self.context)
} }

View File

@ -12,17 +12,17 @@ import (
type BisectController struct { type BisectController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
var _ types.IController = &BisectController{} var _ types.IController = &BisectController{}
func NewBisectController( func NewBisectController(
common *controllerCommon, common *ControllerCommon,
) *BisectController { ) *BisectController {
return &BisectController{ return &BisectController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
@ -42,7 +42,7 @@ func (self *BisectController) GetKeybindings(opts types.KeybindingsOpts) []*type
func (self *BisectController) openMenu(commit *models.Commit) error { func (self *BisectController) openMenu(commit *models.Commit) error {
// no shame in getting this directly rather than using the cached value // no shame in getting this directly rather than using the cached value
// given how cheap it is to obtain // given how cheap it is to obtain
info := self.git.Bisect.GetInfo() info := self.c.Git().Bisect.GetInfo()
if info.Started() { if info.Started() {
return self.openMidBisectMenu(info, commit) return self.openMidBisectMenu(info, commit)
} else { } else {
@ -63,14 +63,14 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c
selectCurrentAfter := info.GetCurrentSha() == "" || info.GetCurrentSha() == commit.Sha selectCurrentAfter := info.GetCurrentSha() == "" || info.GetCurrentSha() == commit.Sha
// we need to wait to reselect if our bisect commits aren't ancestors of our 'start' // we need to wait to reselect if our bisect commits aren't ancestors of our 'start'
// ref, because we'll be reloading our commits in that case. // ref, because we'll be reloading our commits in that case.
waitToReselect := selectCurrentAfter && !self.git.Bisect.ReachableFromStart(info) waitToReselect := selectCurrentAfter && !self.c.Git().Bisect.ReachableFromStart(info)
menuItems := []*types.MenuItem{ menuItems := []*types.MenuItem{
{ {
Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.NewTerm()), Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.NewTerm()),
OnPress: func() error { OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.BisectMark) self.c.LogAction(self.c.Tr.Actions.BisectMark)
if err := self.git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil { if err := self.c.Git().Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -82,7 +82,7 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c
Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.OldTerm()), Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.OldTerm()),
OnPress: func() error { OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.BisectMark) self.c.LogAction(self.c.Tr.Actions.BisectMark)
if err := self.git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil { if err := self.c.Git().Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -94,7 +94,7 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c
Label: fmt.Sprintf(self.c.Tr.Bisect.Skip, commit.ShortSha()), Label: fmt.Sprintf(self.c.Tr.Bisect.Skip, commit.ShortSha()),
OnPress: func() error { OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.BisectSkip) self.c.LogAction(self.c.Tr.Actions.BisectSkip)
if err := self.git.Bisect.Skip(commit.Sha); err != nil { if err := self.c.Git().Bisect.Skip(commit.Sha); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -105,7 +105,7 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c
{ {
Label: self.c.Tr.Bisect.ResetOption, Label: self.c.Tr.Bisect.ResetOption,
OnPress: func() error { OnPress: func() error {
return self.helpers.Bisect.Reset() return self.c.Helpers().Bisect.Reset()
}, },
Key: 'r', Key: 'r',
}, },
@ -125,15 +125,15 @@ func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo,
Label: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.NewTerm()), Label: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.NewTerm()),
OnPress: func() error { OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.StartBisect) self.c.LogAction(self.c.Tr.Actions.StartBisect)
if err := self.git.Bisect.Start(); err != nil { if err := self.c.Git().Bisect.Start(); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
if err := self.git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil { if err := self.c.Git().Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.helpers.Bisect.PostBisectCommandRefresh() return self.c.Helpers().Bisect.PostBisectCommandRefresh()
}, },
Key: 'b', Key: 'b',
}, },
@ -141,15 +141,15 @@ func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo,
Label: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()), Label: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()),
OnPress: func() error { OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.StartBisect) self.c.LogAction(self.c.Tr.Actions.StartBisect)
if err := self.git.Bisect.Start(); err != nil { if err := self.c.Git().Bisect.Start(); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
if err := self.git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil { if err := self.c.Git().Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.helpers.Bisect.PostBisectCommandRefresh() return self.c.Helpers().Bisect.PostBisectCommandRefresh()
}, },
Key: 'g', Key: 'g',
}, },
@ -163,7 +163,7 @@ func (self *BisectController) showBisectCompleteMessage(candidateShas []string)
prompt = self.c.Tr.Bisect.CompletePromptIndeterminate prompt = self.c.Tr.Bisect.CompletePromptIndeterminate
} }
formattedCommits, err := self.git.Commit.GetCommitsOneline(candidateShas) formattedCommits, err := self.c.Git().Commit.GetCommitsOneline(candidateShas)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -173,17 +173,17 @@ func (self *BisectController) showBisectCompleteMessage(candidateShas []string)
Prompt: fmt.Sprintf(prompt, strings.TrimSpace(formattedCommits)), Prompt: fmt.Sprintf(prompt, strings.TrimSpace(formattedCommits)),
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.ResetBisect) self.c.LogAction(self.c.Tr.Actions.ResetBisect)
if err := self.git.Bisect.Reset(); err != nil { if err := self.c.Git().Bisect.Reset(); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.helpers.Bisect.PostBisectCommandRefresh() return self.c.Helpers().Bisect.PostBisectCommandRefresh()
}, },
}) })
} }
func (self *BisectController) afterMark(selectCurrent bool, waitToReselect bool) error { func (self *BisectController) afterMark(selectCurrent bool, waitToReselect bool) error {
done, candidateShas, err := self.git.Bisect.IsDone() done, candidateShas, err := self.c.Git().Bisect.IsDone()
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -211,15 +211,15 @@ func (self *BisectController) afterBisectMarkRefresh(selectCurrent bool, waitToR
} else { } else {
selectFn() selectFn()
return self.helpers.Bisect.PostBisectCommandRefresh() return self.c.Helpers().Bisect.PostBisectCommandRefresh()
} }
} }
func (self *BisectController) selectCurrentBisectCommit() { func (self *BisectController) selectCurrentBisectCommit() {
info := self.git.Bisect.GetInfo() info := self.c.Git().Bisect.GetInfo()
if info.GetCurrentSha() != "" { if info.GetCurrentSha() != "" {
// find index of commit with that sha, move cursor to that. // find index of commit with that sha, move cursor to that.
for i, commit := range self.model.Commits { for i, commit := range self.c.Model().Commits {
if commit.Sha == info.GetCurrentSha() { if commit.Sha == info.GetCurrentSha() {
self.context().SetSelectedLineIdx(i) self.context().SetSelectedLineIdx(i)
_ = self.context().HandleFocus(types.OnFocusOpts{}) _ = self.context().HandleFocus(types.OnFocusOpts{})
@ -245,5 +245,5 @@ func (self *BisectController) Context() types.Context {
} }
func (self *BisectController) context() *context.LocalCommitsContext { func (self *BisectController) context() *context.LocalCommitsContext {
return self.contexts.LocalCommits return self.c.Contexts().LocalCommits
} }

View File

@ -14,17 +14,17 @@ import (
type BranchesController struct { type BranchesController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
var _ types.IController = &BranchesController{} var _ types.IController = &BranchesController{}
func NewBranchesController( func NewBranchesController(
common *controllerCommon, common *ControllerCommon,
) *BranchesController { ) *BranchesController {
return &BranchesController{ return &BranchesController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
@ -111,6 +111,30 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
} }
} }
func (self *BranchesController) GetOnRenderToMain() func() error {
return func() error {
return self.c.Helpers().Diff.WithDiffModeCheck(func() error {
var task types.UpdateTask
branch := self.context().GetSelected()
if branch == nil {
task = types.NewRenderStringTask(self.c.Tr.NoBranchesThisRepo)
} else {
cmdObj := self.c.Git().Branch.GetGraphCmdObj(branch.FullRefName())
task = types.NewRunPtyTask(cmdObj.GetCmd())
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.LogTitle,
Task: task,
},
})
})
}
}
func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error { func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error {
return self.c.Menu(types.CreateMenuOptions{ return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Actions.SetUnsetUpstream, Title: self.c.Tr.Actions.SetUnsetUpstream,
@ -118,7 +142,7 @@ func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error
{ {
LabelColumns: []string{self.c.Tr.LcUnsetUpstream}, LabelColumns: []string{self.c.Tr.LcUnsetUpstream},
OnPress: func() error { OnPress: func() error {
if err := self.git.Branch.UnsetUpstream(selectedBranch.Name); err != nil { if err := self.c.Git().Branch.UnsetUpstream(selectedBranch.Name); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
if err := self.c.Refresh(types.RefreshOptions{ if err := self.c.Refresh(types.RefreshOptions{
@ -137,13 +161,13 @@ func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error
{ {
LabelColumns: []string{self.c.Tr.LcSetUpstream}, LabelColumns: []string{self.c.Tr.LcSetUpstream},
OnPress: func() error { OnPress: func() error {
return self.helpers.Upstream.PromptForUpstreamWithoutInitialContent(selectedBranch, func(upstream string) error { return self.c.Helpers().Upstream.PromptForUpstreamWithoutInitialContent(selectedBranch, func(upstream string) error {
upstreamRemote, upstreamBranch, err := self.helpers.Upstream.ParseUpstream(upstream) upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
if err := self.git.Branch.SetUpstream(upstreamRemote, upstreamBranch, selectedBranch.Name); err != nil { if err := self.c.Git().Branch.SetUpstream(upstreamRemote, upstreamBranch, selectedBranch.Name); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
if err := self.c.Refresh(types.RefreshOptions{ if err := self.c.Refresh(types.RefreshOptions{
@ -169,16 +193,16 @@ func (self *BranchesController) Context() types.Context {
} }
func (self *BranchesController) context() *context.BranchesContext { func (self *BranchesController) context() *context.BranchesContext {
return self.contexts.Branches return self.c.Contexts().Branches
} }
func (self *BranchesController) press(selectedBranch *models.Branch) error { func (self *BranchesController) press(selectedBranch *models.Branch) error {
if selectedBranch == self.helpers.Refs.GetCheckedOutRef() { if selectedBranch == self.c.Helpers().Refs.GetCheckedOutRef() {
return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch) return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch)
} }
self.c.LogAction(self.c.Tr.Actions.CheckoutBranch) self.c.LogAction(self.c.Tr.Actions.CheckoutBranch)
return self.helpers.Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{}) return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{})
} }
func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error { func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error {
@ -186,7 +210,7 @@ func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.B
} }
func (self *BranchesController) handleCreatePullRequestMenu(selectedBranch *models.Branch) error { func (self *BranchesController) handleCreatePullRequestMenu(selectedBranch *models.Branch) error {
checkedOutBranch := self.helpers.Refs.GetCheckedOutRef() checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef()
return self.createPullRequestMenu(selectedBranch, checkedOutBranch) return self.createPullRequestMenu(selectedBranch, checkedOutBranch)
} }
@ -194,18 +218,18 @@ func (self *BranchesController) handleCreatePullRequestMenu(selectedBranch *mode
func (self *BranchesController) copyPullRequestURL() error { func (self *BranchesController) copyPullRequestURL() error {
branch := self.context().GetSelected() branch := self.context().GetSelected()
branchExistsOnRemote := self.git.Remote.CheckRemoteBranchExists(branch.Name) branchExistsOnRemote := self.c.Git().Remote.CheckRemoteBranchExists(branch.Name)
if !branchExistsOnRemote { if !branchExistsOnRemote {
return self.c.Error(errors.New(self.c.Tr.NoBranchOnRemote)) return self.c.Error(errors.New(self.c.Tr.NoBranchOnRemote))
} }
url, err := self.helpers.Host.GetPullRequestURL(branch.Name, "") url, err := self.c.Helpers().Host.GetPullRequestURL(branch.Name, "")
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
self.c.LogAction(self.c.Tr.Actions.CopyPullRequestURL) self.c.LogAction(self.c.Tr.Actions.CopyPullRequestURL)
if err := self.os.CopyToClipboard(url); err != nil { if err := self.c.OS().CopyToClipboard(url); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -224,7 +248,7 @@ func (self *BranchesController) forceCheckout() error {
Prompt: message, Prompt: message,
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.ForceCheckoutBranch) self.c.LogAction(self.c.Tr.Actions.ForceCheckoutBranch)
if err := self.git.Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); err != nil { if err := self.c.Git().Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); err != nil {
_ = self.c.Error(err) _ = self.c.Error(err)
} }
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
@ -235,10 +259,10 @@ func (self *BranchesController) forceCheckout() error {
func (self *BranchesController) checkoutByName() error { func (self *BranchesController) checkoutByName() error {
return self.c.Prompt(types.PromptOpts{ return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.BranchName + ":", Title: self.c.Tr.BranchName + ":",
FindSuggestionsFunc: self.helpers.Suggestions.GetRefsSuggestionsFunc(), FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRefsSuggestionsFunc(),
HandleConfirm: func(response string) error { HandleConfirm: func(response string) error {
self.c.LogAction("Checkout branch") self.c.LogAction("Checkout branch")
return self.helpers.Refs.CheckoutRef(response, types.CheckoutRefOptions{ return self.c.Helpers().Refs.CheckoutRef(response, types.CheckoutRefOptions{
OnRefNotFound: func(ref string) error { OnRefNotFound: func(ref string) error {
return self.c.Confirm(types.ConfirmOpts{ return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.BranchNotFoundTitle, Title: self.c.Tr.BranchNotFoundTitle,
@ -260,7 +284,7 @@ func (self *BranchesController) createNewBranchWithName(newBranchName string) er
return nil return nil
} }
if err := self.git.Branch.New(newBranchName, branch.FullRefName()); err != nil { if err := self.c.Git().Branch.New(newBranchName, branch.FullRefName()); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -269,7 +293,7 @@ func (self *BranchesController) createNewBranchWithName(newBranchName string) er
} }
func (self *BranchesController) delete(branch *models.Branch) error { func (self *BranchesController) delete(branch *models.Branch) error {
checkedOutBranch := self.helpers.Refs.GetCheckedOutRef() checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef()
if checkedOutBranch.Name == branch.Name { if checkedOutBranch.Name == branch.Name {
return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch) return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch)
} }
@ -296,7 +320,7 @@ func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, f
Prompt: message, Prompt: message,
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.DeleteBranch) self.c.LogAction(self.c.Tr.Actions.DeleteBranch)
if err := self.git.Branch.Delete(selectedBranch.Name, force); err != nil { if err := self.c.Git().Branch.Delete(selectedBranch.Name, force); err != nil {
errMessage := err.Error() errMessage := err.Error()
if !force && strings.Contains(errMessage, "git branch -D ") { if !force && strings.Contains(errMessage, "git branch -D ") {
return self.deleteWithForce(selectedBranch, true) return self.deleteWithForce(selectedBranch, true)
@ -310,12 +334,12 @@ func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, f
func (self *BranchesController) merge() error { func (self *BranchesController) merge() error {
selectedBranchName := self.context().GetSelected().Name selectedBranchName := self.context().GetSelected().Name
return self.helpers.MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranchName) return self.c.Helpers().MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranchName)
} }
func (self *BranchesController) rebase() error { func (self *BranchesController) rebase() error {
selectedBranchName := self.context().GetSelected().Name selectedBranchName := self.context().GetSelected().Name
return self.helpers.MergeAndRebase.RebaseOntoRef(selectedBranchName) return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName)
} }
func (self *BranchesController) fastForward(branch *models.Branch) error { func (self *BranchesController) fastForward(branch *models.Branch) error {
@ -340,10 +364,10 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
) )
return self.c.WithLoaderPanel(message, func() error { return self.c.WithLoaderPanel(message, func() error {
if branch == self.helpers.Refs.GetCheckedOutRef() { if branch == self.c.Helpers().Refs.GetCheckedOutRef() {
self.c.LogAction(action) self.c.LogAction(action)
err := self.git.Sync.Pull( err := self.c.Git().Sync.Pull(
git_commands.PullOptions{ git_commands.PullOptions{
RemoteName: branch.UpstreamRemote, RemoteName: branch.UpstreamRemote,
BranchName: branch.UpstreamBranch, BranchName: branch.UpstreamBranch,
@ -357,7 +381,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
} else { } else {
self.c.LogAction(action) self.c.LogAction(action)
err := self.git.Sync.FastForward(branch.Name, branch.UpstreamRemote, branch.UpstreamBranch) err := self.c.Git().Sync.FastForward(branch.Name, branch.UpstreamRemote, branch.UpstreamBranch)
if err != nil { if err != nil {
_ = self.c.Error(err) _ = self.c.Error(err)
} }
@ -369,11 +393,11 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
} }
func (self *BranchesController) createTag(branch *models.Branch) error { func (self *BranchesController) createTag(branch *models.Branch) error {
return self.helpers.Tags.CreateTagMenu(branch.FullRefName(), func() {}) return self.c.Helpers().Tags.CreateTagMenu(branch.FullRefName(), func() {})
} }
func (self *BranchesController) createResetMenu(selectedBranch *models.Branch) error { func (self *BranchesController) createResetMenu(selectedBranch *models.Branch) error {
return self.helpers.Refs.CreateGitResetMenu(selectedBranch.Name) return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.Name)
} }
func (self *BranchesController) rename(branch *models.Branch) error { func (self *BranchesController) rename(branch *models.Branch) error {
@ -383,7 +407,7 @@ func (self *BranchesController) rename(branch *models.Branch) error {
InitialContent: branch.Name, InitialContent: branch.Name,
HandleConfirm: func(newBranchName string) error { HandleConfirm: func(newBranchName string) error {
self.c.LogAction(self.c.Tr.Actions.RenameBranch) self.c.LogAction(self.c.Tr.Actions.RenameBranch)
if err := self.git.Branch.Rename(branch.Name, newBranchName); err != nil { if err := self.c.Git().Branch.Rename(branch.Name, newBranchName); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -391,7 +415,7 @@ func (self *BranchesController) rename(branch *models.Branch) error {
_ = self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.BRANCHES}}) _ = self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.BRANCHES}})
// now that we've got our stuff again we need to find that branch and reselect it. // now that we've got our stuff again we need to find that branch and reselect it.
for i, newBranch := range self.model.Branches { for i, newBranch := range self.c.Model().Branches {
if newBranch.Name == newBranchName { if newBranch.Name == newBranchName {
self.context().SetSelectedLineIdx(i) self.context().SetSelectedLineIdx(i)
if err := self.context().HandleRender(); err != nil { if err := self.context().HandleRender(); err != nil {
@ -420,7 +444,7 @@ func (self *BranchesController) rename(branch *models.Branch) error {
} }
func (self *BranchesController) newBranch(selectedBranch *models.Branch) error { func (self *BranchesController) newBranch(selectedBranch *models.Branch) error {
return self.helpers.Refs.NewBranch(selectedBranch.FullRefName(), selectedBranch.RefName(), "") return self.c.Helpers().Refs.NewBranch(selectedBranch.FullRefName(), selectedBranch.RefName(), "")
} }
func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Branch, checkedOutBranch *models.Branch) error { func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Branch, checkedOutBranch *models.Branch) error {
@ -443,7 +467,7 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra
OnPress: func() error { OnPress: func() error {
return self.c.Prompt(types.PromptOpts{ return self.c.Prompt(types.PromptOpts{
Title: branch.Name + " →", Title: branch.Name + " →",
FindSuggestionsFunc: self.helpers.Suggestions.GetBranchNameSuggestionsFunc(), FindSuggestionsFunc: self.c.Helpers().Suggestions.GetBranchNameSuggestionsFunc(),
HandleConfirm: func(targetBranchName string) error { HandleConfirm: func(targetBranchName string) error {
return self.createPullRequest(branch.Name, targetBranchName) return self.createPullRequest(branch.Name, targetBranchName)
}, },
@ -471,14 +495,14 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra
} }
func (self *BranchesController) createPullRequest(from string, to string) error { func (self *BranchesController) createPullRequest(from string, to string) error {
url, err := self.helpers.Host.GetPullRequestURL(from, to) url, err := self.c.Helpers().Host.GetPullRequestURL(from, to)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
self.c.LogAction(self.c.Tr.Actions.OpenPullRequest) self.c.LogAction(self.c.Tr.Actions.OpenPullRequest)
if err := self.os.OpenLink(url); err != nil { if err := self.c.OS().OpenLink(url); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }

View File

@ -0,0 +1,42 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type CommandLogController struct {
baseController
c *ControllerCommon
}
var _ types.IController = &CommandLogController{}
func NewCommandLogController(
common *ControllerCommon,
) *CommandLogController {
return &CommandLogController{
baseController: baseController{},
c: common,
}
}
func (self *CommandLogController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{}
return bindings
}
func (self *CommandLogController) GetOnFocusLost() func(types.OnFocusLostOpts) error {
return func(types.OnFocusLostOpts) error {
self.c.Views().Extras.Autoscroll = true
return nil
}
}
func (self *CommandLogController) Context() types.Context {
return self.context()
}
func (self *CommandLogController) context() types.Context {
return self.c.Contexts().CommandLog
}

View File

@ -6,17 +6,17 @@ import (
type CommitDescriptionController struct { type CommitDescriptionController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
var _ types.IController = &CommitMessageController{} var _ types.IController = &CommitMessageController{}
func NewCommitDescriptionController( func NewCommitDescriptionController(
common *controllerCommon, common *ControllerCommon,
) *CommitDescriptionController { ) *CommitDescriptionController {
return &CommitDescriptionController{ return &CommitDescriptionController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
@ -44,17 +44,17 @@ func (self *CommitDescriptionController) Context() types.Context {
} }
func (self *CommitDescriptionController) context() types.Context { func (self *CommitDescriptionController) context() types.Context {
return self.contexts.CommitMessage return self.c.Contexts().CommitMessage
} }
func (self *CommitDescriptionController) switchToCommitMessage() error { func (self *CommitDescriptionController) switchToCommitMessage() error {
return self.c.PushContext(self.contexts.CommitMessage) return self.c.PushContext(self.c.Contexts().CommitMessage)
} }
func (self *CommitDescriptionController) close() error { func (self *CommitDescriptionController) close() error {
return self.helpers.Commits.CloseCommitMessagePanel() return self.c.Helpers().Commits.CloseCommitMessagePanel()
} }
func (self *CommitDescriptionController) confirm() error { func (self *CommitDescriptionController) confirm() error {
return self.helpers.Commits.HandleCommitConfirm() return self.c.Helpers().Commits.HandleCommitConfirm()
} }

View File

@ -8,29 +8,33 @@ import (
type CommitMessageController struct { type CommitMessageController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
var _ types.IController = &CommitMessageController{} var _ types.IController = &CommitMessageController{}
func NewCommitMessageController( func NewCommitMessageController(
common *controllerCommon, common *ControllerCommon,
) *CommitMessageController { ) *CommitMessageController {
return &CommitMessageController{ return &CommitMessageController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
// TODO: merge that commit panel PR because we're not currently showing how to add a newline as it's
// handled by the editor func rather than by the controller here.
func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.SubmitEditorText), Key: opts.GetKey(opts.Config.Universal.SubmitEditorText),
Handler: self.confirm, Handler: self.confirm,
Description: self.c.Tr.LcConfirm,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Return), Key: opts.GetKey(opts.Config.Universal.Return),
Handler: self.close, Handler: self.close,
Description: self.c.Tr.LcClose,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.PrevItem), Key: opts.GetKey(opts.Config.Universal.PrevItem),
@ -49,12 +53,19 @@ func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts)
return bindings return bindings
} }
func (self *CommitMessageController) GetOnFocusLost() func(types.OnFocusLostOpts) error {
return func(types.OnFocusLostOpts) error {
self.context().RenderCommitLength()
return nil
}
}
func (self *CommitMessageController) Context() types.Context { func (self *CommitMessageController) Context() types.Context {
return self.context() return self.context()
} }
func (self *CommitMessageController) context() *context.CommitMessageContext { func (self *CommitMessageController) context() *context.CommitMessageContext {
return self.contexts.CommitMessage return self.c.Contexts().CommitMessage
} }
func (self *CommitMessageController) handlePreviousCommit() error { func (self *CommitMessageController) handlePreviousCommit() error {
@ -69,7 +80,7 @@ func (self *CommitMessageController) handleNextCommit() error {
} }
func (self *CommitMessageController) switchToCommitDescription() error { func (self *CommitMessageController) switchToCommitDescription() error {
if err := self.c.PushContext(self.contexts.CommitDescription); err != nil { if err := self.c.PushContext(self.c.Contexts().CommitDescription); err != nil {
return err return err
} }
return nil return nil
@ -80,10 +91,10 @@ func (self *CommitMessageController) handleCommitIndexChange(value int) error {
newIndex := currentIndex + value newIndex := currentIndex + value
if newIndex == context.NoCommitIndex { if newIndex == context.NoCommitIndex {
self.context().SetSelectedIndex(newIndex) self.context().SetSelectedIndex(newIndex)
self.helpers.Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage()) self.c.Helpers().Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage())
return nil return nil
} else if currentIndex == context.NoCommitIndex { } else if currentIndex == context.NoCommitIndex {
self.context().SetHistoryMessage(self.helpers.Commits.JoinCommitMessageAndDescription()) self.context().SetHistoryMessage(self.c.Helpers().Commits.JoinCommitMessageAndDescription())
} }
validCommit, err := self.setCommitMessageAtIndex(newIndex) validCommit, err := self.setCommitMessageAtIndex(newIndex)
@ -95,21 +106,21 @@ func (self *CommitMessageController) handleCommitIndexChange(value int) error {
// returns true if the given index is for a valid commit // returns true if the given index is for a valid commit
func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, error) { func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, error) {
commitMessage, err := self.git.Commit.GetCommitMessageFromHistory(index) commitMessage, err := self.c.Git().Commit.GetCommitMessageFromHistory(index)
if err != nil { if err != nil {
if err == git_commands.ErrInvalidCommitIndex { if err == git_commands.ErrInvalidCommitIndex {
return false, nil return false, nil
} }
return false, self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr) return false, self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
} }
self.helpers.Commits.UpdateCommitPanelView(commitMessage) self.c.Helpers().Commits.UpdateCommitPanelView(commitMessage)
return true, nil return true, nil
} }
func (self *CommitMessageController) confirm() error { func (self *CommitMessageController) confirm() error {
return self.helpers.Commits.HandleCommitConfirm() return self.c.Helpers().Commits.HandleCommitConfirm()
} }
func (self *CommitMessageController) close() error { func (self *CommitMessageController) close() error {
return self.helpers.Commits.CloseCommitMessagePanel() return self.c.Helpers().Commits.CloseCommitMessagePanel()
} }

View File

@ -11,17 +11,17 @@ import (
type CommitFilesController struct { type CommitFilesController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
var _ types.IController = &CommitFilesController{} var _ types.IController = &CommitFilesController{}
func NewCommitFilesController( func NewCommitFilesController(
common *controllerCommon, common *ControllerCommon,
) *CommitFilesController { ) *CommitFilesController {
return &CommitFilesController{ return &CommitFilesController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
@ -99,7 +99,39 @@ func (self *CommitFilesController) Context() types.Context {
} }
func (self *CommitFilesController) context() *context.CommitFilesContext { func (self *CommitFilesController) context() *context.CommitFilesContext {
return self.contexts.CommitFiles return self.c.Contexts().CommitFiles
}
func (self *CommitFilesController) GetOnRenderToMain() func() error {
return func() error {
node := self.context().GetSelected()
if node == nil {
return nil
}
ref := self.context().GetRef()
to := ref.RefName()
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(
from, to, reverse, node.GetPath(), false, self.c.State().GetIgnoreWhitespaceInDiffView(),
)
task := types.NewRunPtyTask(cmdObj.GetCmd())
pair := self.c.MainViewPairs().Normal
if node.File != nil {
pair = self.c.MainViewPairs().PatchBuilding
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: pair,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.Patch,
Task: task,
},
Secondary: secondaryPatchPanelUpdateOpts(self.c),
})
}
} }
func (self *CommitFilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error { func (self *CommitFilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error {
@ -112,7 +144,7 @@ func (self *CommitFilesController) onClickMain(opts gocui.ViewMouseBindingOpts)
func (self *CommitFilesController) checkout(node *filetree.CommitFileNode) error { func (self *CommitFilesController) checkout(node *filetree.CommitFileNode) error {
self.c.LogAction(self.c.Tr.Actions.CheckoutFile) self.c.LogAction(self.c.Tr.Actions.CheckoutFile)
if err := self.git.WorkingTree.CheckoutFile(self.context().GetRef().RefName(), node.GetPath()); err != nil { if err := self.c.Git().WorkingTree.CheckoutFile(self.context().GetRef().RefName(), node.GetPath()); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -120,7 +152,7 @@ func (self *CommitFilesController) checkout(node *filetree.CommitFileNode) error
} }
func (self *CommitFilesController) discard(node *filetree.CommitFileNode) error { func (self *CommitFilesController) discard(node *filetree.CommitFileNode) error {
if ok, err := self.helpers.PatchBuilding.ValidateNormalWorkingTreeState(); !ok { if ok, err := self.c.Helpers().PatchBuilding.ValidateNormalWorkingTreeState(); !ok {
return err return err
} }
@ -130,8 +162,8 @@ func (self *CommitFilesController) discard(node *filetree.CommitFileNode) error
HandleConfirm: func() error { HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.DiscardOldFileChange) self.c.LogAction(self.c.Tr.Actions.DiscardOldFileChange)
if err := self.git.Rebase.DiscardOldFileChanges(self.model.Commits, self.contexts.LocalCommits.GetSelectedLineIdx(), node.GetPath()); err != nil { if err := self.c.Git().Rebase.DiscardOldFileChanges(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), node.GetPath()); err != nil {
if err := self.helpers.MergeAndRebase.CheckMergeOrRebase(err); err != nil { if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err); err != nil {
return err return err
} }
} }
@ -143,7 +175,7 @@ func (self *CommitFilesController) discard(node *filetree.CommitFileNode) error
} }
func (self *CommitFilesController) open(node *filetree.CommitFileNode) error { func (self *CommitFilesController) open(node *filetree.CommitFileNode) error {
return self.helpers.Files.OpenFile(node.GetPath()) return self.c.Helpers().Files.OpenFile(node.GetPath())
} }
func (self *CommitFilesController) edit(node *filetree.CommitFileNode) error { func (self *CommitFilesController) edit(node *filetree.CommitFileNode) error {
@ -151,13 +183,13 @@ func (self *CommitFilesController) edit(node *filetree.CommitFileNode) error {
return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory) return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory)
} }
return self.helpers.Files.EditFile(node.GetPath()) return self.c.Helpers().Files.EditFile(node.GetPath())
} }
func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode) error { func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode) error {
toggle := func() error { toggle := func() error {
return self.c.WithWaitingStatus(self.c.Tr.LcUpdatingPatch, func() error { return self.c.WithWaitingStatus(self.c.Tr.LcUpdatingPatch, func() error {
if !self.git.Patch.PatchBuilder.Active() { if !self.c.Git().Patch.PatchBuilder.Active() {
if err := self.startPatchBuilder(); err != nil { if err := self.startPatchBuilder(); err != nil {
return err return err
} }
@ -166,34 +198,34 @@ func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode)
// if there is any file that hasn't been fully added we'll fully add everything, // if there is any file that hasn't been fully added we'll fully add everything,
// otherwise we'll remove everything // otherwise we'll remove everything
adding := node.SomeFile(func(file *models.CommitFile) bool { adding := node.SomeFile(func(file *models.CommitFile) bool {
return self.git.Patch.PatchBuilder.GetFileStatus(file.Name, self.context().GetRef().RefName()) != patch.WHOLE return self.c.Git().Patch.PatchBuilder.GetFileStatus(file.Name, self.context().GetRef().RefName()) != patch.WHOLE
}) })
err := node.ForEachFile(func(file *models.CommitFile) error { err := node.ForEachFile(func(file *models.CommitFile) error {
if adding { if adding {
return self.git.Patch.PatchBuilder.AddFileWhole(file.Name) return self.c.Git().Patch.PatchBuilder.AddFileWhole(file.Name)
} else { } else {
return self.git.Patch.PatchBuilder.RemoveFile(file.Name) return self.c.Git().Patch.PatchBuilder.RemoveFile(file.Name)
} }
}) })
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
if self.git.Patch.PatchBuilder.IsEmpty() { if self.c.Git().Patch.PatchBuilder.IsEmpty() {
self.git.Patch.PatchBuilder.Reset() self.c.Git().Patch.PatchBuilder.Reset()
} }
return self.c.PostRefreshUpdate(self.context()) return self.c.PostRefreshUpdate(self.context())
}) })
} }
if self.git.Patch.PatchBuilder.Active() && self.git.Patch.PatchBuilder.To != self.context().GetRef().RefName() { if self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.To != self.context().GetRef().RefName() {
return self.c.Confirm(types.ConfirmOpts{ return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DiscardPatch, Title: self.c.Tr.DiscardPatch,
Prompt: self.c.Tr.DiscardPatchConfirm, Prompt: self.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error { HandleConfirm: func() error {
self.git.Patch.PatchBuilder.Reset() self.c.Git().Patch.PatchBuilder.Reset()
return toggle() return toggle()
}, },
}) })
@ -213,9 +245,9 @@ func (self *CommitFilesController) startPatchBuilder() error {
canRebase := commitFilesContext.GetCanRebase() canRebase := commitFilesContext.GetCanRebase()
ref := commitFilesContext.GetRef() ref := commitFilesContext.GetRef()
to := ref.RefName() to := ref.RefName()
from, reverse := self.modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
self.git.Patch.PatchBuilder.Start(from, to, reverse, canRebase) self.c.Git().Patch.PatchBuilder.Start(from, to, reverse, canRebase)
return nil return nil
} }
@ -229,21 +261,21 @@ func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode
} }
enterTheFile := func() error { enterTheFile := func() error {
if !self.git.Patch.PatchBuilder.Active() { if !self.c.Git().Patch.PatchBuilder.Active() {
if err := self.startPatchBuilder(); err != nil { if err := self.startPatchBuilder(); err != nil {
return err return err
} }
} }
return self.c.PushContext(self.contexts.CustomPatchBuilder, opts) return self.c.PushContext(self.c.Contexts().CustomPatchBuilder, opts)
} }
if self.git.Patch.PatchBuilder.Active() && self.git.Patch.PatchBuilder.To != self.context().GetRef().RefName() { if self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.To != self.context().GetRef().RefName() {
return self.c.Confirm(types.ConfirmOpts{ return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DiscardPatch, Title: self.c.Tr.DiscardPatch,
Prompt: self.c.Tr.DiscardPatchConfirm, Prompt: self.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error { HandleConfirm: func() error {
self.git.Patch.PatchBuilder.Reset() self.c.Git().Patch.PatchBuilder.Reset()
return enterTheFile() return enterTheFile()
}, },
}) })

View File

@ -1,42 +1,24 @@
package controllers package controllers
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
) )
type controllerCommon struct { type ControllerCommon struct {
c *types.HelperCommon *helpers.HelperCommon
os *oscommands.OSCommand IGetHelpers
git *commands.GitCommand }
helpers *helpers.Helpers
model *types.Model type IGetHelpers interface {
contexts *context.ContextTree Helpers() *helpers.Helpers
modes *types.Modes
mutexes *types.Mutexes
} }
func NewControllerCommon( func NewControllerCommon(
c *types.HelperCommon, c *helpers.HelperCommon,
os *oscommands.OSCommand, IGetHelpers IGetHelpers,
git *commands.GitCommand, ) *ControllerCommon {
helpers *helpers.Helpers, return &ControllerCommon{
model *types.Model, HelperCommon: c,
contexts *context.ContextTree, IGetHelpers: IGetHelpers,
modes *types.Modes,
mutexes *types.Mutexes,
) *controllerCommon {
return &controllerCommon{
c: c,
os: os,
git: git,
helpers: helpers,
model: model,
contexts: contexts,
modes: modes,
mutexes: mutexes,
} }
} }

View File

@ -0,0 +1,65 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ConfirmationController struct {
baseController
c *ControllerCommon
}
var _ types.IController = &ConfirmationController{}
func NewConfirmationController(
common *ControllerCommon,
) *ConfirmationController {
return &ConfirmationController{
baseController: baseController{},
c: common,
}
}
func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.Confirm),
Handler: func() error { return self.context().State.OnConfirm() },
Description: self.c.Tr.LcConfirm,
Display: true,
},
{
Key: opts.GetKey(opts.Config.Universal.Return),
Handler: func() error { return self.context().State.OnClose() },
Description: self.c.Tr.LcCloseCancel,
Display: true,
},
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error {
if len(self.c.Contexts().Suggestions.State.Suggestions) > 0 {
return self.c.ReplaceContext(self.c.Contexts().Suggestions)
}
return nil
},
},
}
return bindings
}
func (self *ConfirmationController) GetOnFocusLost() func(types.OnFocusLostOpts) error {
return func(types.OnFocusLostOpts) error {
self.c.Helpers().Confirmation.DeactivateConfirmationPrompt()
return nil
}
}
func (self *ConfirmationController) Context() types.Context {
return self.context()
}
func (self *ConfirmationController) context() *context.ConfirmationContext {
return self.c.Contexts().Confirmation
}

View File

@ -24,17 +24,17 @@ var CONTEXT_KEYS_SHOWING_DIFFS = []types.ContextKey{
type ContextLinesController struct { type ContextLinesController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
var _ types.IController = &ContextLinesController{} var _ types.IController = &ContextLinesController{}
func NewContextLinesController( func NewContextLinesController(
common *controllerCommon, common *ControllerCommon,
) *ContextLinesController { ) *ContextLinesController {
return &ContextLinesController{ return &ContextLinesController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
@ -101,7 +101,7 @@ func (self *ContextLinesController) applyChange() error {
} }
func (self *ContextLinesController) checkCanChangeContext() error { func (self *ContextLinesController) checkCanChangeContext() error {
if self.git.Patch.PatchBuilder.Active() { if self.c.Git().Patch.PatchBuilder.Active() {
return errors.New(self.c.Tr.CantChangeContextSizeError) return errors.New(self.c.Tr.CantChangeContextSizeError)
} }

View File

@ -0,0 +1,53 @@
package controllers
import (
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type CustomCommandAction struct {
c *ControllerCommon
}
func (self *CustomCommandAction) Call() error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.CustomCommand,
FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(),
HandleConfirm: func(command string) error {
if self.shouldSaveCommand(command) {
self.c.GetAppState().CustomCommandsHistory = utils.Limit(
lo.Uniq(append(self.c.GetAppState().CustomCommandsHistory, command)),
1000,
)
}
err := self.c.SaveAppState()
if err != nil {
self.c.Log.Error(err)
}
self.c.LogAction(self.c.Tr.Actions.CustomCommand)
return self.c.RunSubprocessAndRefresh(
self.c.OS().Cmd.NewShell(command),
)
},
})
}
func (self *CustomCommandAction) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
// reversing so that we display the latest command first
history := slices.Reverse(self.c.GetAppState().CustomCommandsHistory)
return helpers.FuzzySearchFunc(history)
}
// this mimics the shell functionality `ignorespace`
// which doesn't save a command to history if it starts with a space
func (self *CustomCommandAction) shouldSaveCommand(command string) bool {
return !strings.HasPrefix(command, " ")
}

View File

@ -0,0 +1,219 @@
package controllers
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type CustomPatchOptionsMenuAction struct {
c *ControllerCommon
}
func (self *CustomPatchOptionsMenuAction) Call() error {
if !self.c.Git().Patch.PatchBuilder.Active() {
return self.c.ErrorMsg(self.c.Tr.NoPatchError)
}
menuItems := []*types.MenuItem{
{
Label: "reset patch",
OnPress: self.c.Helpers().PatchBuilding.Reset,
Key: 'c',
},
{
Label: "apply patch",
OnPress: func() error { return self.handleApplyPatch(false) },
Key: 'a',
},
{
Label: "apply patch in reverse",
OnPress: func() error { return self.handleApplyPatch(true) },
Key: 'r',
},
}
if self.c.Git().Patch.PatchBuilder.CanRebase && self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_NONE {
menuItems = append(menuItems, []*types.MenuItem{
{
Label: fmt.Sprintf("remove patch from original commit (%s)", self.c.Git().Patch.PatchBuilder.To),
OnPress: self.handleDeletePatchFromCommit,
Key: 'd',
},
{
Label: "move patch out into index",
OnPress: self.handleMovePatchIntoWorkingTree,
Key: 'i',
},
{
Label: "move patch into new commit",
OnPress: self.handlePullPatchIntoNewCommit,
Key: 'n',
},
}...)
if self.c.CurrentContext().GetKey() == self.c.Contexts().LocalCommits.GetKey() {
selectedCommit := self.c.Contexts().LocalCommits.GetSelected()
if selectedCommit != nil && self.c.Git().Patch.PatchBuilder.To != selectedCommit.Sha {
// adding this option to index 1
menuItems = append(
menuItems[:1],
append(
[]*types.MenuItem{
{
Label: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
OnPress: self.handleMovePatchToSelectedCommit,
Key: 'm',
},
}, menuItems[1:]...,
)...,
)
}
}
}
menuItems = append(menuItems, []*types.MenuItem{
{
Label: "copy patch to clipboard",
OnPress: func() error { return self.copyPatchToClipboard() },
Key: 'y',
},
}...)
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.PatchOptionsTitle, Items: menuItems})
}
func (self *CustomPatchOptionsMenuAction) getPatchCommitIndex() int {
for index, commit := range self.c.Model().Commits {
if commit.Sha == self.c.Git().Patch.PatchBuilder.To {
return index
}
}
return -1
}
func (self *CustomPatchOptionsMenuAction) validateNormalWorkingTreeState() (bool, error) {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return false, self.c.ErrorMsg(self.c.Tr.CantPatchWhileRebasingError)
}
return true, nil
}
func (self *CustomPatchOptionsMenuAction) returnFocusFromPatchExplorerIfNecessary() error {
if self.c.CurrentContext().GetKey() == self.c.Contexts().CustomPatchBuilder.GetKey() {
return self.c.Helpers().PatchBuilding.Escape()
}
return nil
}
func (self *CustomPatchOptionsMenuAction) handleDeletePatchFromCommit() error {
if ok, err := self.validateNormalWorkingTreeState(); !ok {
return err
}
if err := self.returnFocusFromPatchExplorerIfNecessary(); err != nil {
return err
}
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error {
commitIndex := self.getPatchCommitIndex()
self.c.LogAction(self.c.Tr.Actions.RemovePatchFromCommit)
err := self.c.Git().Patch.DeletePatchesFromCommit(self.c.Model().Commits, commitIndex)
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
})
}
func (self *CustomPatchOptionsMenuAction) handleMovePatchToSelectedCommit() error {
if ok, err := self.validateNormalWorkingTreeState(); !ok {
return err
}
if err := self.returnFocusFromPatchExplorerIfNecessary(); err != nil {
return err
}
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error {
commitIndex := self.getPatchCommitIndex()
self.c.LogAction(self.c.Tr.Actions.MovePatchToSelectedCommit)
err := self.c.Git().Patch.MovePatchToSelectedCommit(self.c.Model().Commits, commitIndex, self.c.Contexts().LocalCommits.GetSelectedLineIdx())
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
})
}
func (self *CustomPatchOptionsMenuAction) handleMovePatchIntoWorkingTree() error {
if ok, err := self.validateNormalWorkingTreeState(); !ok {
return err
}
if err := self.returnFocusFromPatchExplorerIfNecessary(); err != nil {
return err
}
pull := func(stash bool) error {
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error {
commitIndex := self.getPatchCommitIndex()
self.c.LogAction(self.c.Tr.Actions.MovePatchIntoIndex)
err := self.c.Git().Patch.MovePatchIntoIndex(self.c.Model().Commits, commitIndex, stash)
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
})
}
if self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.MustStashTitle,
Prompt: self.c.Tr.MustStashWarning,
HandleConfirm: func() error {
return pull(true)
},
})
} else {
return pull(false)
}
}
func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error {
if ok, err := self.validateNormalWorkingTreeState(); !ok {
return err
}
if err := self.returnFocusFromPatchExplorerIfNecessary(); err != nil {
return err
}
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error {
commitIndex := self.getPatchCommitIndex()
self.c.LogAction(self.c.Tr.Actions.MovePatchIntoNewCommit)
err := self.c.Git().Patch.PullPatchIntoNewCommit(self.c.Model().Commits, commitIndex)
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
})
}
func (self *CustomPatchOptionsMenuAction) handleApplyPatch(reverse bool) error {
if err := self.returnFocusFromPatchExplorerIfNecessary(); err != nil {
return err
}
action := self.c.Tr.Actions.ApplyPatch
if reverse {
action = "Apply patch in reverse"
}
self.c.LogAction(action)
if err := self.c.Git().Patch.PatchBuilder.ApplyPatches(reverse); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}
func (self *CustomPatchOptionsMenuAction) copyPatchToClipboard() error {
patch := self.c.Git().Patch.PatchBuilder.RenderAggregatedPatch(true)
self.c.LogAction(self.c.Tr.Actions.CopyPatchToClipboard)
if err := self.c.OS().CopyToClipboard(patch); err != nil {
return self.c.Error(err)
}
self.c.Toast(self.c.Tr.PatchCopiedToClipboard)
return nil
}

View File

@ -0,0 +1,69 @@
package controllers
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type DiffingMenuAction struct {
c *ControllerCommon
}
func (self *DiffingMenuAction) Call() error {
names := self.c.Helpers().Diff.CurrentDiffTerminals()
menuItems := []*types.MenuItem{}
for _, name := range names {
name := name
menuItems = append(menuItems, []*types.MenuItem{
{
Label: fmt.Sprintf("%s %s", self.c.Tr.LcDiff, name),
OnPress: func() error {
self.c.Modes().Diffing.Ref = name
// can scope this down based on current view but too lazy right now
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
},
}...)
}
menuItems = append(menuItems, []*types.MenuItem{
{
Label: self.c.Tr.LcEnterRefToDiff,
OnPress: func() error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.LcEnteRefName,
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRefsSuggestionsFunc(),
HandleConfirm: func(response string) error {
self.c.Modes().Diffing.Ref = strings.TrimSpace(response)
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
})
},
},
}...)
if self.c.Modes().Diffing.Active() {
menuItems = append(menuItems, []*types.MenuItem{
{
Label: self.c.Tr.LcSwapDiff,
OnPress: func() error {
self.c.Modes().Diffing.Reverse = !self.c.Modes().Diffing.Reverse
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
},
{
Label: self.c.Tr.LcExitDiffMode,
OnPress: func() error {
self.c.Modes().Diffing = diffing.New()
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
},
}...)
}
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.DiffingMenuTitle, Items: menuItems})
}

View File

@ -13,23 +13,16 @@ import (
type FilesController struct { type FilesController struct {
baseController // nolint: unused baseController // nolint: unused
*controllerCommon c *ControllerCommon
enterSubmodule func(submodule *models.SubmoduleConfig) error
setCommitMessage func(message string)
} }
var _ types.IController = &FilesController{} var _ types.IController = &FilesController{}
func NewFilesController( func NewFilesController(
common *controllerCommon, common *ControllerCommon,
enterSubmodule func(submodule *models.SubmoduleConfig) error,
setCommitMessage func(message string),
) *FilesController { ) *FilesController {
return &FilesController{ return &FilesController{
controllerCommon: common, c: common,
enterSubmodule: enterSubmodule,
setCommitMessage: setCommitMessage,
} }
} }
@ -47,12 +40,12 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
}, },
{ {
Key: opts.GetKey(opts.Config.Files.CommitChanges), Key: opts.GetKey(opts.Config.Files.CommitChanges),
Handler: self.helpers.WorkingTree.HandleCommitPress, Handler: self.c.Helpers().WorkingTree.HandleCommitPress,
Description: self.c.Tr.CommitChanges, Description: self.c.Tr.CommitChanges,
}, },
{ {
Key: opts.GetKey(opts.Config.Files.CommitChangesWithoutHook), Key: opts.GetKey(opts.Config.Files.CommitChangesWithoutHook),
Handler: self.helpers.WorkingTree.HandleWIPCommitPress, Handler: self.c.Helpers().WorkingTree.HandleWIPCommitPress,
Description: self.c.Tr.LcCommitChangesWithoutHook, Description: self.c.Tr.LcCommitChangesWithoutHook,
}, },
{ {
@ -62,7 +55,7 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
}, },
{ {
Key: opts.GetKey(opts.Config.Files.CommitChangesWithEditor), Key: opts.GetKey(opts.Config.Files.CommitChangesWithEditor),
Handler: self.helpers.WorkingTree.HandleCommitEditorPress, Handler: self.c.Helpers().WorkingTree.HandleCommitEditorPress,
Description: self.c.Tr.CommitChangesWithEditor, Description: self.c.Tr.CommitChangesWithEditor,
}, },
{ {
@ -126,7 +119,7 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
}, },
{ {
Key: opts.GetKey(opts.Config.Files.OpenMergeTool), Key: opts.GetKey(opts.Config.Files.OpenMergeTool),
Handler: self.helpers.WorkingTree.OpenMergeTool, Handler: self.c.Helpers().WorkingTree.OpenMergeTool,
Description: self.c.Tr.LcOpenMergeTool, Description: self.c.Tr.LcOpenMergeTool,
}, },
{ {
@ -172,6 +165,74 @@ func (self *FilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []*
} }
} }
func (self *FilesController) GetOnRenderToMain() func() error {
return func() error {
return self.c.Helpers().Diff.WithDiffModeCheck(func() error {
node := self.context().GetSelected()
if node == nil {
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.DiffTitle,
Task: types.NewRenderStringTask(self.c.Tr.NoChangedFiles),
},
})
}
if node.File != nil && node.File.HasInlineMergeConflicts {
hasConflicts, err := self.c.Helpers().MergeConflicts.SetMergeState(node.GetPath())
if err != nil {
return err
}
if hasConflicts {
return self.c.Helpers().MergeConflicts.Render(false)
}
}
self.c.Helpers().MergeConflicts.ResetMergeState()
pair := self.c.MainViewPairs().Normal
if node.File != nil {
pair = self.c.MainViewPairs().Staging
}
split := self.c.UserConfig.Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
mainShowsStaged := !split && node.GetHasStagedChanges()
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, self.c.State().GetIgnoreWhitespaceInDiffView())
title := self.c.Tr.UnstagedChanges
if mainShowsStaged {
title = self.c.Tr.StagedChanges
}
refreshOpts := types.RefreshMainOpts{
Pair: pair,
Main: &types.ViewUpdateOpts{
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
Title: title,
},
}
if split {
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, self.c.State().GetIgnoreWhitespaceInDiffView())
title := self.c.Tr.StagedChanges
if mainShowsStaged {
title = self.c.Tr.UnstagedChanges
}
refreshOpts.Secondary = &types.ViewUpdateOpts{
Title: title,
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
}
}
return self.c.RenderToMainViews(refreshOpts)
})
}
}
func (self *FilesController) GetOnClick() func() error { func (self *FilesController) GetOnClick() func() error {
return self.checkSelectedFileNode(self.press) return self.checkSelectedFileNode(self.press)
} }
@ -227,7 +288,7 @@ func (self *FilesController) optimisticChange(node *filetree.FileNode, optimisti
rerender := false rerender := false
err := node.ForEachFile(func(f *models.File) error { err := node.ForEachFile(func(f *models.File) error {
// can't act on the file itself: we need to update the original model file // can't act on the file itself: we need to update the original model file
for _, modelFile := range self.model.Files { for _, modelFile := range self.c.Model().Files {
if modelFile.Name == f.Name { if modelFile.Name == f.Name {
if optimisticChangeFn(modelFile) { if optimisticChangeFn(modelFile) {
rerender = true rerender = true
@ -242,7 +303,7 @@ func (self *FilesController) optimisticChange(node *filetree.FileNode, optimisti
return err return err
} }
if rerender { if rerender {
if err := self.c.PostRefreshUpdate(self.contexts.Files); err != nil { if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil {
return err return err
} }
} }
@ -253,8 +314,8 @@ func (self *FilesController) optimisticChange(node *filetree.FileNode, optimisti
func (self *FilesController) pressWithLock(node *filetree.FileNode) error { func (self *FilesController) pressWithLock(node *filetree.FileNode) error {
// Obtaining this lock because optimistic rendering requires us to mutate // Obtaining this lock because optimistic rendering requires us to mutate
// the files in our model. // the files in our model.
self.mutexes.RefreshingFilesMutex.Lock() self.c.Mutexes().RefreshingFilesMutex.Lock()
defer self.mutexes.RefreshingFilesMutex.Unlock() defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
if node.IsFile() { if node.IsFile() {
file := node.File file := node.File
@ -266,7 +327,7 @@ func (self *FilesController) pressWithLock(node *filetree.FileNode) error {
return err return err
} }
if err := self.git.WorkingTree.StageFile(file.Name); err != nil { if err := self.c.Git().WorkingTree.StageFile(file.Name); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
} else { } else {
@ -276,7 +337,7 @@ func (self *FilesController) pressWithLock(node *filetree.FileNode) error {
return err return err
} }
if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { if err := self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
} }
@ -294,7 +355,7 @@ func (self *FilesController) pressWithLock(node *filetree.FileNode) error {
return err return err
} }
if err := self.git.WorkingTree.StageFile(node.Path); err != nil { if err := self.c.Git().WorkingTree.StageFile(node.Path); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
} else { } else {
@ -305,7 +366,7 @@ func (self *FilesController) pressWithLock(node *filetree.FileNode) error {
} }
// pretty sure it doesn't matter that we're always passing true here // pretty sure it doesn't matter that we're always passing true here
if err := self.git.WorkingTree.UnStageFile([]string{node.Path}, true); err != nil { if err := self.c.Git().WorkingTree.UnStageFile([]string{node.Path}, true); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
} }
@ -346,7 +407,7 @@ func (self *FilesController) Context() types.Context {
} }
func (self *FilesController) context() *context.WorkingTreeContext { func (self *FilesController) context() *context.WorkingTreeContext {
return self.contexts.Files return self.c.Contexts().Files
} }
func (self *FilesController) getSelectedFile() *models.File { func (self *FilesController) getSelectedFile() *models.File {
@ -373,10 +434,10 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
file := node.File file := node.File
submoduleConfigs := self.model.Submodules submoduleConfigs := self.c.Model().Submodules
if file.IsSubmodule(submoduleConfigs) { if file.IsSubmodule(submoduleConfigs) {
submoduleConfig := file.SubmoduleConfig(submoduleConfigs) submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
return self.enterSubmodule(submoduleConfig) return self.c.Helpers().Repos.EnterSubmodule(submoduleConfig)
} }
if file.HasInlineMergeConflicts { if file.HasInlineMergeConflicts {
@ -386,7 +447,7 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
return self.c.ErrorMsg(self.c.Tr.FileStagingRequirements) return self.c.ErrorMsg(self.c.Tr.FileStagingRequirements)
} }
return self.c.PushContext(self.contexts.Staging, opts) return self.c.PushContext(self.c.Contexts().Staging, opts)
} }
func (self *FilesController) toggleStagedAll() error { func (self *FilesController) toggleStagedAll() error {
@ -402,8 +463,8 @@ func (self *FilesController) toggleStagedAll() error {
} }
func (self *FilesController) toggleStagedAllWithLock() error { func (self *FilesController) toggleStagedAllWithLock() error {
self.mutexes.RefreshingFilesMutex.Lock() self.c.Mutexes().RefreshingFilesMutex.Lock()
defer self.mutexes.RefreshingFilesMutex.Unlock() defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
root := self.context().FileTreeViewModel.GetRoot() root := self.context().FileTreeViewModel.GetRoot()
@ -420,7 +481,7 @@ func (self *FilesController) toggleStagedAllWithLock() error {
return err return err
} }
if err := self.git.WorkingTree.StageAll(); err != nil { if err := self.c.Git().WorkingTree.StageAll(); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
} else { } else {
@ -430,7 +491,7 @@ func (self *FilesController) toggleStagedAllWithLock() error {
return err return err
} }
if err := self.git.WorkingTree.UnstageAll(); err != nil { if err := self.c.Git().WorkingTree.UnstageAll(); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
} }
@ -441,7 +502,7 @@ func (self *FilesController) toggleStagedAllWithLock() error {
func (self *FilesController) unstageFiles(node *filetree.FileNode) error { func (self *FilesController) unstageFiles(node *filetree.FileNode) error {
return node.ForEachFile(func(file *models.File) error { return node.ForEachFile(func(file *models.File) error {
if file.HasStagedChanges { if file.HasStagedChanges {
if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { if err := self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return err return err
} }
} }
@ -457,7 +518,7 @@ func (self *FilesController) ignoreOrExcludeTracked(node *filetree.FileNode, trA
return err return err
} }
if err := self.git.WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil { if err := self.c.Git().WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil {
return err return err
} }
@ -495,7 +556,7 @@ func (self *FilesController) ignore(node *filetree.FileNode) error {
if node.GetPath() == ".gitignore" { if node.GetPath() == ".gitignore" {
return self.c.ErrorMsg(self.c.Tr.Actions.IgnoreFileErr) return self.c.ErrorMsg(self.c.Tr.Actions.IgnoreFileErr)
} }
err := self.ignoreOrExcludeFile(node, self.c.Tr.IgnoreTracked, self.c.Tr.IgnoreTrackedPrompt, self.c.Tr.Actions.LcIgnoreExcludeFile, self.git.WorkingTree.Ignore) err := self.ignoreOrExcludeFile(node, self.c.Tr.IgnoreTracked, self.c.Tr.IgnoreTrackedPrompt, self.c.Tr.Actions.LcIgnoreExcludeFile, self.c.Git().WorkingTree.Ignore)
if err != nil { if err != nil {
return err return err
} }
@ -512,7 +573,7 @@ func (self *FilesController) exclude(node *filetree.FileNode) error {
return self.c.ErrorMsg(self.c.Tr.Actions.ExcludeGitIgnoreErr) return self.c.ErrorMsg(self.c.Tr.Actions.ExcludeGitIgnoreErr)
} }
err := self.ignoreOrExcludeFile(node, self.c.Tr.ExcludeTracked, self.c.Tr.ExcludeTrackedPrompt, self.c.Tr.Actions.ExcludeFile, self.git.WorkingTree.Exclude) err := self.ignoreOrExcludeFile(node, self.c.Tr.ExcludeTracked, self.c.Tr.ExcludeTrackedPrompt, self.c.Tr.Actions.ExcludeFile, self.c.Git().WorkingTree.Exclude)
if err != nil { if err != nil {
return err return err
} }
@ -552,19 +613,19 @@ func (self *FilesController) refresh() error {
} }
func (self *FilesController) handleAmendCommitPress() error { func (self *FilesController) handleAmendCommitPress() error {
if len(self.model.Files) == 0 { if len(self.c.Model().Files) == 0 {
return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle) return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle)
} }
if !self.helpers.WorkingTree.AnyStagedFiles() { if !self.c.Helpers().WorkingTree.AnyStagedFiles() {
return self.helpers.WorkingTree.PromptToStageAllAndRetry(self.handleAmendCommitPress) return self.c.Helpers().WorkingTree.PromptToStageAllAndRetry(self.handleAmendCommitPress)
} }
if len(self.model.Commits) == 0 { if len(self.c.Model().Commits) == 0 {
return self.c.ErrorMsg(self.c.Tr.NoCommitToAmend) return self.c.ErrorMsg(self.c.Tr.NoCommitToAmend)
} }
return self.helpers.AmendHelper.AmendHead() return self.c.Helpers().AmendHelper.AmendHead()
} }
func (self *FilesController) handleStatusFilterPressed() error { func (self *FilesController) handleStatusFilterPressed() error {
@ -603,7 +664,7 @@ func (self *FilesController) edit(node *filetree.FileNode) error {
return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory) return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory)
} }
return self.helpers.Files.EditFile(node.GetPath()) return self.c.Helpers().Files.EditFile(node.GetPath())
} }
func (self *FilesController) Open() error { func (self *FilesController) Open() error {
@ -612,7 +673,7 @@ func (self *FilesController) Open() error {
return nil return nil
} }
return self.helpers.Files.OpenFile(node.GetPath()) return self.c.Helpers().Files.OpenFile(node.GetPath())
} }
func (self *FilesController) switchToMerge() error { func (self *FilesController) switchToMerge() error {
@ -621,7 +682,7 @@ func (self *FilesController) switchToMerge() error {
return nil return nil
} }
return self.helpers.MergeConflicts.SwitchToMerge(file.Name) return self.c.Helpers().MergeConflicts.SwitchToMerge(file.Name)
} }
func (self *FilesController) createStashMenu() error { func (self *FilesController) createStashMenu() error {
@ -631,28 +692,28 @@ func (self *FilesController) createStashMenu() error {
{ {
Label: self.c.Tr.LcStashAllChanges, Label: self.c.Tr.LcStashAllChanges,
OnPress: func() error { OnPress: func() error {
if !self.helpers.WorkingTree.IsWorkingTreeDirty() { if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
return self.c.ErrorMsg(self.c.Tr.NoFilesToStash) return self.c.ErrorMsg(self.c.Tr.NoFilesToStash)
} }
return self.handleStashSave(self.git.Stash.Save, self.c.Tr.Actions.StashAllChanges) return self.handleStashSave(self.c.Git().Stash.Save, self.c.Tr.Actions.StashAllChanges)
}, },
Key: 'a', Key: 'a',
}, },
{ {
Label: self.c.Tr.LcStashAllChangesKeepIndex, Label: self.c.Tr.LcStashAllChangesKeepIndex,
OnPress: func() error { OnPress: func() error {
if !self.helpers.WorkingTree.IsWorkingTreeDirty() { if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
return self.c.ErrorMsg(self.c.Tr.NoFilesToStash) return self.c.ErrorMsg(self.c.Tr.NoFilesToStash)
} }
// if there are no staged files it behaves the same as Stash.Save // if there are no staged files it behaves the same as Stash.Save
return self.handleStashSave(self.git.Stash.StashAndKeepIndex, self.c.Tr.Actions.StashAllChangesKeepIndex) return self.handleStashSave(self.c.Git().Stash.StashAndKeepIndex, self.c.Tr.Actions.StashAllChangesKeepIndex)
}, },
Key: 'i', Key: 'i',
}, },
{ {
Label: self.c.Tr.LcStashIncludeUntrackedChanges, Label: self.c.Tr.LcStashIncludeUntrackedChanges,
OnPress: func() error { OnPress: func() error {
return self.handleStashSave(self.git.Stash.StashIncludeUntrackedChanges, self.c.Tr.Actions.StashIncludeUntrackedChanges) return self.handleStashSave(self.c.Git().Stash.StashIncludeUntrackedChanges, self.c.Tr.Actions.StashIncludeUntrackedChanges)
}, },
Key: 'U', Key: 'U',
}, },
@ -660,24 +721,24 @@ func (self *FilesController) createStashMenu() error {
Label: self.c.Tr.LcStashStagedChanges, Label: self.c.Tr.LcStashStagedChanges,
OnPress: func() error { OnPress: func() error {
// there must be something in staging otherwise the current implementation mucks the stash up // there must be something in staging otherwise the current implementation mucks the stash up
if !self.helpers.WorkingTree.AnyStagedFiles() { if !self.c.Helpers().WorkingTree.AnyStagedFiles() {
return self.c.ErrorMsg(self.c.Tr.NoTrackedStagedFilesStash) return self.c.ErrorMsg(self.c.Tr.NoTrackedStagedFilesStash)
} }
return self.handleStashSave(self.git.Stash.SaveStagedChanges, self.c.Tr.Actions.StashStagedChanges) return self.handleStashSave(self.c.Git().Stash.SaveStagedChanges, self.c.Tr.Actions.StashStagedChanges)
}, },
Key: 's', Key: 's',
}, },
{ {
Label: self.c.Tr.LcStashUnstagedChanges, Label: self.c.Tr.LcStashUnstagedChanges,
OnPress: func() error { OnPress: func() error {
if !self.helpers.WorkingTree.IsWorkingTreeDirty() { if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
return self.c.ErrorMsg(self.c.Tr.NoFilesToStash) return self.c.ErrorMsg(self.c.Tr.NoFilesToStash)
} }
if self.helpers.WorkingTree.AnyStagedFiles() { if self.c.Helpers().WorkingTree.AnyStagedFiles() {
return self.handleStashSave(self.git.Stash.StashUnstagedChanges, self.c.Tr.Actions.StashUnstagedChanges) return self.handleStashSave(self.c.Git().Stash.StashUnstagedChanges, self.c.Tr.Actions.StashUnstagedChanges)
} }
// ordinary stash // ordinary stash
return self.handleStashSave(self.git.Stash.Save, self.c.Tr.Actions.StashUnstagedChanges) return self.handleStashSave(self.c.Git().Stash.Save, self.c.Tr.Actions.StashUnstagedChanges)
}, },
Key: 'u', Key: 'u',
}, },
@ -686,11 +747,11 @@ func (self *FilesController) createStashMenu() error {
} }
func (self *FilesController) stash() error { func (self *FilesController) stash() error {
return self.handleStashSave(self.git.Stash.Save, self.c.Tr.Actions.StashAllChanges) return self.handleStashSave(self.c.Git().Stash.Save, self.c.Tr.Actions.StashAllChanges)
} }
func (self *FilesController) createResetToUpstreamMenu() error { func (self *FilesController) createResetToUpstreamMenu() error {
return self.helpers.Refs.CreateGitResetMenu("@{upstream}") return self.c.Helpers().Refs.CreateGitResetMenu("@{upstream}")
} }
func (self *FilesController) handleToggleDirCollapsed() error { func (self *FilesController) handleToggleDirCollapsed() error {
@ -701,7 +762,7 @@ func (self *FilesController) handleToggleDirCollapsed() error {
self.context().FileTreeViewModel.ToggleCollapsed(node.GetPath()) self.context().FileTreeViewModel.ToggleCollapsed(node.GetPath())
if err := self.c.PostRefreshUpdate(self.contexts.Files); err != nil { if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil {
self.c.Log.Error(err) self.c.Log.Error(err)
} }
@ -747,7 +808,7 @@ func (self *FilesController) fetch() error {
func (self *FilesController) fetchAux() (err error) { func (self *FilesController) fetchAux() (err error) {
self.c.LogAction("Fetch") self.c.LogAction("Fetch")
err = self.git.Sync.Fetch(git_commands.FetchOptions{}) err = self.c.Git().Sync.Fetch(git_commands.FetchOptions{})
if err != nil && strings.Contains(err.Error(), "exit status 128") { if err != nil && strings.Contains(err.Error(), "exit status 128") {
_ = self.c.ErrorMsg(self.c.Tr.PassUnameWrong) _ = self.c.ErrorMsg(self.c.Tr.PassUnameWrong)

View File

@ -12,17 +12,17 @@ import (
type FilesRemoveController struct { type FilesRemoveController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
var _ types.IController = &FilesRemoveController{} var _ types.IController = &FilesRemoveController{}
func NewFilesRemoveController( func NewFilesRemoveController(
common *controllerCommon, common *ControllerCommon,
) *FilesRemoveController { ) *FilesRemoveController {
return &FilesRemoveController{ return &FilesRemoveController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
@ -47,7 +47,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
Label: self.c.Tr.LcDiscardAllChanges, Label: self.c.Tr.LcDiscardAllChanges,
OnPress: func() error { OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.DiscardAllChangesInDirectory) self.c.LogAction(self.c.Tr.Actions.DiscardAllChangesInDirectory)
if err := self.git.WorkingTree.DiscardAllDirChanges(node); err != nil { if err := self.c.Git().WorkingTree.DiscardAllDirChanges(node); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
@ -67,7 +67,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
Label: self.c.Tr.LcDiscardUnstagedChanges, Label: self.c.Tr.LcDiscardUnstagedChanges,
OnPress: func() error { OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.DiscardUnstagedChangesInDirectory) self.c.LogAction(self.c.Tr.Actions.DiscardUnstagedChangesInDirectory)
if err := self.git.WorkingTree.DiscardUnstagedDirChanges(node); err != nil { if err := self.c.Git().WorkingTree.DiscardUnstagedDirChanges(node); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -85,7 +85,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
} else { } else {
file := node.File file := node.File
submodules := self.model.Submodules submodules := self.c.Model().Submodules
if file.IsSubmodule(submodules) { if file.IsSubmodule(submodules) {
submodule := file.SubmoduleConfig(submodules) submodule := file.SubmoduleConfig(submodules)
@ -103,7 +103,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
Label: self.c.Tr.LcDiscardAllChanges, Label: self.c.Tr.LcDiscardAllChanges,
OnPress: func() error { OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.DiscardAllChangesInFile) self.c.LogAction(self.c.Tr.Actions.DiscardAllChangesInFile)
if err := self.git.WorkingTree.DiscardAllFileChanges(file); err != nil { if err := self.c.Git().WorkingTree.DiscardAllFileChanges(file); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
@ -123,7 +123,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
Label: self.c.Tr.LcDiscardUnstagedChanges, Label: self.c.Tr.LcDiscardUnstagedChanges,
OnPress: func() error { OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.DiscardAllUnstagedChangesInFile) self.c.LogAction(self.c.Tr.Actions.DiscardAllUnstagedChangesInFile)
if err := self.git.WorkingTree.DiscardUnstagedFileChanges(file); err != nil { if err := self.c.Git().WorkingTree.DiscardUnstagedFileChanges(file); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -148,17 +148,17 @@ func (self *FilesRemoveController) ResetSubmodule(submodule *models.SubmoduleCon
return self.c.WithWaitingStatus(self.c.Tr.LcResettingSubmoduleStatus, func() error { return self.c.WithWaitingStatus(self.c.Tr.LcResettingSubmoduleStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.ResetSubmodule) self.c.LogAction(self.c.Tr.Actions.ResetSubmodule)
file := self.helpers.WorkingTree.FileForSubmodule(submodule) file := self.c.Helpers().WorkingTree.FileForSubmodule(submodule)
if file != nil { if file != nil {
if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { if err := self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
} }
if err := self.git.Submodule.Stash(submodule); err != nil { if err := self.c.Git().Submodule.Stash(submodule); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
if err := self.git.Submodule.Reset(submodule); err != nil { if err := self.c.Git().Submodule.Reset(submodule); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -182,5 +182,5 @@ func (self *FilesRemoveController) Context() types.Context {
} }
func (self *FilesRemoveController) context() *context.WorkingTreeContext { func (self *FilesRemoveController) context() *context.WorkingTreeContext {
return self.contexts.Files return self.c.Contexts().Files
} }

View File

@ -0,0 +1,78 @@
package controllers
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type FilteringMenuAction struct {
c *ControllerCommon
}
func (self *FilteringMenuAction) Call() error {
fileName := ""
switch self.c.CurrentSideContext() {
case self.c.Contexts().Files:
node := self.c.Contexts().Files.GetSelected()
if node != nil {
fileName = node.GetPath()
}
case self.c.Contexts().CommitFiles:
node := self.c.Contexts().CommitFiles.GetSelected()
if node != nil {
fileName = node.GetPath()
}
}
menuItems := []*types.MenuItem{}
if fileName != "" {
menuItems = append(menuItems, &types.MenuItem{
Label: fmt.Sprintf("%s '%s'", self.c.Tr.LcFilterBy, fileName),
OnPress: func() error {
return self.setFiltering(fileName)
},
})
}
menuItems = append(menuItems, &types.MenuItem{
Label: self.c.Tr.LcFilterPathOption,
OnPress: func() error {
return self.c.Prompt(types.PromptOpts{
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetFilePathSuggestionsFunc(),
Title: self.c.Tr.EnterFileName,
HandleConfirm: func(response string) error {
return self.setFiltering(strings.TrimSpace(response))
},
})
},
})
if self.c.Modes().Filtering.Active() {
menuItems = append(menuItems, &types.MenuItem{
Label: self.c.Tr.LcExitFilterMode,
OnPress: self.c.Helpers().Mode.ClearFiltering,
})
}
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.FilteringMenuTitle, Items: menuItems})
}
func (self *FilteringMenuAction) setFiltering(path string) error {
self.c.Modes().Filtering.SetPath(path)
repoState := self.c.State().GetRepoState()
if repoState.GetScreenMode() == types.SCREEN_NORMAL {
repoState.SetScreenMode(types.SCREEN_HALF)
}
if err := self.c.PushContext(self.c.Contexts().LocalCommits); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}, Then: func() {
self.c.Contexts().LocalCommits.SetSelectedLineIdx(0)
}})
}

View File

@ -11,17 +11,17 @@ import (
type GitFlowController struct { type GitFlowController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
var _ types.IController = &GitFlowController{} var _ types.IController = &GitFlowController{}
func NewGitFlowController( func NewGitFlowController(
common *controllerCommon, common *ControllerCommon,
) *GitFlowController { ) *GitFlowController {
return &GitFlowController{ return &GitFlowController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
@ -39,7 +39,7 @@ func (self *GitFlowController) GetKeybindings(opts types.KeybindingsOpts) []*typ
} }
func (self *GitFlowController) handleCreateGitFlowMenu(branch *models.Branch) error { func (self *GitFlowController) handleCreateGitFlowMenu(branch *models.Branch) error {
if !self.git.Flow.GitFlowEnabled() { if !self.c.Git().Flow.GitFlowEnabled() {
return self.c.ErrorMsg("You need to install git-flow and enable it in this repo to use git-flow features") return self.c.ErrorMsg("You need to install git-flow and enable it in this repo to use git-flow features")
} }
@ -52,7 +52,7 @@ func (self *GitFlowController) handleCreateGitFlowMenu(branch *models.Branch) er
HandleConfirm: func(name string) error { HandleConfirm: func(name string) error {
self.c.LogAction(self.c.Tr.Actions.GitFlowStart) self.c.LogAction(self.c.Tr.Actions.GitFlowStart)
return self.c.RunSubprocessAndRefresh( return self.c.RunSubprocessAndRefresh(
self.git.Flow.StartCmdObj(branchType, name), self.c.Git().Flow.StartCmdObj(branchType, name),
) )
}, },
}) })
@ -94,7 +94,7 @@ func (self *GitFlowController) handleCreateGitFlowMenu(branch *models.Branch) er
} }
func (self *GitFlowController) gitFlowFinishBranch(branchName string) error { func (self *GitFlowController) gitFlowFinishBranch(branchName string) error {
cmdObj, err := self.git.Flow.FinishCmdObj(branchName) cmdObj, err := self.c.Git().Flow.FinishCmdObj(branchName)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -119,5 +119,5 @@ func (self *GitFlowController) Context() types.Context {
} }
func (self *GitFlowController) context() *context.BranchesContext { func (self *GitFlowController) context() *context.BranchesContext {
return self.contexts.Branches return self.c.Contexts().Branches
} }

View File

@ -1,26 +1,21 @@
package controllers package controllers
import ( import (
"strings" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
) )
type GlobalController struct { type GlobalController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
func NewGlobalController( func NewGlobalController(
common *controllerCommon, common *ControllerCommon,
) *GlobalController { ) *GlobalController {
return &GlobalController{ return &GlobalController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
@ -31,47 +26,143 @@ func (self *GlobalController) GetKeybindings(opts types.KeybindingsOpts) []*type
Handler: self.customCommand, Handler: self.customCommand,
Description: self.c.Tr.LcExecuteCustomCommand, Description: self.c.Tr.LcExecuteCustomCommand,
}, },
} {
} Key: opts.GetKey(opts.Config.Universal.CreatePatchOptionsMenu),
Handler: self.createCustomPatchOptionsMenu,
func (self *GlobalController) customCommand() error { Description: self.c.Tr.ViewPatchOptions,
return self.c.Prompt(types.PromptOpts{ OpensMenu: true,
Title: self.c.Tr.CustomCommand,
FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(),
HandleConfirm: func(command string) error {
if self.shouldSaveCommand(command) {
self.c.GetAppState().CustomCommandsHistory = utils.Limit(
lo.Uniq(append(self.c.GetAppState().CustomCommandsHistory, command)),
1000,
)
}
err := self.c.SaveAppState()
if err != nil {
self.c.Log.Error(err)
}
self.c.LogAction(self.c.Tr.Actions.CustomCommand)
return self.c.RunSubprocessAndRefresh(
self.os.Cmd.NewShell(command),
)
}, },
}) {
} Key: opts.GetKey(opts.Config.Universal.CreateRebaseOptionsMenu),
Handler: self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu,
// this mimics the shell functionality `ignorespace` Description: self.c.Tr.ViewMergeRebaseOptions,
// which doesn't save a command to history if it starts with a space OpensMenu: true,
func (self *GlobalController) shouldSaveCommand(command string) bool { },
return !strings.HasPrefix(command, " ") {
} Key: opts.GetKey(opts.Config.Universal.Refresh),
Handler: self.refresh,
func (self *GlobalController) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion { Description: self.c.Tr.LcRefresh,
// reversing so that we display the latest command first },
history := slices.Reverse(self.c.GetAppState().CustomCommandsHistory) {
Key: opts.GetKey(opts.Config.Universal.NextScreenMode),
return helpers.FuzzySearchFunc(history) Handler: self.nextScreenMode,
Description: self.c.Tr.LcNextScreenMode,
},
{
Key: opts.GetKey(opts.Config.Universal.PrevScreenMode),
Handler: self.prevScreenMode,
Description: self.c.Tr.LcPrevScreenMode,
},
{
ViewName: "",
Key: opts.GetKey(opts.Config.Universal.OptionMenu),
Handler: self.createOptionsMenu,
OpensMenu: true,
},
{
ViewName: "",
Key: opts.GetKey(opts.Config.Universal.OptionMenuAlt1),
Modifier: gocui.ModNone,
// we have the description on the alt key and not the main key for legacy reasons
// (the original main key was 'x' but we've reassigned that to other purposes)
Description: self.c.Tr.LcOpenMenu,
Handler: self.createOptionsMenu,
},
{
ViewName: "",
Key: opts.GetKey(opts.Config.Universal.FilteringMenu),
Handler: self.createFilteringMenu,
Description: self.c.Tr.LcOpenFilteringMenu,
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Universal.DiffingMenu),
Handler: self.createDiffingMenu,
Description: self.c.Tr.LcOpenDiffingMenu,
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Universal.DiffingMenuAlt),
Handler: self.createDiffingMenu,
Description: self.c.Tr.LcOpenDiffingMenu,
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Universal.Quit),
Modifier: gocui.ModNone,
Handler: self.quit,
},
{
Key: opts.GetKey(opts.Config.Universal.QuitAlt1),
Modifier: gocui.ModNone,
Handler: self.quit,
},
{
Key: opts.GetKey(opts.Config.Universal.QuitWithoutChangingDirectory),
Modifier: gocui.ModNone,
Handler: self.quitWithoutChangingDirectory,
},
{
Key: opts.GetKey(opts.Config.Universal.Return),
Modifier: gocui.ModNone,
Handler: self.escape,
},
{
Key: opts.GetKey(opts.Config.Universal.ToggleWhitespaceInDiffView),
Handler: self.toggleWhitespace,
Description: self.c.Tr.ToggleWhitespaceInDiffView,
},
}
} }
func (self *GlobalController) Context() types.Context { func (self *GlobalController) Context() types.Context {
return nil return nil
} }
func (self *GlobalController) customCommand() error {
return (&CustomCommandAction{c: self.c}).Call()
}
func (self *GlobalController) createCustomPatchOptionsMenu() error {
return (&CustomPatchOptionsMenuAction{c: self.c}).Call()
}
func (self *GlobalController) refresh() error {
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}
func (self *GlobalController) nextScreenMode() error {
return (&ScreenModeActions{c: self.c}).Next()
}
func (self *GlobalController) prevScreenMode() error {
return (&ScreenModeActions{c: self.c}).Prev()
}
func (self *GlobalController) createOptionsMenu() error {
return (&OptionsMenuAction{c: self.c}).Call()
}
func (self *GlobalController) createFilteringMenu() error {
return (&FilteringMenuAction{c: self.c}).Call()
}
func (self *GlobalController) createDiffingMenu() error {
return (&DiffingMenuAction{c: self.c}).Call()
}
func (self *GlobalController) quit() error {
return (&QuitActions{c: self.c}).Quit()
}
func (self *GlobalController) quitWithoutChangingDirectory() error {
return (&QuitActions{c: self.c}).QuitWithoutChangingDirectory()
}
func (self *GlobalController) escape() error {
return (&QuitActions{c: self.c}).Escape()
}
func (self *GlobalController) toggleWhitespace() error {
return (&ToggleWhitespaceAction{c: self.c}).Call()
}

View File

@ -1,24 +1,20 @@
package helpers package helpers
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
type AmendHelper struct { type AmendHelper struct {
c *types.HelperCommon c *HelperCommon
git *commands.GitCommand
gpg *GpgHelper gpg *GpgHelper
} }
func NewAmendHelper( func NewAmendHelper(
c *types.HelperCommon, c *HelperCommon,
git *commands.GitCommand,
gpg *GpgHelper, gpg *GpgHelper,
) *AmendHelper { ) *AmendHelper {
return &AmendHelper{ return &AmendHelper{
c: c, c: c,
git: git,
gpg: gpg, gpg: gpg,
} }
} }
@ -28,7 +24,7 @@ func (self *AmendHelper) AmendHead() error {
Title: self.c.Tr.AmendLastCommitTitle, Title: self.c.Tr.AmendLastCommitTitle,
Prompt: self.c.Tr.SureToAmend, Prompt: self.c.Tr.SureToAmend,
HandleConfirm: func() error { HandleConfirm: func() error {
cmdObj := self.git.Commit.AmendHeadCmdObj() cmdObj := self.c.Git().Commit.AmendHeadCmdObj()
self.c.LogAction(self.c.Tr.Actions.AmendCommit) self.c.LogAction(self.c.Tr.Actions.AmendCommit)
return self.gpg.WithGpgHandling(cmdObj, self.c.Tr.AmendingStatus, nil) return self.gpg.WithGpgHandling(cmdObj, self.c.Tr.AmendingStatus, nil)
}, },

View File

@ -0,0 +1,68 @@
package helpers
import (
"time"
"github.com/jesseduffield/lazygit/pkg/gui/status"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type AppStatusHelper struct {
c *HelperCommon
statusMgr func() *status.StatusManager
}
func NewAppStatusHelper(c *HelperCommon, statusMgr func() *status.StatusManager) *AppStatusHelper {
return &AppStatusHelper{
c: c,
statusMgr: statusMgr,
}
}
func (self *AppStatusHelper) Toast(message string) {
self.statusMgr().AddToastStatus(message)
self.renderAppStatus()
}
// withWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (self *AppStatusHelper) WithWaitingStatus(message string, f func() error) {
go utils.Safe(func() {
self.statusMgr().WithWaitingStatus(message, func() {
self.renderAppStatus()
if err := f(); err != nil {
self.c.OnUIThread(func() error {
return self.c.Error(err)
})
}
})
})
}
func (self *AppStatusHelper) HasStatus() bool {
return self.statusMgr().HasStatus()
}
func (self *AppStatusHelper) GetStatusString() string {
return self.statusMgr().GetStatusString()
}
func (self *AppStatusHelper) renderAppStatus() {
go utils.Safe(func() {
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for range ticker.C {
appStatus := self.statusMgr().GetStatusString()
self.c.OnUIThread(func() error {
self.c.SetViewContent(self.c.Views().AppStatus, appStatus)
return nil
})
if appStatus == "" {
return
}
}
})
}

View File

@ -1,23 +1,15 @@
package helpers package helpers
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
type BisectHelper struct { type BisectHelper struct {
c *types.HelperCommon c *HelperCommon
git *commands.GitCommand
} }
func NewBisectHelper( func NewBisectHelper(c *HelperCommon) *BisectHelper {
c *types.HelperCommon, return &BisectHelper{c: c}
git *commands.GitCommand,
) *BisectHelper {
return &BisectHelper{
c: c,
git: git,
}
} }
func (self *BisectHelper) Reset() error { func (self *BisectHelper) Reset() error {
@ -26,7 +18,7 @@ func (self *BisectHelper) Reset() error {
Prompt: self.c.Tr.Bisect.ResetPrompt, Prompt: self.c.Tr.Bisect.ResetPrompt,
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.ResetBisect) self.c.LogAction(self.c.Tr.Actions.ResetBisect)
if err := self.git.Bisect.Reset(); err != nil { if err := self.c.Git().Bisect.Reset(); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }

View File

@ -1,22 +1,13 @@
package helpers package helpers
import ( import (
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking" "github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
type CherryPickHelper struct { type CherryPickHelper struct {
c *types.HelperCommon c *HelperCommon
git *commands.GitCommand
contexts *context.ContextTree
getData func() *cherrypicking.CherryPicking
rebaseHelper *MergeAndRebaseHelper rebaseHelper *MergeAndRebaseHelper
} }
@ -25,38 +16,31 @@ type CherryPickHelper struct {
// even if in truth we're running git cherry-pick // even if in truth we're running git cherry-pick
func NewCherryPickHelper( func NewCherryPickHelper(
c *types.HelperCommon, c *HelperCommon,
git *commands.GitCommand,
contexts *context.ContextTree,
getData func() *cherrypicking.CherryPicking,
rebaseHelper *MergeAndRebaseHelper, rebaseHelper *MergeAndRebaseHelper,
) *CherryPickHelper { ) *CherryPickHelper {
return &CherryPickHelper{ return &CherryPickHelper{
c: c, c: c,
git: git,
contexts: contexts,
getData: getData,
rebaseHelper: rebaseHelper, rebaseHelper: rebaseHelper,
} }
} }
func (self *CherryPickHelper) getData() *cherrypicking.CherryPicking {
return self.c.Modes().CherryPicking
}
func (self *CherryPickHelper) Copy(commit *models.Commit, commitsList []*models.Commit, context types.Context) error { func (self *CherryPickHelper) Copy(commit *models.Commit, commitsList []*models.Commit, context types.Context) error {
if err := self.resetIfNecessary(context); err != nil { if err := self.resetIfNecessary(context); err != nil {
return err return err
} }
// we will un-copy it if it's already copied // we will un-copy it if it's already copied
for index, cherryPickedCommit := range self.getData().CherryPickedCommits { if self.getData().SelectedShaSet().Includes(commit.Sha) {
if commit.Sha == cherryPickedCommit.Sha { self.getData().Remove(commit, commitsList)
self.getData().CherryPickedCommits = append( } else {
self.getData().CherryPickedCommits[0:index], self.getData().Add(commit, commitsList)
self.getData().CherryPickedCommits[index+1:]...,
)
return self.rerender()
}
} }
self.add(commit, commitsList)
return self.rerender() return self.rerender()
} }
@ -65,7 +49,7 @@ func (self *CherryPickHelper) CopyRange(selectedIndex int, commitsList []*models
return err return err
} }
commitSet := self.CherryPickedCommitShaSet() commitSet := self.getData().SelectedShaSet()
// find the last commit that is copied that's above our position // find the last commit that is copied that's above our position
// if there are none, startIndex = 0 // if there are none, startIndex = 0
@ -78,7 +62,7 @@ func (self *CherryPickHelper) CopyRange(selectedIndex int, commitsList []*models
for index := startIndex; index <= selectedIndex; index++ { for index := startIndex; index <= selectedIndex; index++ {
commit := commitsList[index] commit := commitsList[index]
self.add(commit, commitsList) self.getData().Add(commit, commitsList)
} }
return self.rerender() return self.rerender()
@ -93,7 +77,7 @@ func (self *CherryPickHelper) Paste() error {
HandleConfirm: func() error { HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.CherryPickingStatus, func() error { return self.c.WithWaitingStatus(self.c.Tr.CherryPickingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.CherryPick) self.c.LogAction(self.c.Tr.Actions.CherryPick)
err := self.git.Rebase.CherryPickCommits(self.getData().CherryPickedCommits) err := self.c.Git().Rebase.CherryPickCommits(self.getData().CherryPickedCommits)
return self.rebaseHelper.CheckMergeOrRebase(err) return self.rebaseHelper.CheckMergeOrRebase(err)
}) })
}, },
@ -107,26 +91,6 @@ func (self *CherryPickHelper) Reset() error {
return self.rerender() return self.rerender()
} }
func (self *CherryPickHelper) CherryPickedCommitShaSet() *set.Set[string] {
shas := slices.Map(self.getData().CherryPickedCommits, func(commit *models.Commit) string {
return commit.Sha
})
return set.NewFromSlice(shas)
}
func (self *CherryPickHelper) add(selectedCommit *models.Commit, commitsList []*models.Commit) {
commitSet := self.CherryPickedCommitShaSet()
commitSet.Add(selectedCommit.Sha)
cherryPickedCommits := slices.Filter(commitsList, func(commit *models.Commit) bool {
return commitSet.Includes(commit.Sha)
})
self.getData().CherryPickedCommits = slices.Map(cherryPickedCommits, func(commit *models.Commit) *models.Commit {
return &models.Commit{Name: commit.Name, Sha: commit.Sha}
})
}
// you can only copy from one context at a time, because the order and position of commits matter // you can only copy from one context at a time, because the order and position of commits matter
func (self *CherryPickHelper) resetIfNecessary(context types.Context) error { func (self *CherryPickHelper) resetIfNecessary(context types.Context) error {
oldContextKey := types.ContextKey(self.getData().ContextKey) oldContextKey := types.ContextKey(self.getData().ContextKey)
@ -142,9 +106,9 @@ func (self *CherryPickHelper) resetIfNecessary(context types.Context) error {
func (self *CherryPickHelper) rerender() error { func (self *CherryPickHelper) rerender() error {
for _, context := range []types.Context{ for _, context := range []types.Context{
self.contexts.LocalCommits, self.c.Contexts().LocalCommits,
self.contexts.ReflogCommits, self.c.Contexts().ReflogCommits,
self.contexts.SubCommits, self.c.Contexts().SubCommits,
} { } {
if err := self.c.PostRefreshUpdate(context); err != nil { if err := self.c.PostRefreshUpdate(context); err != nil {
return err return err

View File

@ -3,7 +3,6 @@ package helpers
import ( import (
"strings" "strings"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -12,38 +11,29 @@ type ICommitsHelper interface {
} }
type CommitsHelper struct { type CommitsHelper struct {
c *types.HelperCommon c *HelperCommon
model *types.Model
contexts *context.ContextTree
getCommitSummary func() string getCommitSummary func() string
setCommitSummary func(string) setCommitSummary func(string)
getCommitDescription func() string getCommitDescription func() string
setCommitDescription func(string) setCommitDescription func(string)
renderCommitLength func()
} }
var _ ICommitsHelper = &CommitsHelper{} var _ ICommitsHelper = &CommitsHelper{}
func NewCommitsHelper( func NewCommitsHelper(
c *types.HelperCommon, c *HelperCommon,
model *types.Model,
contexts *context.ContextTree,
getCommitSummary func() string, getCommitSummary func() string,
setCommitSummary func(string), setCommitSummary func(string),
getCommitDescription func() string, getCommitDescription func() string,
setCommitDescription func(string), setCommitDescription func(string),
renderCommitLength func(),
) *CommitsHelper { ) *CommitsHelper {
return &CommitsHelper{ return &CommitsHelper{
c: c, c: c,
model: model,
contexts: contexts,
getCommitSummary: getCommitSummary, getCommitSummary: getCommitSummary,
setCommitSummary: setCommitSummary, setCommitSummary: setCommitSummary,
getCommitDescription: getCommitDescription, getCommitDescription: getCommitDescription,
setCommitDescription: setCommitDescription, setCommitDescription: setCommitDescription,
renderCommitLength: renderCommitLength,
} }
} }
@ -62,7 +52,7 @@ func (self *CommitsHelper) SetMessageAndDescriptionInView(message string) {
self.setCommitSummary(summary) self.setCommitSummary(summary)
self.setCommitDescription(description) self.setCommitDescription(description)
self.renderCommitLength() self.c.Contexts().CommitMessage.RenderCommitLength()
} }
func (self *CommitsHelper) JoinCommitMessageAndDescription() string { func (self *CommitsHelper) JoinCommitMessageAndDescription() string {
@ -78,7 +68,7 @@ func (self *CommitsHelper) UpdateCommitPanelView(message string) {
self.SetMessageAndDescriptionInView(message) self.SetMessageAndDescriptionInView(message)
return return
} }
message = self.contexts.CommitMessage.GetPreservedMessage() message = self.c.Contexts().CommitMessage.GetPreservedMessage()
if message != "" { if message != "" {
self.SetMessageAndDescriptionInView(message) self.SetMessageAndDescriptionInView(message)
} else { } else {
@ -95,7 +85,7 @@ type OpenCommitMessagePanelOpts struct {
} }
func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOpts) error { func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOpts) error {
self.contexts.CommitMessage.SetPanelState( self.c.Contexts().CommitMessage.SetPanelState(
opts.CommitIndex, opts.CommitIndex,
opts.Title, opts.Title,
opts.PreserveMessage, opts.PreserveMessage,
@ -109,8 +99,8 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp
func (self *CommitsHelper) OnCommitSuccess() { func (self *CommitsHelper) OnCommitSuccess() {
// if we have a preserved message we want to clear it on success // if we have a preserved message we want to clear it on success
if self.contexts.CommitMessage.GetPreserveMessage() { if self.c.Contexts().CommitMessage.GetPreserveMessage() {
self.contexts.CommitMessage.SetPreservedMessage("") self.c.Contexts().CommitMessage.SetPreservedMessage("")
} }
self.SetMessageAndDescriptionInView("") self.SetMessageAndDescriptionInView("")
} }
@ -122,7 +112,7 @@ func (self *CommitsHelper) HandleCommitConfirm() error {
return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr) return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
} }
err := self.contexts.CommitMessage.OnConfirm(fullMessage) err := self.c.Contexts().CommitMessage.OnConfirm(fullMessage)
if err != nil { if err != nil {
return err return err
} }
@ -131,15 +121,15 @@ func (self *CommitsHelper) HandleCommitConfirm() error {
} }
func (self *CommitsHelper) CloseCommitMessagePanel() error { func (self *CommitsHelper) CloseCommitMessagePanel() error {
if self.contexts.CommitMessage.GetPreserveMessage() { if self.c.Contexts().CommitMessage.GetPreserveMessage() {
message := self.JoinCommitMessageAndDescription() message := self.JoinCommitMessageAndDescription()
self.contexts.CommitMessage.SetPreservedMessage(message) self.c.Contexts().CommitMessage.SetPreservedMessage(message)
} else { } else {
self.SetMessageAndDescriptionInView("") self.SetMessageAndDescriptionInView("")
} }
self.contexts.CommitMessage.SetHistoryMessage("") self.c.Contexts().CommitMessage.SetHistoryMessage("")
return self.PopCommitMessageContexts() return self.PopCommitMessageContexts()
} }
@ -160,7 +150,7 @@ func (self *CommitsHelper) pushCommitMessageContexts() error {
func (self *CommitsHelper) commitMessageContexts() []types.Context { func (self *CommitsHelper) commitMessageContexts() []types.Context {
return []types.Context{ return []types.Context{
self.contexts.CommitDescription, self.c.Contexts().CommitDescription,
self.contexts.CommitMessage, self.c.Contexts().CommitMessage,
} }
} }

View File

@ -0,0 +1,349 @@
package helpers
import (
goContext "context"
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/mattn/go-runewidth"
)
type ConfirmationHelper struct {
c *HelperCommon
}
func NewConfirmationHelper(c *HelperCommon) *ConfirmationHelper {
return &ConfirmationHelper{
c: c,
}
}
// This file is for the rendering of confirmation panels along with setting and handling associated
// keybindings.
func (self *ConfirmationHelper) wrappedConfirmationFunction(cancel goContext.CancelFunc, function func() error) func() error {
return func() error {
cancel()
if err := self.c.PopContext(); err != nil {
return err
}
if function != nil {
if err := function(); err != nil {
return self.c.Error(err)
}
}
return nil
}
}
func (self *ConfirmationHelper) wrappedPromptConfirmationFunction(cancel goContext.CancelFunc, function func(string) error, getResponse func() string) func() error {
return self.wrappedConfirmationFunction(cancel, func() error {
return function(getResponse())
})
}
func (self *ConfirmationHelper) DeactivateConfirmationPrompt() {
self.c.Mutexes().PopupMutex.Lock()
self.c.State().GetRepoState().SetCurrentPopupOpts(nil)
self.c.Mutexes().PopupMutex.Unlock()
self.c.Views().Confirmation.Visible = false
self.c.Views().Suggestions.Visible = false
self.clearConfirmationViewKeyBindings()
}
func getMessageHeight(wrap bool, message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
// if we need to wrap, calculate height to fit content within view's width
if wrap {
for _, line := range lines {
lineCount += runewidth.StringWidth(line)/width + 1
}
} else {
lineCount = len(lines)
}
return lineCount
}
func (self *ConfirmationHelper) getPopupPanelDimensions(wrap bool, prompt string) (int, int, int, int) {
panelWidth := self.getPopupPanelWidth()
panelHeight := getMessageHeight(wrap, prompt, panelWidth)
return self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
}
func (self *ConfirmationHelper) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
return self.getPopupPanelDimensionsAux(panelWidth, contentHeight)
}
func (self *ConfirmationHelper) getPopupPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) {
width, height := self.c.GocuiGui().Size()
if panelHeight > height*3/4 {
panelHeight = height * 3 / 4
}
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
func (self *ConfirmationHelper) getPopupPanelWidth() int {
width, _ := self.c.GocuiGui().Size()
// we want a minimum width up to a point, then we do it based on ratio.
panelWidth := 4 * width / 7
minWidth := 80
if panelWidth < minWidth {
if width-2 < minWidth {
panelWidth = width - 2
} else {
panelWidth = minWidth
}
}
return panelWidth
}
func (self *ConfirmationHelper) prepareConfirmationPanel(
ctx goContext.Context,
opts types.ConfirmOpts,
) error {
self.c.Views().Confirmation.HasLoader = opts.HasLoader
if opts.HasLoader {
self.c.GocuiGui().StartTicking(ctx)
}
self.c.Views().Confirmation.Title = opts.Title
// for now we do not support wrapping in our editor
self.c.Views().Confirmation.Wrap = !opts.Editable
self.c.Views().Confirmation.FgColor = theme.GocuiDefaultTextColor
self.c.Views().Confirmation.Mask = runeForMask(opts.Mask)
_ = self.c.Views().Confirmation.SetOrigin(0, 0)
suggestionsContext := self.c.Contexts().Suggestions
suggestionsContext.State.FindSuggestions = opts.FindSuggestionsFunc
if opts.FindSuggestionsFunc != nil {
suggestionsView := self.c.Views().Suggestions
suggestionsView.Wrap = false
suggestionsView.FgColor = theme.GocuiDefaultTextColor
suggestionsContext.SetSuggestions(opts.FindSuggestionsFunc(""))
suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(self.c.Tr.SuggestionsTitle, self.c.UserConfig.Keybinding.Universal.TogglePanel)
}
self.ResizeConfirmationPanel()
return nil
}
func runeForMask(mask bool) rune {
if mask {
return '*'
}
return 0
}
func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts types.CreatePopupPanelOpts) error {
self.c.Mutexes().PopupMutex.Lock()
defer self.c.Mutexes().PopupMutex.Unlock()
ctx, cancel := goContext.WithCancel(ctx)
// we don't allow interruptions of non-loader popups in case we get stuck somehow
// e.g. a credentials popup never gets its required user input so a process hangs
// forever.
// The proper solution is to have a queue of popup options
currentPopupOpts := self.c.State().GetRepoState().GetCurrentPopupOpts()
if currentPopupOpts != nil && !currentPopupOpts.HasLoader {
self.c.Log.Error("ignoring create popup panel because a popup panel is already open")
cancel()
return nil
}
// remove any previous keybindings
self.clearConfirmationViewKeyBindings()
err := self.prepareConfirmationPanel(
ctx,
types.ConfirmOpts{
Title: opts.Title,
Prompt: opts.Prompt,
HasLoader: opts.HasLoader,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
Editable: opts.Editable,
Mask: opts.Mask,
})
if err != nil {
cancel()
return err
}
confirmationView := self.c.Views().Confirmation
confirmationView.Editable = opts.Editable
if opts.Editable {
textArea := confirmationView.TextArea
textArea.Clear()
textArea.TypeString(opts.Prompt)
self.ResizeConfirmationPanel()
confirmationView.RenderTextArea()
} else {
self.c.ResetViewOrigin(confirmationView)
self.c.SetViewContent(confirmationView, style.AttrBold.Sprint(opts.Prompt))
}
if err := self.setKeyBindings(cancel, opts); err != nil {
cancel()
return err
}
self.c.State().GetRepoState().SetCurrentPopupOpts(&opts)
return self.c.PushContext(self.c.Contexts().Confirmation)
}
func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts types.CreatePopupPanelOpts) error {
var onConfirm func() error
if opts.HandleConfirmPrompt != nil {
onConfirm = self.wrappedPromptConfirmationFunction(cancel, opts.HandleConfirmPrompt, func() string { return self.c.Views().Confirmation.TextArea.GetContent() })
} else {
onConfirm = self.wrappedConfirmationFunction(cancel, opts.HandleConfirm)
}
onSuggestionConfirm := self.wrappedPromptConfirmationFunction(
cancel,
opts.HandleConfirmPrompt,
self.getSelectedSuggestionValue,
)
onClose := self.wrappedConfirmationFunction(cancel, opts.HandleClose)
self.c.Contexts().Confirmation.State.OnConfirm = onConfirm
self.c.Contexts().Confirmation.State.OnClose = onClose
self.c.Contexts().Suggestions.State.OnConfirm = onSuggestionConfirm
self.c.Contexts().Suggestions.State.OnClose = onClose
return nil
}
func (self *ConfirmationHelper) clearConfirmationViewKeyBindings() {
noop := func() error { return nil }
self.c.Contexts().Confirmation.State.OnConfirm = noop
self.c.Contexts().Confirmation.State.OnClose = noop
self.c.Contexts().Suggestions.State.OnConfirm = noop
self.c.Contexts().Suggestions.State.OnClose = noop
}
func (self *ConfirmationHelper) getSelectedSuggestionValue() string {
selectedSuggestion := self.c.Contexts().Suggestions.GetSelected()
if selectedSuggestion != nil {
return selectedSuggestion.Value
}
return ""
}
func (self *ConfirmationHelper) ResizeConfirmationPanel() {
suggestionsViewHeight := 0
if self.c.Views().Suggestions.Visible {
suggestionsViewHeight = 11
}
panelWidth := self.getPopupPanelWidth()
prompt := self.c.Views().Confirmation.Buffer()
wrap := true
if self.c.Views().Confirmation.Editable {
prompt = self.c.Views().Confirmation.TextArea.GetContent()
wrap = false
}
panelHeight := getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
confirmationViewBottom := y1 - suggestionsViewHeight
_, _ = self.c.GocuiGui().SetView(self.c.Views().Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0)
suggestionsViewTop := confirmationViewBottom + 1
_, _ = self.c.GocuiGui().SetView(self.c.Views().Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0)
}
func (self *ConfirmationHelper) ResizeCurrentPopupPanel() error {
c := self.c.CurrentContext()
switch c {
case self.c.Contexts().Menu:
self.resizeMenu()
case self.c.Contexts().Confirmation, self.c.Contexts().Suggestions:
self.resizeConfirmationPanel()
case self.c.Contexts().CommitMessage, self.c.Contexts().CommitDescription:
self.ResizeCommitMessagePanels()
}
return nil
}
func (self *ConfirmationHelper) ResizePopupPanel(v *gocui.View, content string) error {
x0, y0, x1, y1 := self.getPopupPanelDimensions(v.Wrap, content)
_, err := self.c.GocuiGui().SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}
func (self *ConfirmationHelper) resizeMenu() {
itemCount := self.c.Contexts().Menu.GetList().Len()
offset := 3
panelWidth := self.getPopupPanelWidth()
x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset)
menuBottom := y1 - offset
_, _ = self.c.GocuiGui().SetView(self.c.Views().Menu.Name(), x0, y0, x1, menuBottom, 0)
tooltipTop := menuBottom + 1
tooltipHeight := getMessageHeight(true, self.c.Contexts().Menu.GetSelected().Tooltip, panelWidth) + 2 // plus 2 for the frame
_, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0)
}
func (self *ConfirmationHelper) resizeConfirmationPanel() {
suggestionsViewHeight := 0
if self.c.Views().Suggestions.Visible {
suggestionsViewHeight = 11
}
panelWidth := self.getPopupPanelWidth()
prompt := self.c.Views().Confirmation.Buffer()
wrap := true
if self.c.Views().Confirmation.Editable {
prompt = self.c.Views().Confirmation.TextArea.GetContent()
wrap = false
}
panelHeight := getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
confirmationViewBottom := y1 - suggestionsViewHeight
_, _ = self.c.GocuiGui().SetView(self.c.Views().Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0)
suggestionsViewTop := confirmationViewBottom + 1
_, _ = self.c.GocuiGui().SetView(self.c.Views().Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0)
}
func (self *ConfirmationHelper) ResizeCommitMessagePanels() {
panelWidth := self.getPopupPanelWidth()
content := self.c.Views().CommitDescription.TextArea.GetContent()
summaryViewHeight := 3
panelHeight := getMessageHeight(false, content, panelWidth)
minHeight := 7
if panelHeight < minHeight {
panelHeight = minHeight
}
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
_, _ = self.c.GocuiGui().SetView(self.c.Views().CommitMessage.Name(), x0, y0, x1, y0+summaryViewHeight-1, 0)
_, _ = self.c.GocuiGui().SetView(self.c.Views().CommitDescription.Name(), x0, y0+summaryViewHeight, x1, y1+summaryViewHeight, 0)
}
func (self *ConfirmationHelper) IsPopupPanel(viewName string) bool {
return viewName == "commitMessage" || viewName == "confirmation" || viewName == "menu"
}
func (self *ConfirmationHelper) IsPopupPanelFocused() bool {
return self.IsPopupPanel(self.c.CurrentContext().GetViewName())
}

View File

@ -8,11 +8,11 @@ import (
) )
type CredentialsHelper struct { type CredentialsHelper struct {
c *types.HelperCommon c *HelperCommon
} }
func NewCredentialsHelper( func NewCredentialsHelper(
c *types.HelperCommon, c *HelperCommon,
) *CredentialsHelper { ) *CredentialsHelper {
return &CredentialsHelper{ return &CredentialsHelper{
c: c, c: c,

View File

@ -0,0 +1,114 @@
package helpers
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
type DiffHelper struct {
c *HelperCommon
}
func NewDiffHelper(c *HelperCommon) *DiffHelper {
return &DiffHelper{
c: c,
}
}
func (self *DiffHelper) DiffStr() string {
output := self.c.Modes().Diffing.Ref
right := self.currentDiffTerminal()
if right != "" {
output += " " + right
}
if self.c.Modes().Diffing.Reverse {
output += " -R"
}
if self.c.State().GetIgnoreWhitespaceInDiffView() {
output += " --ignore-all-space"
}
file := self.currentlySelectedFilename()
if file != "" {
output += " -- " + file
} else if self.c.Modes().Filtering.Active() {
output += " -- " + self.c.Modes().Filtering.GetPath()
}
return output
}
func (self *DiffHelper) ExitDiffMode() error {
self.c.Modes().Diffing = diffing.New()
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}
func (self *DiffHelper) RenderDiff() error {
cmdObj := self.c.OS().Cmd.New(
fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", self.DiffStr()),
)
task := types.NewRunPtyTask(cmdObj.GetCmd())
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Diff",
Task: task,
},
})
}
// CurrentDiffTerminals returns the current diff terminals of the currently selected item.
// in the case of a branch it returns both the branch and it's upstream name,
// which becomes an option when you bring up the diff menu, but when you're just
// flicking through branches it will be using the local branch name.
func (self *DiffHelper) CurrentDiffTerminals() []string {
c := self.c.CurrentSideContext()
if c.GetKey() == "" {
return nil
}
switch v := c.(type) {
case types.DiffableContext:
return v.GetDiffTerminals()
}
return nil
}
func (self *DiffHelper) currentDiffTerminal() string {
names := self.CurrentDiffTerminals()
if len(names) == 0 {
return ""
}
return names[0]
}
func (self *DiffHelper) currentlySelectedFilename() string {
currentContext := self.c.CurrentContext()
switch currentContext := currentContext.(type) {
case types.IListContext:
if lo.Contains([]types.ContextKey{context.FILES_CONTEXT_KEY, context.COMMIT_FILES_CONTEXT_KEY}, currentContext.GetKey()) {
return currentContext.GetSelectedItemId()
}
}
return ""
}
func (self *DiffHelper) WithDiffModeCheck(f func() error) error {
if self.c.Modes().Diffing.Active() {
return self.RenderDiff()
}
return f()
}

View File

@ -1,11 +1,5 @@
package helpers package helpers
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type IFilesHelper interface { type IFilesHelper interface {
EditFile(filename string) error EditFile(filename string) error
EditFileAtLine(filename string, lineNumber int) error EditFileAtLine(filename string, lineNumber int) error
@ -13,37 +7,29 @@ type IFilesHelper interface {
} }
type FilesHelper struct { type FilesHelper struct {
c *types.HelperCommon c *HelperCommon
git *commands.GitCommand
os *oscommands.OSCommand
} }
func NewFilesHelper( func NewFilesHelper(c *HelperCommon) *FilesHelper {
c *types.HelperCommon,
git *commands.GitCommand,
os *oscommands.OSCommand,
) *FilesHelper {
return &FilesHelper{ return &FilesHelper{
c: c, c: c,
git: git,
os: os,
} }
} }
var _ IFilesHelper = &FilesHelper{} var _ IFilesHelper = &FilesHelper{}
func (self *FilesHelper) EditFile(filename string) error { func (self *FilesHelper) EditFile(filename string) error {
cmdStr, editInTerminal := self.git.File.GetEditCmdStr(filename) cmdStr, editInTerminal := self.c.Git().File.GetEditCmdStr(filename)
return self.callEditor(cmdStr, editInTerminal) return self.callEditor(cmdStr, editInTerminal)
} }
func (self *FilesHelper) EditFileAtLine(filename string, lineNumber int) error { func (self *FilesHelper) EditFileAtLine(filename string, lineNumber int) error {
cmdStr, editInTerminal := self.git.File.GetEditAtLineCmdStr(filename, lineNumber) cmdStr, editInTerminal := self.c.Git().File.GetEditAtLineCmdStr(filename, lineNumber)
return self.callEditor(cmdStr, editInTerminal) return self.callEditor(cmdStr, editInTerminal)
} }
func (self *FilesHelper) EditFileAtLineAndWait(filename string, lineNumber int) error { func (self *FilesHelper) EditFileAtLineAndWait(filename string, lineNumber int) error {
cmdStr := self.git.File.GetEditAtLineAndWaitCmdStr(filename, lineNumber) cmdStr := self.c.Git().File.GetEditAtLineAndWaitCmdStr(filename, lineNumber)
// Always suspend, regardless of the value of the editInTerminal config, // Always suspend, regardless of the value of the editInTerminal config,
// since we want to prevent interacting with the UI until the editor // since we want to prevent interacting with the UI until the editor
@ -54,16 +40,16 @@ func (self *FilesHelper) EditFileAtLineAndWait(filename string, lineNumber int)
func (self *FilesHelper) callEditor(cmdStr string, editInTerminal bool) error { func (self *FilesHelper) callEditor(cmdStr string, editInTerminal bool) error {
if editInTerminal { if editInTerminal {
return self.c.RunSubprocessAndRefresh( return self.c.RunSubprocessAndRefresh(
self.os.Cmd.NewShell(cmdStr), self.c.OS().Cmd.NewShell(cmdStr),
) )
} }
return self.os.Cmd.NewShell(cmdStr).Run() return self.c.OS().Cmd.NewShell(cmdStr).Run()
} }
func (self *FilesHelper) OpenFile(filename string) error { func (self *FilesHelper) OpenFile(filename string) error {
self.c.LogAction(self.c.Tr.Actions.OpenFile) self.c.LogAction(self.c.Tr.Actions.OpenFile)
if err := self.os.OpenFile(filename); err != nil { if err := self.c.OS().OpenFile(filename); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return nil return nil

View File

@ -3,26 +3,17 @@ package helpers
import ( import (
"fmt" "fmt"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
type GpgHelper struct { type GpgHelper struct {
c *types.HelperCommon c *HelperCommon
os *oscommands.OSCommand
git *commands.GitCommand
} }
func NewGpgHelper( func NewGpgHelper(c *HelperCommon) *GpgHelper {
c *types.HelperCommon,
os *oscommands.OSCommand,
git *commands.GitCommand,
) *GpgHelper {
return &GpgHelper{ return &GpgHelper{
c: c, c: c,
os: os,
git: git,
} }
} }
@ -32,9 +23,9 @@ func NewGpgHelper(
// we don't need to see a loading status if we're in a subprocess. // we don't need to see a loading status if we're in a subprocess.
// TODO: we shouldn't need to use a shell here, but looks like that NewShell function contains some windows specific quoting stuff. We should centralise that. // TODO: we shouldn't need to use a shell here, but looks like that NewShell function contains some windows specific quoting stuff. We should centralise that.
func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error { func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
useSubprocess := self.git.Config.UsingGpg() useSubprocess := self.c.Git().Config.UsingGpg()
if useSubprocess { if useSubprocess {
success, err := self.c.RunSubprocess(self.os.Cmd.NewShell(cmdObj.ToString())) success, err := self.c.RunSubprocess(self.c.OS().Cmd.NewShell(cmdObj.ToString()))
if success && onSuccess != nil { if success && onSuccess != nil {
if err := onSuccess(); err != nil { if err := onSuccess(); err != nil {
return err return err
@ -51,7 +42,7 @@ func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus
} }
func (self *GpgHelper) runAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error { func (self *GpgHelper) runAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
cmdObj = self.os.Cmd.NewShell(cmdObj.ToString()) cmdObj = self.c.OS().Cmd.NewShell(cmdObj.ToString())
return self.c.WithWaitingStatus(waitingStatus, func() error { return self.c.WithWaitingStatus(waitingStatus, func() error {
if err := cmdObj.StreamOutput().Run(); err != nil { if err := cmdObj.StreamOutput().Run(); err != nil {

View File

@ -1,5 +1,21 @@
package helpers package helpers
import (
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type HelperCommon struct {
*common.Common
types.IGuiCommon
IGetContexts
}
type IGetContexts interface {
Contexts() *context.ContextTree
}
type Helpers struct { type Helpers struct {
Refs *RefsHelper Refs *RefsHelper
Bisect *BisectHelper Bisect *BisectHelper
@ -12,28 +28,55 @@ type Helpers struct {
CherryPick *CherryPickHelper CherryPick *CherryPickHelper
Host *HostHelper Host *HostHelper
PatchBuilding *PatchBuildingHelper PatchBuilding *PatchBuildingHelper
Staging *StagingHelper
GPG *GpgHelper GPG *GpgHelper
Upstream *UpstreamHelper Upstream *UpstreamHelper
AmendHelper *AmendHelper AmendHelper *AmendHelper
Commits *CommitsHelper Commits *CommitsHelper
Snake *SnakeHelper
// lives in context package because our contexts need it to render to main
Diff *DiffHelper
Repos *ReposHelper
RecordDirectory *RecordDirectoryHelper
Update *UpdateHelper
Window *WindowHelper
View *ViewHelper
Refresh *RefreshHelper
Confirmation *ConfirmationHelper
Mode *ModeHelper
AppStatus *AppStatusHelper
WindowArrangement *WindowArrangementHelper
} }
func NewStubHelpers() *Helpers { func NewStubHelpers() *Helpers {
return &Helpers{ return &Helpers{
Refs: &RefsHelper{}, Refs: &RefsHelper{},
Bisect: &BisectHelper{}, Bisect: &BisectHelper{},
Suggestions: &SuggestionsHelper{}, Suggestions: &SuggestionsHelper{},
Files: &FilesHelper{}, Files: &FilesHelper{},
WorkingTree: &WorkingTreeHelper{}, WorkingTree: &WorkingTreeHelper{},
Tags: &TagsHelper{}, Tags: &TagsHelper{},
MergeAndRebase: &MergeAndRebaseHelper{}, MergeAndRebase: &MergeAndRebaseHelper{},
MergeConflicts: &MergeConflictsHelper{}, MergeConflicts: &MergeConflictsHelper{},
CherryPick: &CherryPickHelper{}, CherryPick: &CherryPickHelper{},
Host: &HostHelper{}, Host: &HostHelper{},
PatchBuilding: &PatchBuildingHelper{}, PatchBuilding: &PatchBuildingHelper{},
GPG: &GpgHelper{}, Staging: &StagingHelper{},
Upstream: &UpstreamHelper{}, GPG: &GpgHelper{},
AmendHelper: &AmendHelper{}, Upstream: &UpstreamHelper{},
Commits: &CommitsHelper{}, AmendHelper: &AmendHelper{},
Commits: &CommitsHelper{},
Snake: &SnakeHelper{},
Diff: &DiffHelper{},
Repos: &ReposHelper{},
RecordDirectory: &RecordDirectoryHelper{},
Update: &UpdateHelper{},
Window: &WindowHelper{},
View: &ViewHelper{},
Refresh: &RefreshHelper{},
Confirmation: &ConfirmationHelper{},
Mode: &ModeHelper{},
AppStatus: &AppStatusHelper{},
WindowArrangement: &WindowArrangementHelper{},
} }
} }

View File

@ -1,9 +1,7 @@
package helpers package helpers
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service" "github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
"github.com/jesseduffield/lazygit/pkg/gui/types"
) )
// this helper just wraps our hosting_service package // this helper just wraps our hosting_service package
@ -14,17 +12,14 @@ type IHostHelper interface {
} }
type HostHelper struct { type HostHelper struct {
c *types.HelperCommon c *HelperCommon
git *commands.GitCommand
} }
func NewHostHelper( func NewHostHelper(
c *types.HelperCommon, c *HelperCommon,
git *commands.GitCommand,
) *HostHelper { ) *HostHelper {
return &HostHelper{ return &HostHelper{
c: c, c: c,
git: git,
} }
} }
@ -40,7 +35,7 @@ func (self *HostHelper) GetCommitURL(commitSha string) (string, error) {
// from one invocation to the next. Note however that we're currently caching config // 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. // results so we might want to invalidate the cache here if it becomes a problem.
func (self *HostHelper) getHostingServiceMgr() *hosting_service.HostingServiceMgr { func (self *HostHelper) getHostingServiceMgr() *hosting_service.HostingServiceMgr {
remoteUrl := self.git.Config.GetRemoteURL() remoteUrl := self.c.Git().Config.GetRemoteURL()
configServices := self.c.UserConfig.Services configServices := self.c.UserConfig.Services
return hosting_service.NewHostingServiceMgr(self.c.Log, self.c.Tr, remoteUrl, configServices) return hosting_service.NewHostingServiceMgr(self.c.Log, self.c.Tr, remoteUrl, configServices)
} }

View File

@ -5,31 +5,23 @@ import (
"strings" "strings"
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
type MergeAndRebaseHelper struct { type MergeAndRebaseHelper struct {
c *types.HelperCommon c *HelperCommon
contexts *context.ContextTree
git *commands.GitCommand
refsHelper *RefsHelper refsHelper *RefsHelper
} }
func NewMergeAndRebaseHelper( func NewMergeAndRebaseHelper(
c *types.HelperCommon, c *HelperCommon,
contexts *context.ContextTree,
git *commands.GitCommand,
refsHelper *RefsHelper, refsHelper *RefsHelper,
) *MergeAndRebaseHelper { ) *MergeAndRebaseHelper {
return &MergeAndRebaseHelper{ return &MergeAndRebaseHelper{
c: c, c: c,
contexts: contexts,
git: git,
refsHelper: refsHelper, refsHelper: refsHelper,
} }
} }
@ -53,7 +45,7 @@ func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error {
{option: REBASE_OPTION_ABORT, key: 'a'}, {option: REBASE_OPTION_ABORT, key: 'a'},
} }
if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
options = append(options, optionAndKey{ options = append(options, optionAndKey{
option: REBASE_OPTION_SKIP, key: 's', option: REBASE_OPTION_SKIP, key: 's',
}) })
@ -70,7 +62,7 @@ func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error {
}) })
var title string var title string
if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_MERGING { if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_MERGING {
title = self.c.Tr.MergeOptionsTitle title = self.c.Tr.MergeOptionsTitle
} else { } else {
title = self.c.Tr.RebaseOptionsTitle title = self.c.Tr.RebaseOptionsTitle
@ -80,7 +72,7 @@ func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error {
} }
func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error { func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
status := self.git.Status.WorkingTreeState() status := self.c.Git().Status.WorkingTreeState()
if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING { if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING {
return self.c.ErrorMsg(self.c.Tr.NotMergingOrRebasing) return self.c.ErrorMsg(self.c.Tr.NotMergingOrRebasing)
@ -104,10 +96,10 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
if status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && self.c.UserConfig.Git.Merging.ManualCommit { if status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && self.c.UserConfig.Git.Merging.ManualCommit {
// TODO: see if we should be calling more of the code from self.Git.Rebase.GenericMergeOrRebaseAction // TODO: see if we should be calling more of the code from self.Git.Rebase.GenericMergeOrRebaseAction
return self.c.RunSubprocessAndRefresh( return self.c.RunSubprocessAndRefresh(
self.git.Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command), self.c.Git().Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command),
) )
} }
result := self.git.Rebase.GenericMergeOrRebaseAction(commandType, command) result := self.c.Git().Rebase.GenericMergeOrRebaseAction(commandType, command)
if err := self.CheckMergeOrRebase(result); err != nil { if err := self.CheckMergeOrRebase(result); err != nil {
return err return err
} }
@ -150,7 +142,7 @@ func (self *MergeAndRebaseHelper) CheckMergeOrRebase(result error) error {
Title: self.c.Tr.FoundConflictsTitle, Title: self.c.Tr.FoundConflictsTitle,
Prompt: self.c.Tr.FoundConflicts, Prompt: self.c.Tr.FoundConflicts,
HandleConfirm: func() error { HandleConfirm: func() error {
return self.c.PushContext(self.contexts.Files) return self.c.PushContext(self.c.Contexts().Files)
}, },
HandleClose: func() error { HandleClose: func() error {
return self.genericMergeCommand(REBASE_OPTION_ABORT) return self.genericMergeCommand(REBASE_OPTION_ABORT)
@ -174,7 +166,7 @@ func (self *MergeAndRebaseHelper) AbortMergeOrRebaseWithConfirm() error {
} }
func (self *MergeAndRebaseHelper) workingTreeStateNoun() string { func (self *MergeAndRebaseHelper) workingTreeStateNoun() string {
workingTreeState := self.git.Status.WorkingTreeState() workingTreeState := self.c.Git().Status.WorkingTreeState()
switch workingTreeState { switch workingTreeState {
case enums.REBASE_MODE_NONE: case enums.REBASE_MODE_NONE:
return "" return ""
@ -207,7 +199,7 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
Key: 's', Key: 's',
OnPress: func() error { OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch) self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
err := self.git.Rebase.RebaseBranch(ref) err := self.c.Git().Rebase.RebaseBranch(ref)
return self.CheckMergeOrRebase(err) return self.CheckMergeOrRebase(err)
}, },
}, },
@ -217,11 +209,11 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
Tooltip: self.c.Tr.InteractiveRebaseTooltip, Tooltip: self.c.Tr.InteractiveRebaseTooltip,
OnPress: func() error { OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch) self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
err := self.git.Rebase.EditRebase(ref) err := self.c.Git().Rebase.EditRebase(ref)
if err = self.CheckMergeOrRebase(err); err != nil { if err = self.CheckMergeOrRebase(err); err != nil {
return err return err
} }
return self.c.PushContext(self.contexts.LocalCommits) return self.c.PushContext(self.c.Contexts().LocalCommits)
}, },
}, },
} }
@ -241,7 +233,7 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
} }
func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) error { func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) error {
if self.git.Branch.IsHeadDetached() { if self.c.Git().Branch.IsHeadDetached() {
return self.c.ErrorMsg("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on") return self.c.ErrorMsg("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
} }
checkedOutBranchName := self.refsHelper.GetCheckedOutRef().Name checkedOutBranchName := self.refsHelper.GetCheckedOutRef().Name
@ -261,7 +253,7 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e
Prompt: prompt, Prompt: prompt,
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.Merge) self.c.LogAction(self.c.Tr.Actions.Merge)
err := self.git.Branch.Merge(refName, git_commands.MergeOpts{}) err := self.c.Git().Branch.Merge(refName, git_commands.MergeOpts{})
return self.CheckMergeOrRebase(err) return self.CheckMergeOrRebase(err)
}, },
}) })

View File

@ -1,41 +1,19 @@
package helpers package helpers
import ( import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
type MergeConflictsHelper struct { type MergeConflictsHelper struct {
c *types.HelperCommon c *HelperCommon
contexts *context.ContextTree
git *commands.GitCommand
} }
func NewMergeConflictsHelper( func NewMergeConflictsHelper(
c *types.HelperCommon, c *HelperCommon,
contexts *context.ContextTree,
git *commands.GitCommand,
) *MergeConflictsHelper { ) *MergeConflictsHelper {
return &MergeConflictsHelper{ return &MergeConflictsHelper{
c: c, c: c,
contexts: contexts,
git: git,
}
}
func (self *MergeConflictsHelper) GetMergingOptions() map[string]string {
keybindingConfig := self.c.UserConfig.Keybinding
return map[string]string{
fmt.Sprintf("%s %s", keybindings.Label(keybindingConfig.Universal.PrevItem), keybindings.Label(keybindingConfig.Universal.NextItem)): self.c.Tr.LcSelectHunk,
fmt.Sprintf("%s %s", keybindings.Label(keybindingConfig.Universal.PrevBlock), keybindings.Label(keybindingConfig.Universal.NextBlock)): self.c.Tr.LcNavigateConflicts,
keybindings.Label(keybindingConfig.Universal.Select): self.c.Tr.LcPickHunk,
keybindings.Label(keybindingConfig.Main.PickBothHunks): self.c.Tr.LcPickAllHunks,
keybindings.Label(keybindingConfig.Universal.Undo): self.c.Tr.LcUndo,
} }
} }
@ -47,7 +25,7 @@ func (self *MergeConflictsHelper) SetMergeState(path string) (bool, error) {
} }
func (self *MergeConflictsHelper) setMergeStateWithoutLock(path string) (bool, error) { func (self *MergeConflictsHelper) setMergeStateWithoutLock(path string) (bool, error) {
content, err := self.git.File.Cat(path) content, err := self.c.Git().File.Cat(path)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -78,7 +56,7 @@ func (self *MergeConflictsHelper) EscapeMerge() error {
// doing this in separate UI thread so that we're not still holding the lock by the time refresh the file // doing this in separate UI thread so that we're not still holding the lock by the time refresh the file
self.c.OnUIThread(func() error { self.c.OnUIThread(func() error {
return self.c.PushContext(self.contexts.Files) return self.c.PushContext(self.c.Contexts().Files)
}) })
return nil return nil
} }
@ -107,9 +85,48 @@ func (self *MergeConflictsHelper) SwitchToMerge(path string) error {
} }
} }
return self.c.PushContext(self.contexts.MergeConflicts) return self.c.PushContext(self.c.Contexts().MergeConflicts)
} }
func (self *MergeConflictsHelper) context() *context.MergeConflictsContext { func (self *MergeConflictsHelper) context() *context.MergeConflictsContext {
return self.contexts.MergeConflicts return self.c.Contexts().MergeConflicts
}
func (self *MergeConflictsHelper) Render(isFocused bool) error {
content := self.context().GetContentToRender(isFocused)
var task types.UpdateTask
if self.context().IsUserScrolling() {
task = types.NewRenderStringWithoutScrollTask(content)
} else {
originY := self.context().GetOriginY()
task = types.NewRenderStringWithScrollTask(content, 0, originY)
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().MergeConflicts,
Main: &types.ViewUpdateOpts{
Task: task,
},
})
}
func (self *MergeConflictsHelper) RefreshMergeState() error {
self.c.Contexts().MergeConflicts.GetMutex().Lock()
defer self.c.Contexts().MergeConflicts.GetMutex().Unlock()
if self.c.CurrentContext().GetKey() != context.MERGE_CONFLICTS_CONTEXT_KEY {
return nil
}
hasConflicts, err := self.SetConflictsAndRender(self.c.Contexts().MergeConflicts.GetState().GetPath(), true)
if err != nil {
return self.c.Error(err)
}
if !hasConflicts {
return self.EscapeMerge()
}
return nil
} }

View File

@ -0,0 +1,159 @@
package helpers
import (
"fmt"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ModeHelper struct {
c *HelperCommon
diffHelper *DiffHelper
patchBuildingHelper *PatchBuildingHelper
cherryPickHelper *CherryPickHelper
mergeAndRebaseHelper *MergeAndRebaseHelper
bisectHelper *BisectHelper
}
func NewModeHelper(
c *HelperCommon,
diffHelper *DiffHelper,
patchBuildingHelper *PatchBuildingHelper,
cherryPickHelper *CherryPickHelper,
mergeAndRebaseHelper *MergeAndRebaseHelper,
bisectHelper *BisectHelper,
) *ModeHelper {
return &ModeHelper{
c: c,
diffHelper: diffHelper,
patchBuildingHelper: patchBuildingHelper,
cherryPickHelper: cherryPickHelper,
mergeAndRebaseHelper: mergeAndRebaseHelper,
bisectHelper: bisectHelper,
}
}
type ModeStatus struct {
IsActive func() bool
Description func() string
Reset func() error
}
func (self *ModeHelper) Statuses() []ModeStatus {
return []ModeStatus{
{
IsActive: self.c.Modes().Diffing.Active,
Description: func() string {
return self.withResetButton(
fmt.Sprintf(
"%s %s",
self.c.Tr.LcShowingGitDiff,
"git diff "+self.diffHelper.DiffStr(),
),
style.FgMagenta,
)
},
Reset: self.diffHelper.ExitDiffMode,
},
{
IsActive: self.c.Git().Patch.PatchBuilder.Active,
Description: func() string {
return self.withResetButton(self.c.Tr.LcBuildingPatch, style.FgYellow.SetBold())
},
Reset: self.patchBuildingHelper.Reset,
},
{
IsActive: self.c.Modes().Filtering.Active,
Description: func() string {
return self.withResetButton(
fmt.Sprintf(
"%s '%s'",
self.c.Tr.LcFilteringBy,
self.c.Modes().Filtering.GetPath(),
),
style.FgRed,
)
},
Reset: self.ExitFilterMode,
},
{
IsActive: self.c.Modes().CherryPicking.Active,
Description: func() string {
copiedCount := len(self.c.Modes().CherryPicking.CherryPickedCommits)
text := self.c.Tr.LcCommitsCopied
if copiedCount == 1 {
text = self.c.Tr.LcCommitCopied
}
return self.withResetButton(
fmt.Sprintf(
"%d %s",
copiedCount,
text,
),
style.FgCyan,
)
},
Reset: self.cherryPickHelper.Reset,
},
{
IsActive: func() bool {
return self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE
},
Description: func() string {
workingTreeState := self.c.Git().Status.WorkingTreeState()
return self.withResetButton(
presentation.FormatWorkingTreeState(workingTreeState), style.FgYellow,
)
},
Reset: self.mergeAndRebaseHelper.AbortMergeOrRebaseWithConfirm,
},
{
IsActive: func() bool {
return self.c.Model().BisectInfo.Started()
},
Description: func() string {
return self.withResetButton("bisecting", style.FgGreen)
},
Reset: self.bisectHelper.Reset,
},
}
}
func (self *ModeHelper) withResetButton(content string, textStyle style.TextStyle) string {
return textStyle.Sprintf(
"%s %s",
content,
style.AttrUnderline.Sprint(self.c.Tr.ResetInParentheses),
)
}
func (self *ModeHelper) GetActiveMode() (ModeStatus, bool) {
return slices.Find(self.Statuses(), func(mode ModeStatus) bool {
return mode.IsActive()
})
}
func (self *ModeHelper) IsAnyModeActive() bool {
return slices.Some(self.Statuses(), func(mode ModeStatus) bool {
return mode.IsActive()
})
}
func (self *ModeHelper) ExitFilterMode() error {
return self.ClearFiltering()
}
func (self *ModeHelper) ClearFiltering() error {
self.c.Modes().Filtering.Reset()
if self.c.State().GetRepoState().GetScreenMode() == types.SCREEN_HALF {
self.c.State().GetRepoState().SetScreenMode(types.SCREEN_NORMAL)
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}})
}

View File

@ -1,9 +1,8 @@
package helpers package helpers
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -12,25 +11,19 @@ type IPatchBuildingHelper interface {
} }
type PatchBuildingHelper struct { type PatchBuildingHelper struct {
c *types.HelperCommon c *HelperCommon
git *commands.GitCommand
contexts *context.ContextTree
} }
func NewPatchBuildingHelper( func NewPatchBuildingHelper(
c *types.HelperCommon, c *HelperCommon,
git *commands.GitCommand,
contexts *context.ContextTree,
) *PatchBuildingHelper { ) *PatchBuildingHelper {
return &PatchBuildingHelper{ return &PatchBuildingHelper{
c: c, c: c,
git: git,
contexts: contexts,
} }
} }
func (self *PatchBuildingHelper) ValidateNormalWorkingTreeState() (bool, error) { func (self *PatchBuildingHelper) ValidateNormalWorkingTreeState() (bool, error) {
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return false, self.c.ErrorMsg(self.c.Tr.CantPatchWhileRebasingError) return false, self.c.ErrorMsg(self.c.Tr.CantPatchWhileRebasingError)
} }
return true, nil return true, nil
@ -43,7 +36,7 @@ func (self *PatchBuildingHelper) Escape() error {
// kills the custom patch and returns us back to the commit files panel if needed // kills the custom patch and returns us back to the commit files panel if needed
func (self *PatchBuildingHelper) Reset() error { func (self *PatchBuildingHelper) Reset() error {
self.git.Patch.PatchBuilder.Reset() self.c.Git().Patch.PatchBuilder.Reset()
if self.c.CurrentStaticContext().GetKind() != types.SIDE_CONTEXT { if self.c.CurrentStaticContext().GetKind() != types.SIDE_CONTEXT {
if err := self.Escape(); err != nil { if err := self.Escape(); err != nil {
@ -60,3 +53,59 @@ func (self *PatchBuildingHelper) Reset() error {
// refreshing the current context so that the secondary panel is hidden if necessary. // refreshing the current context so that the secondary panel is hidden if necessary.
return self.c.PostRefreshUpdate(self.c.CurrentContext()) return self.c.PostRefreshUpdate(self.c.CurrentContext())
} }
func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpts) error {
selectedLineIdx := -1
if opts.ClickedWindowName == "main" {
selectedLineIdx = opts.ClickedViewLineIdx
}
if !self.c.Git().Patch.PatchBuilder.Active() {
return self.Escape()
}
// get diff from commit file that's currently selected
path := self.c.Contexts().CommitFiles.GetSelectedPath()
if path == "" {
return nil
}
ref := self.c.Contexts().CommitFiles.CommitFileTreeViewModel.GetRef()
to := ref.RefName()
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
diff, err := self.c.Git().WorkingTree.ShowFileDiff(from, to, reverse, path, true, self.c.State().GetIgnoreWhitespaceInDiffView())
if err != nil {
return err
}
secondaryDiff := self.c.Git().Patch.PatchBuilder.RenderPatchForFile(path, false, false)
if err != nil {
return err
}
context := self.c.Contexts().CustomPatchBuilder
oldState := context.GetState()
state := patch_exploring.NewState(diff, selectedLineIdx, oldState, self.c.Log)
context.SetState(state)
if state == nil {
return self.Escape()
}
mainContent := context.GetContentToRender(true)
self.c.Contexts().CustomPatchBuilder.FocusSelection()
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().PatchBuilding,
Main: &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(mainContent),
Title: self.c.Tr.Patch,
},
Secondary: &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(secondaryDiff),
Title: self.c.Tr.CustomPatch,
},
})
}

View File

@ -0,0 +1,36 @@
package helpers
import (
"os"
)
type RecordDirectoryHelper struct {
c *HelperCommon
}
func NewRecordDirectoryHelper(c *HelperCommon) *RecordDirectoryHelper {
return &RecordDirectoryHelper{
c: c,
}
}
// when a user runs lazygit with the LAZYGIT_NEW_DIR_FILE env variable defined
// we will write the current directory to that file on exit so that their
// shell can then change to that directory. That means you don't get kicked
// back to the directory that you started with.
func (self *RecordDirectoryHelper) RecordCurrentDirectory() error {
// determine current directory, set it in LAZYGIT_NEW_DIR_FILE
dirName, err := os.Getwd()
if err != nil {
return err
}
return self.RecordDirectory(dirName)
}
func (self *RecordDirectoryHelper) RecordDirectory(dirName string) error {
newDirFilePath := os.Getenv("LAZYGIT_NEW_DIR_FILE")
if newDirFilePath == "" {
return nil
}
return self.c.OS().CreateFileWithContent(newDirFilePath, dirName)
}

View File

@ -0,0 +1,611 @@
package helpers
import (
"fmt"
"strings"
"sync"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"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"
)
type RefreshHelper struct {
c *HelperCommon
refsHelper *RefsHelper
mergeAndRebaseHelper *MergeAndRebaseHelper
patchBuildingHelper *PatchBuildingHelper
stagingHelper *StagingHelper
mergeConflictsHelper *MergeConflictsHelper
fileWatcher types.IFileWatcher
}
func NewRefreshHelper(
c *HelperCommon,
refsHelper *RefsHelper,
mergeAndRebaseHelper *MergeAndRebaseHelper,
patchBuildingHelper *PatchBuildingHelper,
stagingHelper *StagingHelper,
mergeConflictsHelper *MergeConflictsHelper,
fileWatcher types.IFileWatcher,
) *RefreshHelper {
return &RefreshHelper{
c: c,
refsHelper: refsHelper,
mergeAndRebaseHelper: mergeAndRebaseHelper,
patchBuildingHelper: patchBuildingHelper,
stagingHelper: stagingHelper,
mergeConflictsHelper: mergeConflictsHelper,
fileWatcher: fileWatcher,
}
}
func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
if options.Scope == nil {
self.c.Log.Infof(
"refreshing all scopes in %s mode",
getModeName(options.Mode),
)
} else {
self.c.Log.Infof(
"refreshing the following scopes in %s mode: %s",
getModeName(options.Mode),
strings.Join(getScopeNames(options.Scope), ","),
)
}
wg := sync.WaitGroup{}
f := func() {
var scopeSet *set.Set[types.RefreshableView]
if len(options.Scope) == 0 {
// not refreshing staging/patch-building unless explicitly requested because we only need
// to refresh those while focused.
scopeSet = set.NewFromSlice([]types.RefreshableView{
types.COMMITS,
types.BRANCHES,
types.FILES,
types.STASH,
types.REFLOG,
types.TAGS,
types.REMOTES,
types.STATUS,
types.BISECT_INFO,
types.STAGING,
})
} else {
scopeSet = set.NewFromSlice(options.Scope)
}
refresh := func(f func()) {
wg.Add(1)
func() {
if options.Mode == types.ASYNC {
go utils.Safe(f)
} else {
f()
}
wg.Done()
}()
}
if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) {
refresh(self.refreshCommits)
} else if scopeSet.Includes(types.REBASE_COMMITS) {
// the above block handles rebase commits so we only need to call this one
// if we've asked specifically for rebase commits and not those other things
refresh(func() { _ = self.refreshRebaseCommits() })
}
if scopeSet.Includes(types.SUB_COMMITS) {
refresh(func() { _ = self.refreshSubCommitsWithLimit() })
}
// reason we're not doing this if the COMMITS type is included is that if the COMMITS type _is_ included we will refresh the commit files context anyway
if scopeSet.Includes(types.COMMIT_FILES) && !scopeSet.Includes(types.COMMITS) {
refresh(func() { _ = self.refreshCommitFilesContext() })
}
if scopeSet.Includes(types.FILES) || scopeSet.Includes(types.SUBMODULES) {
refresh(func() { _ = self.refreshFilesAndSubmodules() })
}
if scopeSet.Includes(types.STASH) {
refresh(func() { _ = self.refreshStashEntries() })
}
if scopeSet.Includes(types.TAGS) {
refresh(func() { _ = self.refreshTags() })
}
if scopeSet.Includes(types.REMOTES) {
refresh(func() { _ = self.refreshRemotes() })
}
if scopeSet.Includes(types.STAGING) {
refresh(func() { _ = self.stagingHelper.RefreshStagingPanel(types.OnFocusOpts{}) })
}
if scopeSet.Includes(types.PATCH_BUILDING) {
refresh(func() { _ = self.patchBuildingHelper.RefreshPatchBuildingPanel(types.OnFocusOpts{}) })
}
if scopeSet.Includes(types.MERGE_CONFLICTS) || scopeSet.Includes(types.FILES) {
refresh(func() { _ = self.mergeConflictsHelper.RefreshMergeState() })
}
wg.Wait()
self.refreshStatus()
if options.Then != nil {
options.Then()
}
}
if options.Mode == types.BLOCK_UI {
self.c.OnUIThread(func() error {
f()
return nil
})
} else {
f()
}
return nil
}
func getScopeNames(scopes []types.RefreshableView) []string {
scopeNameMap := map[types.RefreshableView]string{
types.COMMITS: "commits",
types.BRANCHES: "branches",
types.FILES: "files",
types.SUBMODULES: "submodules",
types.SUB_COMMITS: "subCommits",
types.STASH: "stash",
types.REFLOG: "reflog",
types.TAGS: "tags",
types.REMOTES: "remotes",
types.STATUS: "status",
types.BISECT_INFO: "bisect",
types.STAGING: "staging",
types.MERGE_CONFLICTS: "mergeConflicts",
}
return slices.Map(scopes, func(scope types.RefreshableView) string {
return scopeNameMap[scope]
})
}
func getModeName(mode types.RefreshMode) string {
switch mode {
case types.SYNC:
return "sync"
case types.ASYNC:
return "async"
case types.BLOCK_UI:
return "block-ui"
default:
return "unknown mode"
}
}
// during startup, the bottleneck is fetching the reflog entries. We need these
// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE.
// In the initial phase we don't get any reflog commits, but we asynchronously get them
// and refresh the branches after that
func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
switch self.c.State().GetRepoState().GetStartupStage() {
case types.INITIAL:
go utils.Safe(func() {
_ = self.refreshReflogCommits()
self.refreshBranches()
self.c.State().GetRepoState().SetStartupStage(types.COMPLETE)
})
case types.COMPLETE:
_ = self.refreshReflogCommits()
}
}
// whenever we change commits, we should update branches because the upstream/downstream
// counts can change. Whenever we change branches we should probably also change commits
// e.g. in the case of switching branches.
func (self *RefreshHelper) refreshCommits() {
wg := sync.WaitGroup{}
wg.Add(2)
go utils.Safe(func() {
self.refreshReflogCommitsConsideringStartup()
self.refreshBranches()
wg.Done()
})
go utils.Safe(func() {
_ = self.refreshCommitsWithLimit()
ctx, ok := self.c.Contexts().CommitFiles.GetParentContext()
if ok && ctx.GetKey() == context.LOCAL_COMMITS_CONTEXT_KEY {
// This makes sense when we've e.g. just amended a commit, meaning we get a new commit SHA at the same position.
// However if we've just added a brand new commit, it pushes the list down by one and so we would end up
// showing the contents of a different commit than the one we initially entered.
// Ideally we would know when to refresh the commit files context and when not to,
// or perhaps we could just pop that context off the stack whenever cycling windows.
// For now the awkwardness remains.
commit := self.c.Contexts().LocalCommits.GetSelected()
if commit != nil {
self.c.Contexts().CommitFiles.SetRef(commit)
self.c.Contexts().CommitFiles.SetTitleRef(commit.RefName())
_ = self.refreshCommitFilesContext()
}
}
wg.Done()
})
wg.Wait()
}
func (self *RefreshHelper) refreshCommitsWithLimit() error {
self.c.Mutexes().LocalCommitsMutex.Lock()
defer self.c.Mutexes().LocalCommitsMutex.Unlock()
commits, err := self.c.Git().Loaders.CommitLoader.GetCommits(
git_commands.GetCommitsOptions{
Limit: self.c.Contexts().LocalCommits.GetLimitCommits(),
FilterPath: self.c.Modes().Filtering.GetPath(),
IncludeRebaseCommits: true,
RefName: self.refForLog(),
All: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(),
},
)
if err != nil {
return err
}
self.c.Model().Commits = commits
self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState()
return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
}
func (self *RefreshHelper) refreshSubCommitsWithLimit() error {
self.c.Mutexes().SubCommitsMutex.Lock()
defer self.c.Mutexes().SubCommitsMutex.Unlock()
commits, err := self.c.Git().Loaders.CommitLoader.GetCommits(
git_commands.GetCommitsOptions{
Limit: self.c.Contexts().SubCommits.GetLimitCommits(),
FilterPath: self.c.Modes().Filtering.GetPath(),
IncludeRebaseCommits: false,
RefName: self.c.Contexts().SubCommits.GetRef().FullRefName(),
},
)
if err != nil {
return err
}
self.c.Model().SubCommits = commits
return self.c.PostRefreshUpdate(self.c.Contexts().SubCommits)
}
func (self *RefreshHelper) refreshCommitFilesContext() error {
ref := self.c.Contexts().CommitFiles.GetRef()
to := ref.RefName()
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
files, err := self.c.Git().Loaders.CommitFileLoader.GetFilesInDiff(from, to, reverse)
if err != nil {
return self.c.Error(err)
}
self.c.Model().CommitFiles = files
self.c.Contexts().CommitFiles.CommitFileTreeViewModel.SetTree()
return self.c.PostRefreshUpdate(self.c.Contexts().CommitFiles)
}
func (self *RefreshHelper) refreshRebaseCommits() error {
self.c.Mutexes().LocalCommitsMutex.Lock()
defer self.c.Mutexes().LocalCommitsMutex.Unlock()
updatedCommits, err := self.c.Git().Loaders.CommitLoader.MergeRebasingCommits(self.c.Model().Commits)
if err != nil {
return err
}
self.c.Model().Commits = updatedCommits
self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState()
return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
}
func (self *RefreshHelper) refreshTags() error {
tags, err := self.c.Git().Loaders.TagLoader.GetTags()
if err != nil {
return self.c.Error(err)
}
self.c.Model().Tags = tags
return self.c.PostRefreshUpdate(self.c.Contexts().Tags)
}
func (self *RefreshHelper) refreshStateSubmoduleConfigs() error {
configs, err := self.c.Git().Submodule.GetConfigs()
if err != nil {
return err
}
self.c.Model().Submodules = configs
return nil
}
// self.refreshStatus is called at the end of this because that's when we can
// be sure there is a State.Model.Branches array to pick the current branch from
func (self *RefreshHelper) refreshBranches() {
reflogCommits := self.c.Model().FilteredReflogCommits
if self.c.Modes().Filtering.Active() {
// in filter mode we filter our reflog commits to just those containing the path
// however we need all the reflog entries to populate the recencies of our branches
// which allows us to order them correctly. So if we're filtering we'll just
// manually load all the reflog commits here
var err error
reflogCommits, _, err = self.c.Git().Loaders.ReflogCommitLoader.GetReflogCommits(nil, "")
if err != nil {
self.c.Log.Error(err)
}
}
branches, err := self.c.Git().Loaders.BranchLoader.Load(reflogCommits)
if err != nil {
_ = self.c.Error(err)
}
self.c.Model().Branches = branches
if err := self.c.PostRefreshUpdate(self.c.Contexts().Branches); err != nil {
self.c.Log.Error(err)
}
self.refreshStatus()
}
func (self *RefreshHelper) refreshFilesAndSubmodules() error {
self.c.Mutexes().RefreshingFilesMutex.Lock()
self.c.State().SetIsRefreshingFiles(true)
defer func() {
self.c.State().SetIsRefreshingFiles(false)
self.c.Mutexes().RefreshingFilesMutex.Unlock()
}()
if err := self.refreshStateSubmoduleConfigs(); err != nil {
return err
}
if err := self.refreshStateFiles(); err != nil {
return err
}
self.c.OnUIThread(func() error {
if err := self.c.PostRefreshUpdate(self.c.Contexts().Submodules); err != nil {
self.c.Log.Error(err)
}
if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil {
self.c.Log.Error(err)
}
return nil
})
return nil
}
func (self *RefreshHelper) refreshStateFiles() error {
fileTreeViewModel := self.c.Contexts().Files.FileTreeViewModel
// If git thinks any of our files have inline merge conflicts, but they actually don't,
// we stage them.
// Note that if files with merge conflicts have both arisen and have been resolved
// between refreshes, we won't stage them here. This is super unlikely though,
// and this approach spares us from having to call `git status` twice in a row.
// Although this also means that at startup we won't be staging anything until
// we call git status again.
pathsToStage := []string{}
prevConflictFileCount := 0
for _, file := range self.c.Model().Files {
if file.HasMergeConflicts {
prevConflictFileCount++
}
if file.HasInlineMergeConflicts {
hasConflicts, err := mergeconflicts.FileHasConflictMarkers(file.Name)
if err != nil {
self.c.Log.Error(err)
} else if !hasConflicts {
pathsToStage = append(pathsToStage, file.Name)
}
}
}
if len(pathsToStage) > 0 {
self.c.LogAction(self.c.Tr.Actions.StageResolvedFiles)
if err := self.c.Git().WorkingTree.StageFiles(pathsToStage); err != nil {
return self.c.Error(err)
}
}
files := self.c.Git().Loaders.FileLoader.
GetStatusFiles(git_commands.GetStatusFileOptions{})
conflictFileCount := 0
for _, file := range files {
if file.HasMergeConflicts {
conflictFileCount++
}
}
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 {
self.c.OnUIThread(func() error { return self.mergeAndRebaseHelper.PromptToContinueRebase() })
}
fileTreeViewModel.RWMutex.Lock()
// only taking over the filter if it hasn't already been set by the user.
// Though this does make it impossible for the user to actually say they want to display all if
// conflicts are currently being shown. Hmm. Worth it I reckon. If we need to add some
// extra state here to see if the user's set the filter themselves we can do that, but
// I'd prefer to maintain as little state as possible.
if conflictFileCount > 0 {
if fileTreeViewModel.GetFilter() == filetree.DisplayAll {
fileTreeViewModel.SetFilter(filetree.DisplayConflicted)
}
} else if fileTreeViewModel.GetFilter() == filetree.DisplayConflicted {
fileTreeViewModel.SetFilter(filetree.DisplayAll)
}
self.c.Model().Files = files
fileTreeViewModel.SetTree()
fileTreeViewModel.RWMutex.Unlock()
if err := self.fileWatcher.AddFilesToFileWatcher(files); err != nil {
return err
}
return nil
}
// the reflogs panel is the only panel where we cache data, in that we only
// load entries that have been created since we last ran the call. This means
// we need to be more careful with how we use this, and to ensure we're emptying
// the reflogs array when changing contexts.
// This method also manages two things: ReflogCommits and FilteredReflogCommits.
// FilteredReflogCommits are rendered in the reflogs panel, and ReflogCommits
// are used by the branches panel to obtain recency values for sorting.
func (self *RefreshHelper) refreshReflogCommits() error {
// pulling state into its own variable incase it gets swapped out for another state
// and we get an out of bounds exception
model := self.c.Model()
var lastReflogCommit *models.Commit
if len(model.ReflogCommits) > 0 {
lastReflogCommit = model.ReflogCommits[0]
}
refresh := func(stateCommits *[]*models.Commit, filterPath string) error {
commits, onlyObtainedNewReflogCommits, err := self.c.Git().Loaders.ReflogCommitLoader.
GetReflogCommits(lastReflogCommit, filterPath)
if err != nil {
return self.c.Error(err)
}
if onlyObtainedNewReflogCommits {
*stateCommits = append(commits, *stateCommits...)
} else {
*stateCommits = commits
}
return nil
}
if err := refresh(&model.ReflogCommits, ""); err != nil {
return err
}
if self.c.Modes().Filtering.Active() {
if err := refresh(&model.FilteredReflogCommits, self.c.Modes().Filtering.GetPath()); err != nil {
return err
}
} else {
model.FilteredReflogCommits = model.ReflogCommits
}
return self.c.PostRefreshUpdate(self.c.Contexts().ReflogCommits)
}
func (self *RefreshHelper) refreshRemotes() error {
prevSelectedRemote := self.c.Contexts().Remotes.GetSelected()
remotes, err := self.c.Git().Loaders.RemoteLoader.GetRemotes()
if err != nil {
return self.c.Error(err)
}
self.c.Model().Remotes = remotes
// we need to ensure our selected remote branches aren't now outdated
if prevSelectedRemote != nil && self.c.Model().RemoteBranches != nil {
// find remote now
for _, remote := range remotes {
if remote.Name == prevSelectedRemote.Name {
self.c.Model().RemoteBranches = remote.Branches
break
}
}
}
if err := self.c.PostRefreshUpdate(self.c.Contexts().Remotes); err != nil {
return err
}
if err := self.c.PostRefreshUpdate(self.c.Contexts().RemoteBranches); err != nil {
return err
}
return nil
}
func (self *RefreshHelper) refreshStashEntries() error {
self.c.Model().StashEntries = self.c.Git().Loaders.StashLoader.
GetStashEntries(self.c.Modes().Filtering.GetPath())
return self.c.PostRefreshUpdate(self.c.Contexts().Stash)
}
// never call this on its own, it should only be called from within refreshCommits()
func (self *RefreshHelper) refreshStatus() {
self.c.Mutexes().RefreshingStatusMutex.Lock()
defer self.c.Mutexes().RefreshingStatusMutex.Unlock()
currentBranch := self.refsHelper.GetCheckedOutRef()
if currentBranch == nil {
// need to wait for branches to refresh
return
}
status := ""
if currentBranch.IsRealBranch() {
status += presentation.ColoredBranchStatus(currentBranch, self.c.Tr) + " "
}
workingTreeState := self.c.Git().Status.WorkingTreeState()
if workingTreeState != enums.REBASE_MODE_NONE {
status += style.FgYellow.Sprintf("(%s) ", presentation.FormatWorkingTreeState(workingTreeState))
}
name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
repoName := utils.GetCurrentRepoName()
status += fmt.Sprintf("%s → %s ", repoName, name)
self.c.SetViewContent(self.c.Views().Status, status)
}
func (self *RefreshHelper) refForLog() string {
bisectInfo := self.c.Git().Bisect.GetInfo()
self.c.Model().BisectInfo = bisectInfo
if !bisectInfo.Started() {
return "HEAD"
}
// need to see if our bisect's current commit is reachable from our 'new' ref.
if bisectInfo.Bisecting() && !self.c.Git().Bisect.ReachableFromStart(bisectInfo) {
return bisectInfo.GetNewSha()
}
return bisectInfo.GetStartSha()
}

View File

@ -5,10 +5,8 @@ import (
"strings" "strings"
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
@ -23,23 +21,14 @@ type IRefsHelper interface {
} }
type RefsHelper struct { type RefsHelper struct {
c *types.HelperCommon c *HelperCommon
git *commands.GitCommand
contexts *context.ContextTree
model *types.Model
} }
func NewRefsHelper( func NewRefsHelper(
c *types.HelperCommon, c *HelperCommon,
git *commands.GitCommand,
contexts *context.ContextTree,
model *types.Model,
) *RefsHelper { ) *RefsHelper {
return &RefsHelper{ return &RefsHelper{
c: c, c: c,
git: git,
contexts: contexts,
model: model,
} }
} }
@ -54,15 +43,15 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions
cmdOptions := git_commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars} cmdOptions := git_commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars}
onSuccess := func() { onSuccess := func() {
self.contexts.Branches.SetSelectedLineIdx(0) self.c.Contexts().Branches.SetSelectedLineIdx(0)
self.contexts.ReflogCommits.SetSelectedLineIdx(0) self.c.Contexts().ReflogCommits.SetSelectedLineIdx(0)
self.contexts.LocalCommits.SetSelectedLineIdx(0) self.c.Contexts().LocalCommits.SetSelectedLineIdx(0)
// loading a heap of commits is slow so we limit them whenever doing a reset // loading a heap of commits is slow so we limit them whenever doing a reset
self.contexts.LocalCommits.SetLimitCommits(true) self.c.Contexts().LocalCommits.SetLimitCommits(true)
} }
return self.c.WithWaitingStatus(waitingStatus, func() error { return self.c.WithWaitingStatus(waitingStatus, func() error {
if err := self.git.Branch.Checkout(ref, cmdOptions); err != nil { if err := self.c.Git().Branch.Checkout(ref, cmdOptions); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option // note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
if options.OnRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") { if options.OnRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") {
@ -75,15 +64,15 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions
Title: self.c.Tr.AutoStashTitle, Title: self.c.Tr.AutoStashTitle,
Prompt: self.c.Tr.AutoStashPrompt, Prompt: self.c.Tr.AutoStashPrompt,
HandleConfirm: func() error { HandleConfirm: func() error {
if err := self.git.Stash.Save(self.c.Tr.StashPrefix + ref); err != nil { if err := self.c.Git().Stash.Save(self.c.Tr.StashPrefix + ref); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
if err := self.git.Branch.Checkout(ref, cmdOptions); err != nil { if err := self.c.Git().Branch.Checkout(ref, cmdOptions); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
onSuccess() onSuccess()
if err := self.git.Stash.Pop(0); err != nil { if err := self.c.Git().Stash.Pop(0); err != nil {
if err := self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI}); err != nil { if err := self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI}); err != nil {
return err return err
} }
@ -105,22 +94,22 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions
} }
func (self *RefsHelper) GetCheckedOutRef() *models.Branch { func (self *RefsHelper) GetCheckedOutRef() *models.Branch {
if len(self.model.Branches) == 0 { if len(self.c.Model().Branches) == 0 {
return nil return nil
} }
return self.model.Branches[0] return self.c.Model().Branches[0]
} }
func (self *RefsHelper) ResetToRef(ref string, strength string, envVars []string) error { func (self *RefsHelper) ResetToRef(ref string, strength string, envVars []string) error {
if err := self.git.Commit.ResetToCommit(ref, strength, envVars); err != nil { if err := self.c.Git().Commit.ResetToCommit(ref, strength, envVars); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
self.contexts.LocalCommits.SetSelectedLineIdx(0) self.c.Contexts().LocalCommits.SetSelectedLineIdx(0)
self.contexts.ReflogCommits.SetSelectedLineIdx(0) self.c.Contexts().ReflogCommits.SetSelectedLineIdx(0)
// loading a heap of commits is slow so we limit them whenever doing a reset // loading a heap of commits is slow so we limit them whenever doing a reset
self.contexts.LocalCommits.SetLimitCommits(true) self.c.Contexts().LocalCommits.SetLimitCommits(true)
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.BRANCHES, types.REFLOG, types.COMMITS}}); err != nil { if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.BRANCHES, types.REFLOG, types.COMMITS}}); err != nil {
return err return err
@ -173,18 +162,18 @@ func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggest
InitialContent: suggestedBranchName, InitialContent: suggestedBranchName,
HandleConfirm: func(response string) error { HandleConfirm: func(response string) error {
self.c.LogAction(self.c.Tr.Actions.CreateBranch) self.c.LogAction(self.c.Tr.Actions.CreateBranch)
if err := self.git.Branch.New(sanitizedBranchName(response), from); err != nil { if err := self.c.Git().Branch.New(sanitizedBranchName(response), from); err != nil {
return err return err
} }
if self.c.CurrentContext() != self.contexts.Branches { if self.c.CurrentContext() != self.c.Contexts().Branches {
if err := self.c.PushContext(self.contexts.Branches); err != nil { if err := self.c.PushContext(self.c.Contexts().Branches); err != nil {
return err return err
} }
} }
self.contexts.LocalCommits.SetSelectedLineIdx(0) self.c.Contexts().LocalCommits.SetSelectedLineIdx(0)
self.contexts.Branches.SetSelectedLineIdx(0) self.c.Contexts().Branches.SetSelectedLineIdx(0)
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}, },

View File

@ -0,0 +1,175 @@
package helpers
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/jesseduffield/generics/slices"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/env"
"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"
)
type onNewRepoFn func(startArgs appTypes.StartArgs, reuseState bool) error
// helps switch back and forth between repos
type ReposHelper struct {
c *HelperCommon
recordDirectoryHelper *RecordDirectoryHelper
onNewRepo onNewRepoFn
}
func NewRecentReposHelper(
c *HelperCommon,
recordDirectoryHelper *RecordDirectoryHelper,
onNewRepo onNewRepoFn,
) *ReposHelper {
return &ReposHelper{
c: c,
recordDirectoryHelper: recordDirectoryHelper,
onNewRepo: onNewRepo,
}
}
func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error {
wd, err := os.Getwd()
if err != nil {
return err
}
self.c.State().GetRepoPathStack().Push(wd)
return self.DispatchSwitchToRepo(submodule.Path, true)
}
func (self *ReposHelper) getCurrentBranch(path string) string {
readHeadFile := func(path string) (string, error) {
headFile, err := os.ReadFile(filepath.Join(path, "HEAD"))
if err == nil {
content := strings.TrimSpace(string(headFile))
refsPrefix := "ref: refs/heads/"
var branchDisplay string
if strings.HasPrefix(content, refsPrefix) {
// is a branch
branchDisplay = strings.TrimPrefix(content, refsPrefix)
} else {
// detached HEAD state, displaying short SHA
branchDisplay = utils.ShortSha(content)
}
return branchDisplay, nil
}
return "", err
}
gitDirPath := filepath.Join(path, ".git")
if gitDir, err := os.Stat(gitDirPath); err == nil {
if gitDir.IsDir() {
// ordinary repo
if branch, err := readHeadFile(gitDirPath); err == nil {
return branch
}
} else {
// worktree
if worktreeGitDir, err := os.ReadFile(gitDirPath); err == nil {
content := strings.TrimSpace(string(worktreeGitDir))
worktreePath := strings.TrimPrefix(content, "gitdir: ")
if branch, err := readHeadFile(worktreePath); err == nil {
return branch
}
}
}
}
return self.c.Tr.LcBranchUnknown
}
func (self *ReposHelper) CreateRecentReposMenu() error {
// we'll show an empty panel if there are no recent repos
recentRepoPaths := []string{}
if len(self.c.GetAppState().RecentRepos) > 0 {
// we skip the first one because we're currently in it
recentRepoPaths = self.c.GetAppState().RecentRepos[1:]
}
currentBranches := sync.Map{}
wg := sync.WaitGroup{}
wg.Add(len(recentRepoPaths))
for _, path := range recentRepoPaths {
go func(path string) {
defer wg.Done()
currentBranches.Store(path, self.getCurrentBranch(path))
}(path)
}
wg.Wait()
menuItems := slices.Map(recentRepoPaths, func(path string) *types.MenuItem {
branchName, _ := currentBranches.Load(path)
if icons.IsIconEnabled() {
branchName = icons.BRANCH_ICON + " " + fmt.Sprintf("%v", branchName)
}
return &types.MenuItem{
LabelColumns: []string{
filepath.Base(path),
style.FgCyan.Sprint(branchName),
style.FgMagenta.Sprint(path),
},
OnPress: func() error {
// if we were in a submodule, we want to forget about that stack of repos
// so that hitting escape in the new repo does nothing
self.c.State().GetRepoPathStack().Clear()
return self.DispatchSwitchToRepo(path, false)
},
}
})
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.RecentRepos, Items: menuItems})
}
func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool) error {
env.UnsetGitDirEnvs()
originalPath, err := os.Getwd()
if err != nil {
return nil
}
if err := os.Chdir(path); err != nil {
if os.IsNotExist(err) {
return self.c.ErrorMsg(self.c.Tr.ErrRepositoryMovedOrDeleted)
}
return err
}
if err := commands.VerifyInGitRepo(self.c.OS()); err != nil {
if err := os.Chdir(originalPath); err != nil {
return err
}
return err
}
if err := self.recordDirectoryHelper.RecordCurrentDirectory(); err != nil {
return err
}
// these two mutexes are used by our background goroutines (triggered via `self.goEvery`. We don't want to
// switch to a repo while one of these goroutines is in the process of updating something
self.c.Mutexes().SyncMutex.Lock()
defer self.c.Mutexes().SyncMutex.Unlock()
self.c.Mutexes().RefreshingFilesMutex.Lock()
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
return self.onNewRepo(appTypes.StartArgs{}, reuse)
}

View File

@ -0,0 +1,75 @@
package helpers
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/snake"
)
type SnakeHelper struct {
c *HelperCommon
game *snake.Game
}
func NewSnakeHelper(c *HelperCommon) *SnakeHelper {
return &SnakeHelper{
c: c,
}
}
func (self *SnakeHelper) StartGame() {
view := self.c.Views().Snake
game := snake.NewGame(view.Width(), view.Height(), self.renderSnakeGame, self.c.LogAction)
self.game = game
game.Start()
}
func (self *SnakeHelper) ExitGame() {
self.game.Exit()
}
func (self *SnakeHelper) SetDirection(direction snake.Direction) {
self.game.SetDirection(direction)
}
func (self *SnakeHelper) renderSnakeGame(cells [][]snake.CellType, alive bool) {
view := self.c.Views().Snake
if !alive {
_ = self.c.ErrorMsg(self.c.Tr.YouDied)
return
}
output := self.drawSnakeGame(cells)
view.Clear()
fmt.Fprint(view, output)
self.c.Render()
}
func (self *SnakeHelper) drawSnakeGame(cells [][]snake.CellType) string {
writer := &strings.Builder{}
for i, row := range cells {
for _, cell := range row {
switch cell {
case snake.None:
writer.WriteString(" ")
case snake.Snake:
writer.WriteString("█")
case snake.Food:
writer.WriteString(style.FgMagenta.Sprint("█"))
}
}
if i < len(cells) {
writer.WriteString("\n")
}
}
output := writer.String()
return output
}

View File

@ -0,0 +1,122 @@
package helpers
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type StagingHelper struct {
c *HelperCommon
}
func NewStagingHelper(
c *HelperCommon,
) *StagingHelper {
return &StagingHelper{
c: c,
}
}
// NOTE: used from outside this file
func (self *StagingHelper) RefreshStagingPanel(focusOpts types.OnFocusOpts) error {
secondaryFocused := self.secondaryStagingFocused()
mainFocused := self.mainStagingFocused()
// this method could be called when the staging panel is not being used,
// in which case we don't want to do anything.
if !mainFocused && !secondaryFocused {
return nil
}
mainSelectedLineIdx := -1
secondarySelectedLineIdx := -1
if focusOpts.ClickedViewLineIdx > 0 {
if secondaryFocused {
secondarySelectedLineIdx = focusOpts.ClickedViewLineIdx
} else {
mainSelectedLineIdx = focusOpts.ClickedViewLineIdx
}
}
mainContext := self.c.Contexts().Staging
secondaryContext := self.c.Contexts().StagingSecondary
var file *models.File
node := self.c.Contexts().Files.GetSelected()
if node != nil {
file = node.File
}
if file == nil || (!file.HasUnstagedChanges && !file.HasStagedChanges) {
return self.handleStagingEscape()
}
mainDiff := self.c.Git().WorkingTree.WorktreeFileDiff(file, true, false, false)
secondaryDiff := self.c.Git().WorkingTree.WorktreeFileDiff(file, true, true, false)
// grabbing locks here and releasing before we finish the function
// because pushing say the secondary context could mean entering this function
// again, and we don't want to have a deadlock
mainContext.GetMutex().Lock()
secondaryContext.GetMutex().Lock()
mainContext.SetState(
patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainContext.GetState(), self.c.Log),
)
secondaryContext.SetState(
patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondaryContext.GetState(), self.c.Log),
)
mainState := mainContext.GetState()
secondaryState := secondaryContext.GetState()
mainContent := mainContext.GetContentToRender(!secondaryFocused)
secondaryContent := secondaryContext.GetContentToRender(secondaryFocused)
mainContext.GetMutex().Unlock()
secondaryContext.GetMutex().Unlock()
if mainState == nil && secondaryState == nil {
return self.handleStagingEscape()
}
if mainState == nil && !secondaryFocused {
return self.c.PushContext(secondaryContext, focusOpts)
}
if secondaryState == nil && secondaryFocused {
return self.c.PushContext(mainContext, focusOpts)
}
if secondaryFocused {
self.c.Contexts().StagingSecondary.FocusSelection()
} else {
self.c.Contexts().Staging.FocusSelection()
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Staging,
Main: &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(mainContent),
Title: self.c.Tr.UnstagedChanges,
},
Secondary: &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(secondaryContent),
Title: self.c.Tr.StagedChanges,
},
})
}
func (self *StagingHelper) handleStagingEscape() error {
return self.c.PushContext(self.c.Contexts().Files)
}
func (self *StagingHelper) secondaryStagingFocused() bool {
return self.c.CurrentStaticContext().GetKey() == self.c.Contexts().StagingSecondary.GetKey()
}
func (self *StagingHelper) mainStagingFocused() bool {
return self.c.CurrentStaticContext().GetKey() == self.c.Contexts().Staging.GetKey()
}

View File

@ -33,28 +33,21 @@ type ISuggestionsHelper interface {
} }
type SuggestionsHelper struct { type SuggestionsHelper struct {
c *types.HelperCommon c *HelperCommon
model *types.Model
refreshSuggestionsFn func()
} }
var _ ISuggestionsHelper = &SuggestionsHelper{} var _ ISuggestionsHelper = &SuggestionsHelper{}
func NewSuggestionsHelper( func NewSuggestionsHelper(
c *types.HelperCommon, c *HelperCommon,
model *types.Model,
refreshSuggestionsFn func(),
) *SuggestionsHelper { ) *SuggestionsHelper {
return &SuggestionsHelper{ return &SuggestionsHelper{
c: c, c: c,
model: model,
refreshSuggestionsFn: refreshSuggestionsFn,
} }
} }
func (self *SuggestionsHelper) getRemoteNames() []string { func (self *SuggestionsHelper) getRemoteNames() []string {
return slices.Map(self.model.Remotes, func(remote *models.Remote) string { return slices.Map(self.c.Model().Remotes, func(remote *models.Remote) string {
return remote.Name return remote.Name
}) })
} }
@ -75,7 +68,7 @@ func (self *SuggestionsHelper) GetRemoteSuggestionsFunc() func(string) []*types.
} }
func (self *SuggestionsHelper) getBranchNames() []string { func (self *SuggestionsHelper) getBranchNames() []string {
return slices.Map(self.model.Branches, func(branch *models.Branch) string { return slices.Map(self.c.Model().Branches, func(branch *models.Branch) string {
return branch.Name return branch.Name
}) })
} }
@ -101,8 +94,8 @@ func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*ty
} }
// here we asynchronously fetch the latest set of paths in the repo and store in // here we asynchronously fetch the latest set of paths in the repo and store in
// self.model.FilesTrie. On the main thread we'll be doing a fuzzy search via // self.c.Model().FilesTrie. On the main thread we'll be doing a fuzzy search via
// self.model.FilesTrie. So if we've looked for a file previously, we'll start with // self.c.Model().FilesTrie. So if we've looked for a file previously, we'll start with
// the old trie and eventually it'll be swapped out for the new one. // the old trie and eventually it'll be swapped out for the new one.
// Notably, unlike other suggestion functions we're not showing all the options // Notably, unlike other suggestion functions we're not showing all the options
// if nothing has been typed because there'll be too much to display efficiently // if nothing has been typed because there'll be too much to display efficiently
@ -125,16 +118,16 @@ func (self *SuggestionsHelper) GetFilePathSuggestionsFunc() func(string) []*type
}) })
// cache the trie for future use // cache the trie for future use
self.model.FilesTrie = trie self.c.Model().FilesTrie = trie
self.refreshSuggestionsFn() self.c.Contexts().Suggestions.RefreshSuggestions()
return err return err
}) })
return func(input string) []*types.Suggestion { return func(input string) []*types.Suggestion {
matchingNames := []string{} matchingNames := []string{}
_ = self.model.FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error { _ = self.c.Model().FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error {
matchingNames = append(matchingNames, item.(string)) matchingNames = append(matchingNames, item.(string))
return nil return nil
}) })
@ -147,7 +140,7 @@ func (self *SuggestionsHelper) GetFilePathSuggestionsFunc() func(string) []*type
} }
func (self *SuggestionsHelper) getRemoteBranchNames(separator string) []string { func (self *SuggestionsHelper) getRemoteBranchNames(separator string) []string {
return slices.FlatMap(self.model.Remotes, func(remote *models.Remote) []string { return slices.FlatMap(self.c.Model().Remotes, func(remote *models.Remote) []string {
return slices.Map(remote.Branches, func(branch *models.RemoteBranch) string { return slices.Map(remote.Branches, func(branch *models.RemoteBranch) string {
return fmt.Sprintf("%s%s%s", remote.Name, separator, branch.Name) return fmt.Sprintf("%s%s%s", remote.Name, separator, branch.Name)
}) })
@ -159,7 +152,7 @@ func (self *SuggestionsHelper) GetRemoteBranchesSuggestionsFunc(separator string
} }
func (self *SuggestionsHelper) getTagNames() []string { func (self *SuggestionsHelper) getTagNames() []string {
return slices.Map(self.model.Tags, func(tag *models.Tag) string { return slices.Map(self.c.Model().Tags, func(tag *models.Tag) string {
return tag.Name return tag.Name
}) })
} }
@ -176,7 +169,7 @@ func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Su
} }
func (self *SuggestionsHelper) GetAuthorsSuggestionsFunc() func(string) []*types.Suggestion { func (self *SuggestionsHelper) GetAuthorsSuggestionsFunc() func(string) []*types.Suggestion {
authors := lo.Uniq(slices.Map(self.model.Commits, func(commit *models.Commit) string { authors := lo.Uniq(slices.Map(self.c.Model().Commits, func(commit *models.Commit) string {
return fmt.Sprintf("%s <%s>", commit.AuthorName, commit.AuthorEmail) return fmt.Sprintf("%s <%s>", commit.AuthorName, commit.AuthorEmail)
})) }))

View File

@ -1,7 +1,6 @@
package helpers package helpers
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -10,14 +9,12 @@ import (
// and the commits context. // and the commits context.
type TagsHelper struct { type TagsHelper struct {
c *types.HelperCommon c *HelperCommon
git *commands.GitCommand
} }
func NewTagsHelper(c *types.HelperCommon, git *commands.GitCommand) *TagsHelper { func NewTagsHelper(c *HelperCommon) *TagsHelper {
return &TagsHelper{ return &TagsHelper{
c: c, c: c,
git: git,
} }
} }
@ -56,7 +53,7 @@ func (self *TagsHelper) handleCreateAnnotatedTag(ref string, onCreate func()) er
Title: self.c.Tr.TagMessageTitle, Title: self.c.Tr.TagMessageTitle,
HandleConfirm: func(msg string) error { HandleConfirm: func(msg string) error {
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag) self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
if err := self.git.Tag.CreateAnnotated(tagName, ref, msg); err != nil { if err := self.c.Git().Tag.CreateAnnotated(tagName, ref, msg); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.afterTagCreate(onCreate) return self.afterTagCreate(onCreate)
@ -71,7 +68,7 @@ func (self *TagsHelper) handleCreateLightweightTag(ref string, onCreate func())
Title: self.c.Tr.TagNameTitle, Title: self.c.Tr.TagNameTitle,
HandleConfirm: func(tagName string) error { HandleConfirm: func(tagName string) error {
self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag) self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
if err := self.git.Tag.CreateLightweight(tagName, ref); err != nil { if err := self.c.Git().Tag.CreateLightweight(tagName, ref); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.afterTagCreate(onCreate) return self.afterTagCreate(onCreate)

View File

@ -0,0 +1,96 @@
package helpers
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type UpdateHelper struct {
c *HelperCommon
updater *updates.Updater
}
func NewUpdateHelper(c *HelperCommon, updater *updates.Updater) *UpdateHelper {
return &UpdateHelper{
c: c,
updater: updater,
}
}
func (self *UpdateHelper) CheckForUpdateInBackground() {
self.updater.CheckForNewUpdate(func(newVersion string, err error) error {
if err != nil {
// ignoring the error for now so that I'm not annoying users
self.c.Log.Error(err.Error())
return nil
}
if newVersion == "" {
return nil
}
if self.c.UserConfig.Update.Method == "background" {
self.startUpdating(newVersion)
return nil
}
return self.showUpdatePrompt(newVersion)
}, false)
}
func (self *UpdateHelper) CheckForUpdateInForeground() error {
return self.c.WithWaitingStatus(self.c.Tr.CheckingForUpdates, func() error {
self.updater.CheckForNewUpdate(func(newVersion string, err error) error {
if err != nil {
return self.c.Error(err)
}
if newVersion == "" {
return self.c.ErrorMsg(self.c.Tr.FailedToRetrieveLatestVersionErr)
}
return self.showUpdatePrompt(newVersion)
}, true)
return nil
})
}
func (self *UpdateHelper) startUpdating(newVersion string) {
_ = self.c.WithWaitingStatus(self.c.Tr.UpdateInProgressWaitingStatus, func() error {
self.c.State().SetUpdating(true)
err := self.updater.Update(newVersion)
return self.onUpdateFinish(err)
})
}
func (self *UpdateHelper) onUpdateFinish(err error) error {
self.c.State().SetUpdating(false)
self.c.OnUIThread(func() error {
self.c.SetViewContent(self.c.Views().AppStatus, "")
if err != nil {
errMessage := utils.ResolvePlaceholderString(
self.c.Tr.UpdateFailedErr, map[string]string{
"errMessage": err.Error(),
},
)
return self.c.ErrorMsg(errMessage)
}
return self.c.Alert(self.c.Tr.UpdateCompletedTitle, self.c.Tr.UpdateCompleted)
})
return nil
}
func (self *UpdateHelper) showUpdatePrompt(newVersion string) error {
message := utils.ResolvePlaceholderString(
self.c.Tr.UpdateAvailable, map[string]string{
"newVersion": newVersion,
},
)
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.UpdateAvailableTitle,
Prompt: message,
HandleConfirm: func() error {
self.startUpdating(newVersion)
return nil
},
})
}

View File

@ -9,8 +9,7 @@ import (
) )
type UpstreamHelper struct { type UpstreamHelper struct {
c *types.HelperCommon c *HelperCommon
model *types.Model
getRemoteBranchesSuggestionsFunc func(string) func(string) []*types.Suggestion getRemoteBranchesSuggestionsFunc func(string) func(string) []*types.Suggestion
} }
@ -25,13 +24,11 @@ type IUpstreamHelper interface {
var _ IUpstreamHelper = &UpstreamHelper{} var _ IUpstreamHelper = &UpstreamHelper{}
func NewUpstreamHelper( func NewUpstreamHelper(
c *types.HelperCommon, c *HelperCommon,
model *types.Model,
getRemoteBranchesSuggestionsFunc func(string) func(string) []*types.Suggestion, getRemoteBranchesSuggestionsFunc func(string) func(string) []*types.Suggestion,
) *UpstreamHelper { ) *UpstreamHelper {
return &UpstreamHelper{ return &UpstreamHelper{
c: c, c: c,
model: model,
getRemoteBranchesSuggestionsFunc: getRemoteBranchesSuggestionsFunc, getRemoteBranchesSuggestionsFunc: getRemoteBranchesSuggestionsFunc,
} }
} }
@ -70,7 +67,7 @@ func (self *UpstreamHelper) PromptForUpstreamWithoutInitialContent(_ *models.Bra
} }
func (self *UpstreamHelper) GetSuggestedRemote() string { func (self *UpstreamHelper) GetSuggestedRemote() string {
return getSuggestedRemote(self.model.Remotes) return getSuggestedRemote(self.c.Model().Remotes)
} }
func getSuggestedRemote(remotes []*models.Remote) string { func getSuggestedRemote(remotes []*models.Remote) string {

View File

@ -0,0 +1,31 @@
package helpers
import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ViewHelper struct {
c *HelperCommon
}
func NewViewHelper(c *HelperCommon, contexts *context.ContextTree) *ViewHelper {
return &ViewHelper{
c: c,
}
}
func (self *ViewHelper) ContextForView(viewName string) (types.Context, bool) {
view, err := self.c.GocuiGui().View(viewName)
if err != nil {
return nil, false
}
for _, context := range self.c.Contexts().Flatten() {
if context.GetViewName() == view.Name() {
return context, true
}
}
return nil, false
}

View File

@ -1,7 +1,8 @@
package gui package helpers
import ( import (
"github.com/jesseduffield/lazycore/pkg/boxlayout" "github.com/jesseduffield/lazycore/pkg/boxlayout"
"github.com/jesseduffield/lazygit/pkg/gui/constants"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
@ -11,12 +12,33 @@ import (
// In this file we use the boxlayout package, along with knowledge about the app's state, // In this file we use the boxlayout package, along with knowledge about the app's state,
// to arrange the windows (i.e. panels) on the screen. // to arrange the windows (i.e. panels) on the screen.
type WindowArrangementHelper struct {
c *HelperCommon
windowHelper *WindowHelper
modeHelper *ModeHelper
appStatusHelper *AppStatusHelper
}
func NewWindowArrangementHelper(
c *HelperCommon,
windowHelper *WindowHelper,
modeHelper *ModeHelper,
appStatusHelper *AppStatusHelper,
) *WindowArrangementHelper {
return &WindowArrangementHelper{
c: c,
windowHelper: windowHelper,
modeHelper: modeHelper,
appStatusHelper: appStatusHelper,
}
}
const INFO_SECTION_PADDING = " " const INFO_SECTION_PADDING = " "
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
width, height := gui.g.Size() width, height := self.c.GocuiGui().Size()
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights() sideSectionWeight, mainSectionWeight := self.getMidSectionWeights()
sidePanelsDirection := boxlayout.COLUMN sidePanelsDirection := boxlayout.COLUMN
portraitMode := width <= 84 && height > 45 portraitMode := width <= 84 && height > 45
@ -25,13 +47,18 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
} }
mainPanelsDirection := boxlayout.ROW mainPanelsDirection := boxlayout.ROW
if gui.splitMainPanelSideBySide() { if self.splitMainPanelSideBySide() {
mainPanelsDirection = boxlayout.COLUMN mainPanelsDirection = boxlayout.COLUMN
} }
extrasWindowSize := gui.getExtrasWindowSize(height) extrasWindowSize := self.getExtrasWindowSize(height)
showInfoSection := gui.c.UserConfig.Gui.ShowBottomLine || gui.State.Searching.isSearching || gui.isAnyModeActive() || gui.statusManager.showStatus() self.c.Modes().Filtering.Active()
showInfoSection := self.c.UserConfig.Gui.ShowBottomLine ||
self.c.State().GetRepoState().IsSearching() ||
self.modeHelper.IsAnyModeActive() ||
self.appStatusHelper.HasStatus()
infoSectionSize := 0 infoSectionSize := 0
if showInfoSection { if showInfoSection {
infoSectionSize = 1 infoSectionSize = 1
@ -47,7 +74,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
{ {
Direction: boxlayout.ROW, Direction: boxlayout.ROW,
Weight: sideSectionWeight, Weight: sideSectionWeight,
ConditionalChildren: gui.sidePanelChildren, ConditionalChildren: self.sidePanelChildren,
}, },
{ {
Direction: boxlayout.ROW, Direction: boxlayout.ROW,
@ -55,7 +82,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
Children: []*boxlayout.Box{ Children: []*boxlayout.Box{
{ {
Direction: mainPanelsDirection, Direction: mainPanelsDirection,
Children: gui.mainSectionChildren(), Children: self.mainSectionChildren(),
Weight: 1, Weight: 1,
}, },
{ {
@ -69,7 +96,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
{ {
Direction: boxlayout.COLUMN, Direction: boxlayout.COLUMN,
Size: infoSectionSize, Size: infoSectionSize,
Children: gui.infoSectionChildren(informationStr, appStatus), Children: self.infoSectionChildren(informationStr, appStatus),
}, },
}, },
} }
@ -91,12 +118,12 @@ func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
return result return result
} }
func (gui *Gui) mainSectionChildren() []*boxlayout.Box { func (self *WindowArrangementHelper) mainSectionChildren() []*boxlayout.Box {
currentWindow := gui.currentWindow() currentWindow := self.windowHelper.CurrentWindow()
// if we're not in split mode we can just show the one main panel. Likewise if // if we're not in split mode we can just show the one main panel. Likewise if
// the main panel is focused and we're in full-screen mode // the main panel is focused and we're in full-screen mode
if !gui.isMainPanelSplit() || (gui.State.ScreenMode == SCREEN_FULL && currentWindow == "main") { if !self.c.State().GetRepoState().GetSplitMainPanel() || (self.c.State().GetRepoState().GetScreenMode() == types.SCREEN_FULL && currentWindow == "main") {
return []*boxlayout.Box{ return []*boxlayout.Box{
{ {
Window: "main", Window: "main",
@ -117,27 +144,29 @@ func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
} }
} }
func (gui *Gui) getMidSectionWeights() (int, int) { func (self *WindowArrangementHelper) getMidSectionWeights() (int, int) {
currentWindow := gui.currentWindow() currentWindow := self.windowHelper.CurrentWindow()
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4 // we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
sidePanelWidthRatio := gui.c.UserConfig.Gui.SidePanelWidth sidePanelWidthRatio := self.c.UserConfig.Gui.SidePanelWidth
// we could make this better by creating ratios like 2:3 rather than always 1:something // we could make this better by creating ratios like 2:3 rather than always 1:something
mainSectionWeight := int(1/sidePanelWidthRatio) - 1 mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 1 sideSectionWeight := 1
if gui.splitMainPanelSideBySide() { if self.splitMainPanelSideBySide() {
mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side
} }
screenMode := self.c.State().GetRepoState().GetScreenMode()
if currentWindow == "main" { if currentWindow == "main" {
if gui.State.ScreenMode == SCREEN_HALF || gui.State.ScreenMode == SCREEN_FULL { if screenMode == types.SCREEN_HALF || screenMode == types.SCREEN_FULL {
sideSectionWeight = 0 sideSectionWeight = 0
} }
} else { } else {
if gui.State.ScreenMode == SCREEN_HALF { if screenMode == types.SCREEN_HALF {
mainSectionWeight = 1 mainSectionWeight = 1
} else if gui.State.ScreenMode == SCREEN_FULL { } else if screenMode == types.SCREEN_FULL {
mainSectionWeight = 0 mainSectionWeight = 0
} }
} }
@ -145,12 +174,12 @@ func (gui *Gui) getMidSectionWeights() (int, int) {
return sideSectionWeight, mainSectionWeight return sideSectionWeight, mainSectionWeight
} }
func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { func (self *WindowArrangementHelper) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
if gui.State.Searching.isSearching { if self.c.State().GetRepoState().IsSearching() {
return []*boxlayout.Box{ return []*boxlayout.Box{
{ {
Window: "searchPrefix", Window: "searchPrefix",
Size: runewidth.StringWidth(SEARCH_PREFIX), Size: runewidth.StringWidth(constants.SEARCH_PREFIX),
}, },
{ {
Window: "search", Window: "search",
@ -162,7 +191,7 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
appStatusBox := &boxlayout.Box{Window: "appStatus"} appStatusBox := &boxlayout.Box{Window: "appStatus"}
optionsBox := &boxlayout.Box{Window: "options"} optionsBox := &boxlayout.Box{Window: "options"}
if !gui.c.UserConfig.Gui.ShowBottomLine { if !self.c.UserConfig.Gui.ShowBottomLine {
optionsBox.Weight = 0 optionsBox.Weight = 0
appStatusBox.Weight = 1 appStatusBox.Weight = 1
} else { } else {
@ -172,7 +201,7 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
result := []*boxlayout.Box{appStatusBox, optionsBox} result := []*boxlayout.Box{appStatusBox, optionsBox}
if gui.c.UserConfig.Gui.ShowBottomLine || gui.isAnyModeActive() { if self.c.UserConfig.Gui.ShowBottomLine || self.modeHelper.IsAnyModeActive() {
result = append(result, &boxlayout.Box{ result = append(result, &boxlayout.Box{
Window: "information", Window: "information",
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length // unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
@ -183,13 +212,13 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
return result return result
} }
func (gui *Gui) splitMainPanelSideBySide() bool { func (self *WindowArrangementHelper) splitMainPanelSideBySide() bool {
if !gui.isMainPanelSplit() { if !self.c.State().GetRepoState().GetSplitMainPanel() {
return false return false
} }
mainPanelSplitMode := gui.c.UserConfig.Gui.MainPanelSplitMode mainPanelSplitMode := self.c.UserConfig.Gui.MainPanelSplitMode
width, height := gui.g.Size() width, height := self.c.GocuiGui().Size()
switch mainPanelSplitMode { switch mainPanelSplitMode {
case "vertical": case "vertical":
@ -205,18 +234,18 @@ func (gui *Gui) splitMainPanelSideBySide() bool {
} }
} }
func (gui *Gui) getExtrasWindowSize(screenHeight int) int { func (self *WindowArrangementHelper) getExtrasWindowSize(screenHeight int) int {
if !gui.ShowExtrasWindow { if !self.c.State().GetShowExtrasWindow() {
return 0 return 0
} }
var baseSize int var baseSize int
if gui.currentStaticContext().GetKey() == context.COMMAND_LOG_CONTEXT_KEY { if self.c.CurrentStaticContext().GetKey() == context.COMMAND_LOG_CONTEXT_KEY {
baseSize = 1000 // my way of saying 'fill the available space' baseSize = 1000 // my way of saying 'fill the available space'
} else if screenHeight < 40 { } else if screenHeight < 40 {
baseSize = 1 baseSize = 1
} else { } else {
baseSize = gui.c.UserConfig.Gui.CommandLogSize baseSize = self.c.UserConfig.Gui.CommandLogSize
} }
frameSize := 2 frameSize := 2
@ -227,17 +256,15 @@ func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
// too much space, but if you access it it should take up some space. This is // too much space, but if you access it it should take up some space. This is
// the default behaviour when accordion mode is NOT in effect. If it is in effect // the default behaviour when accordion mode is NOT in effect. If it is in effect
// then when it's accessed it will have weight 2, not 1. // then when it's accessed it will have weight 2, not 1.
func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box { func (self *WindowArrangementHelper) getDefaultStashWindowBox() *boxlayout.Box {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
box := &boxlayout.Box{Window: "stash"}
stashWindowAccessed := false stashWindowAccessed := false
for _, context := range gui.State.ContextManager.ContextStack { self.c.Context().ForEach(func(context types.Context) {
if context.GetWindowName() == "stash" { if context.GetWindowName() == "stash" {
stashWindowAccessed = true stashWindowAccessed = true
} }
} })
box := &boxlayout.Box{Window: "stash"}
// if the stash window is anywhere in our stack we should enlargen it // if the stash window is anywhere in our stack we should enlargen it
if stashWindowAccessed { if stashWindowAccessed {
box.Weight = 1 box.Weight = 1
@ -248,10 +275,11 @@ func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box {
return box return box
} }
func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { func (self *WindowArrangementHelper) sidePanelChildren(width int, height int) []*boxlayout.Box {
currentWindow := gui.currentSideWindowName() currentWindow := self.c.CurrentSideContext().GetWindowName()
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF { screenMode := self.c.State().GetRepoState().GetScreenMode()
if screenMode == types.SCREEN_FULL || screenMode == types.SCREEN_HALF {
fullHeightBox := func(window string) *boxlayout.Box { fullHeightBox := func(window string) *boxlayout.Box {
if window == currentWindow { if window == currentWindow {
return &boxlayout.Box{ return &boxlayout.Box{
@ -274,7 +302,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
fullHeightBox("stash"), fullHeightBox("stash"),
} }
} else if height >= 28 { } else if height >= 28 {
accordionMode := gui.c.UserConfig.Gui.ExpandFocusedSidePanel accordionMode := self.c.UserConfig.Gui.ExpandFocusedSidePanel
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box { accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordionMode && defaultBox.Window == currentWindow { if accordionMode && defaultBox.Window == currentWindow {
return &boxlayout.Box{ return &boxlayout.Box{
@ -294,7 +322,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
accordionBox(&boxlayout.Box{Window: "files", Weight: 1}), accordionBox(&boxlayout.Box{Window: "files", Weight: 1}),
accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}), accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}),
accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}), accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}),
accordionBox(gui.getDefaultStashWindowBox()), accordionBox(self.getDefaultStashWindowBox()),
} }
} else { } else {
squashedHeight := 1 squashedHeight := 1
@ -325,24 +353,3 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
} }
} }
} }
func (gui *Gui) getCyclableWindows() []string {
return []string{"status", "files", "branches", "commits", "stash"}
}
func (gui *Gui) currentSideWindowName() string {
// there is always one and only one cyclable context in the context stack. We'll look from top to bottom
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
for idx := range gui.State.ContextManager.ContextStack {
reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx
context := gui.State.ContextManager.ContextStack[reversedIdx]
if context.GetKind() == types.SIDE_CONTEXT {
return context.GetWindowName()
}
}
return "files" // default
}

View File

@ -0,0 +1,139 @@
package helpers
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type WindowHelper struct {
c *HelperCommon
viewHelper *ViewHelper
}
func NewWindowHelper(c *HelperCommon, viewHelper *ViewHelper) *WindowHelper {
return &WindowHelper{
c: c,
viewHelper: viewHelper,
}
}
// A window refers to a place on the screen which can hold one or more views.
// A view is a box that renders content, and within a window only one view will
// appear at a time. When a view appears within a window, it occupies the whole
// space. Right now most windows are 1:1 with views, except for commitFiles which
// is a view that moves between windows
func (self *WindowHelper) GetViewNameForWindow(window string) string {
viewName, ok := self.windowViewNameMap().Get(window)
if !ok {
panic(fmt.Sprintf("Viewname not found for window: %s", window))
}
return viewName
}
func (self *WindowHelper) GetContextForWindow(window string) types.Context {
viewName := self.GetViewNameForWindow(window)
context, ok := self.viewHelper.ContextForView(viewName)
if !ok {
panic("TODO: fix this")
}
return context
}
// for now all we actually care about is the context's view so we're storing that
func (self *WindowHelper) SetWindowContext(c types.Context) {
if c.IsTransient() {
self.resetWindowContext(c)
}
self.windowViewNameMap().Set(c.GetWindowName(), c.GetViewName())
}
func (self *WindowHelper) windowViewNameMap() *utils.ThreadSafeMap[string, string] {
return self.c.State().GetRepoState().GetWindowViewNameMap()
}
func (self *WindowHelper) CurrentWindow() string {
return self.c.CurrentContext().GetWindowName()
}
// assumes the context's windowName has been set to the new window if necessary
func (self *WindowHelper) resetWindowContext(c types.Context) {
for _, windowName := range self.windowViewNameMap().Keys() {
viewName, ok := self.windowViewNameMap().Get(windowName)
if !ok {
continue
}
if viewName == c.GetViewName() && windowName != c.GetWindowName() {
for _, context := range self.c.Contexts().Flatten() {
if context.GetKey() != c.GetKey() && context.GetWindowName() == windowName {
self.windowViewNameMap().Set(windowName, context.GetViewName())
}
}
}
}
}
// moves given context's view to the top of the window
func (self *WindowHelper) MoveToTopOfWindow(context types.Context) {
view := context.GetView()
if view == nil {
return
}
window := context.GetWindowName()
topView := self.TopViewInWindow(window)
if view.Name() != topView.Name() {
if err := self.c.GocuiGui().SetViewOnTopOf(view.Name(), topView.Name()); err != nil {
self.c.Log.Error(err)
}
}
}
func (self *WindowHelper) TopViewInWindow(windowName string) *gocui.View {
// now I need to find all views in that same window, via contexts. And I guess then I need to find the index of the highest view in that list.
viewNamesInWindow := self.viewNamesInWindow(windowName)
// The views list is ordered highest-last, so we're grabbing the last view of the window
var topView *gocui.View
for _, currentView := range self.c.GocuiGui().Views() {
if lo.Contains(viewNamesInWindow, currentView.Name()) {
topView = currentView
}
}
return topView
}
func (self *WindowHelper) viewNamesInWindow(windowName string) []string {
result := []string{}
for _, context := range self.c.Contexts().Flatten() {
if context.GetWindowName() == windowName {
result = append(result, context.GetViewName())
}
}
return result
}
func (self *WindowHelper) WindowForView(viewName string) string {
context, ok := self.viewHelper.ContextForView(viewName)
if !ok {
panic("todo: deal with this")
}
return context.GetWindowName()
}
func (self *WindowHelper) SideWindows() []string {
return []string{"status", "files", "branches", "commits", "stash"}
}

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
@ -20,40 +19,28 @@ type IWorkingTreeHelper interface {
} }
type WorkingTreeHelper struct { type WorkingTreeHelper struct {
c *types.HelperCommon c *HelperCommon
git *commands.GitCommand refHelper *RefsHelper
contexts *context.ContextTree commitsHelper *CommitsHelper
refHelper *RefsHelper gpgHelper *GpgHelper
model *types.Model
setCommitMessage func(message string)
commitsHelper *CommitsHelper
gpgHelper *GpgHelper
} }
func NewWorkingTreeHelper( func NewWorkingTreeHelper(
c *types.HelperCommon, c *HelperCommon,
git *commands.GitCommand,
contexts *context.ContextTree,
refHelper *RefsHelper, refHelper *RefsHelper,
model *types.Model,
setCommitMessage func(message string),
commitsHelper *CommitsHelper, commitsHelper *CommitsHelper,
gpgHelper *GpgHelper, gpgHelper *GpgHelper,
) *WorkingTreeHelper { ) *WorkingTreeHelper {
return &WorkingTreeHelper{ return &WorkingTreeHelper{
c: c, c: c,
git: git, refHelper: refHelper,
contexts: contexts, commitsHelper: commitsHelper,
refHelper: refHelper, gpgHelper: gpgHelper,
model: model,
setCommitMessage: setCommitMessage,
commitsHelper: commitsHelper,
gpgHelper: gpgHelper,
} }
} }
func (self *WorkingTreeHelper) AnyStagedFiles() bool { func (self *WorkingTreeHelper) AnyStagedFiles() bool {
for _, file := range self.model.Files { for _, file := range self.c.Model().Files {
if file.HasStagedChanges { if file.HasStagedChanges {
return true return true
} }
@ -62,7 +49,7 @@ func (self *WorkingTreeHelper) AnyStagedFiles() bool {
} }
func (self *WorkingTreeHelper) AnyTrackedFiles() bool { func (self *WorkingTreeHelper) AnyTrackedFiles() bool {
for _, file := range self.model.Files { for _, file := range self.c.Model().Files {
if file.Tracked { if file.Tracked {
return true return true
} }
@ -75,7 +62,7 @@ func (self *WorkingTreeHelper) IsWorkingTreeDirty() bool {
} }
func (self *WorkingTreeHelper) FileForSubmodule(submodule *models.SubmoduleConfig) *models.File { func (self *WorkingTreeHelper) FileForSubmodule(submodule *models.SubmoduleConfig) *models.File {
for _, file := range self.model.Files { for _, file := range self.c.Model().Files {
if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) { if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) {
return file return file
} }
@ -91,7 +78,7 @@ func (self *WorkingTreeHelper) OpenMergeTool() error {
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.OpenMergeTool) self.c.LogAction(self.c.Tr.Actions.OpenMergeTool)
return self.c.RunSubprocessAndRefresh( return self.c.RunSubprocessAndRefresh(
self.git.WorkingTree.OpenMergeToolCmdObj(), self.c.Git().WorkingTree.OpenMergeToolCmdObj(),
) )
}, },
}) })
@ -102,7 +89,7 @@ func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage strin
return self.c.Error(err) return self.c.Error(err)
} }
if len(self.model.Files) == 0 { if len(self.c.Model().Files) == 0 {
return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle) return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle)
} }
@ -122,7 +109,7 @@ func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage strin
} }
func (self *WorkingTreeHelper) handleCommit(message string) error { func (self *WorkingTreeHelper) handleCommit(message string) error {
cmdObj := self.git.Commit.CommitCmdObj(message) cmdObj := self.c.Git().Commit.CommitCmdObj(message)
self.c.LogAction(self.c.Tr.Actions.Commit) self.c.LogAction(self.c.Tr.Actions.Commit)
_ = self.commitsHelper.PopCommitMessageContexts() _ = self.commitsHelper.PopCommitMessageContexts()
return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error { return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
@ -134,7 +121,7 @@ func (self *WorkingTreeHelper) handleCommit(message string) error {
// HandleCommitEditorPress - handle when the user wants to commit changes via // HandleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel // their editor rather than via the popup panel
func (self *WorkingTreeHelper) HandleCommitEditorPress() error { func (self *WorkingTreeHelper) HandleCommitEditorPress() error {
if len(self.model.Files) == 0 { if len(self.c.Model().Files) == 0 {
return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle) return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle)
} }
@ -144,7 +131,7 @@ func (self *WorkingTreeHelper) HandleCommitEditorPress() error {
self.c.LogAction(self.c.Tr.Actions.Commit) self.c.LogAction(self.c.Tr.Actions.Commit)
return self.c.RunSubprocessAndRefresh( return self.c.RunSubprocessAndRefresh(
self.git.Commit.CommitEditorCmdObj(), self.c.Git().Commit.CommitEditorCmdObj(),
) )
} }
@ -158,7 +145,7 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error {
} }
func (self *WorkingTreeHelper) HandleCommitPress() error { func (self *WorkingTreeHelper) HandleCommitPress() error {
message := self.contexts.CommitMessage.GetPreservedMessage() message := self.c.Contexts().CommitMessage.GetPreservedMessage()
if message != "" { if message != "" {
commitPrefixConfig := self.commitPrefixConfigForRepo() commitPrefixConfig := self.commitPrefixConfigForRepo()
@ -183,7 +170,7 @@ func (self *WorkingTreeHelper) PromptToStageAllAndRetry(retry func() error) erro
Prompt: self.c.Tr.NoFilesStagedPrompt, Prompt: self.c.Tr.NoFilesStagedPrompt,
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.StageAllFiles) self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
if err := self.git.WorkingTree.StageAll(); err != nil { if err := self.c.Git().WorkingTree.StageAll(); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
if err := self.syncRefresh(); err != nil { if err := self.syncRefresh(); err != nil {
@ -204,7 +191,7 @@ func (self *WorkingTreeHelper) prepareFilesForCommit() error {
noStagedFiles := !self.AnyStagedFiles() noStagedFiles := !self.AnyStagedFiles()
if noStagedFiles && self.c.UserConfig.Gui.SkipNoStagedFilesWarning { if noStagedFiles && self.c.UserConfig.Gui.SkipNoStagedFilesWarning {
self.c.LogAction(self.c.Tr.Actions.StageAllFiles) self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
err := self.git.WorkingTree.StageAll() err := self.c.Git().WorkingTree.StageAll()
if err != nil { if err != nil {
return err return err
} }

View File

@ -0,0 +1,53 @@
package controllers
import (
"log"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
type JumpToSideWindowController struct {
baseController
c *ControllerCommon
}
func NewJumpToSideWindowController(
common *ControllerCommon,
) *JumpToSideWindowController {
return &JumpToSideWindowController{
baseController: baseController{},
c: common,
}
}
func (self *JumpToSideWindowController) Context() types.Context {
return nil
}
func (self *JumpToSideWindowController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
windows := self.c.Helpers().Window.SideWindows()
if len(opts.Config.Universal.JumpToBlock) != len(windows) {
log.Fatal("Jump to block keybindings cannot be set. Exactly 5 keybindings must be supplied.")
}
return lo.Map(windows, func(window string, index int) *types.Binding {
return &types.Binding{
ViewName: "",
// by default the keys are 1, 2, 3, etc
Key: opts.GetKey(opts.Config.Universal.JumpToBlock[index]),
Modifier: gocui.ModNone,
Handler: self.goToSideWindow(window),
}
})
}
func (self *JumpToSideWindowController) goToSideWindow(window string) func() error {
return func() error {
context := self.c.Helpers().Window.GetContextForWindow(window)
return self.c.PushContext(context)
}
}

View File

@ -6,10 +6,10 @@ import (
) )
type ListControllerFactory struct { type ListControllerFactory struct {
c *types.HelperCommon c *ControllerCommon
} }
func NewListControllerFactory(c *types.HelperCommon) *ListControllerFactory { func NewListControllerFactory(c *ControllerCommon) *ListControllerFactory {
return &ListControllerFactory{ return &ListControllerFactory{
c: c, c: c,
} }
@ -25,7 +25,7 @@ func (self *ListControllerFactory) Create(context types.IListContext) *ListContr
type ListController struct { type ListController struct {
baseController baseController
c *types.HelperCommon c *ControllerCommon
context types.IListContext context types.IListContext
} }

View File

@ -13,26 +13,30 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
) )
// after selecting the 200th commit, we'll load in all the rest
const COMMIT_THRESHOLD = 200
type ( type (
PullFilesFn func() error PullFilesFn func() error
) )
type LocalCommitsController struct { type LocalCommitsController struct {
baseController baseController
*controllerCommon c *ControllerCommon
pullFiles PullFilesFn pullFiles PullFilesFn
} }
var _ types.IController = &LocalCommitsController{} var _ types.IController = &LocalCommitsController{}
func NewLocalCommitsController( func NewLocalCommitsController(
common *controllerCommon, common *ControllerCommon,
pullFiles PullFilesFn, pullFiles PullFilesFn,
) *LocalCommitsController { ) *LocalCommitsController {
return &LocalCommitsController{ return &LocalCommitsController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
pullFiles: pullFiles, pullFiles: pullFiles,
} }
} }
@ -150,8 +154,52 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
return bindings return bindings
} }
func (self *LocalCommitsController) GetOnRenderToMain() func() error {
return func() error {
return self.c.Helpers().Diff.WithDiffModeCheck(func() error {
var task types.UpdateTask
commit := self.context().GetSelected()
if commit == nil {
task = types.NewRenderStringTask(self.c.Tr.NoCommitsThisBranch)
} else if commit.Action == todo.UpdateRef {
task = types.NewRenderStringTask(
utils.ResolvePlaceholderString(
self.c.Tr.UpdateRefHere,
map[string]string{
"ref": commit.Name,
}))
} else {
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Sha, self.c.Modes().Filtering.GetPath(), self.c.State().GetIgnoreWhitespaceInDiffView())
task = types.NewRunPtyTask(cmdObj.GetCmd())
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Patch",
Task: task,
},
Secondary: secondaryPatchPanelUpdateOpts(self.c),
})
})
}
}
func secondaryPatchPanelUpdateOpts(c *ControllerCommon) *types.ViewUpdateOpts {
if c.Git().Patch.PatchBuilder.Active() {
patch := c.Git().Patch.PatchBuilder.RenderAggregatedPatch(false)
return &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(patch),
Title: c.Tr.CustomPatch,
}
}
return nil
}
func (self *LocalCommitsController) squashDown(commit *models.Commit) error { func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 { if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit) return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
} }
@ -176,7 +224,7 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
} }
func (self *LocalCommitsController) fixup(commit *models.Commit) error { func (self *LocalCommitsController) fixup(commit *models.Commit) error {
if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 { if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit) return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
} }
@ -209,12 +257,12 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
return nil return nil
} }
commitMessage, err := self.git.Commit.GetCommitMessage(commit.Sha) commitMessage, err := self.c.Git().Commit.GetCommitMessage(commit.Sha)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.helpers.Commits.OpenCommitMessagePanel( return self.c.Helpers().Commits.OpenCommitMessagePanel(
&helpers.OpenCommitMessagePanelOpts{ &helpers.OpenCommitMessagePanelOpts{
CommitIndex: self.context().GetSelectedLineIdx(), CommitIndex: self.context().GetSelectedLineIdx(),
InitialMessage: commitMessage, InitialMessage: commitMessage,
@ -226,12 +274,12 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
} }
func (self *LocalCommitsController) handleReword(message string) error { func (self *LocalCommitsController) handleReword(message string) error {
err := self.git.Rebase.RewordCommit(self.model.Commits, self.contexts.LocalCommits.GetSelectedLineIdx(), message) err := self.c.Git().Rebase.RewordCommit(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), message)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
self.helpers.Commits.OnCommitSuccess() self.c.Helpers().Commits.OnCommitSuccess()
_ = self.helpers.Commits.PopCommitMessageContexts() _ = self.c.Helpers().Commits.PopCommitMessageContexts()
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
} }
@ -239,11 +287,11 @@ func (self *LocalCommitsController) doRewordEditor() error {
self.c.LogAction(self.c.Tr.Actions.RewordCommit) self.c.LogAction(self.c.Tr.Actions.RewordCommit)
if self.isHeadCommit() { if self.isHeadCommit() {
return self.c.RunSubprocessAndRefresh(self.os.Cmd.New("git commit --allow-empty --amend --only")) return self.c.RunSubprocessAndRefresh(self.c.OS().Cmd.New("git commit --allow-empty --amend --only"))
} }
subProcess, err := self.git.Rebase.RewordCommitInEditor( subProcess, err := self.c.Git().Rebase.RewordCommitInEditor(
self.model.Commits, self.context().GetSelectedLineIdx(), self.c.Model().Commits, self.context().GetSelectedLineIdx(),
) )
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
@ -307,8 +355,8 @@ func (self *LocalCommitsController) edit(commit *models.Commit) error {
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.EditCommit) self.c.LogAction(self.c.Tr.Actions.EditCommit)
err := self.git.Rebase.InteractiveRebaseBreakAfter(self.model.Commits, self.context().GetSelectedLineIdx()) err := self.c.Git().Rebase.InteractiveRebaseBreakAfter(self.c.Model().Commits, self.context().GetSelectedLineIdx())
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
}) })
} }
@ -327,8 +375,8 @@ func (self *LocalCommitsController) pick(commit *models.Commit) error {
} }
func (self *LocalCommitsController) interactiveRebase(action string) error { func (self *LocalCommitsController) interactiveRebase(action string) error {
err := self.git.Rebase.InteractiveRebase(self.model.Commits, self.context().GetSelectedLineIdx(), action) err := self.c.Git().Rebase.InteractiveRebase(self.c.Model().Commits, self.context().GetSelectedLineIdx(), action)
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
} }
// handleMidRebaseCommand sees if the selected commit is in fact a rebasing // handleMidRebaseCommand sees if the selected commit is in fact a rebasing
@ -336,7 +384,7 @@ func (self *LocalCommitsController) interactiveRebase(action string) error {
// begin a rebase. It then updates the todo file with that action // begin a rebase. It then updates the todo file with that action
func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) { func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) {
if !commit.IsTODO() { if !commit.IsTODO() {
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
// If we are in a rebase, the only action that is allowed for // If we are in a rebase, the only action that is allowed for
// non-todo commits is rewording the current head commit // non-todo commits is rewording the current head commit
if !(action == todo.Reword && self.isHeadCommit()) { if !(action == todo.Reword && self.isHeadCommit()) {
@ -365,7 +413,7 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma
false, false,
) )
if err := self.git.Rebase.EditRebaseTodo(commit, action); err != nil { if err := self.c.Git().Rebase.EditRebaseTodo(commit, action); err != nil {
return false, self.c.Error(err) return false, self.c.Error(err)
} }
@ -376,7 +424,7 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma
func (self *LocalCommitsController) moveDown(commit *models.Commit) error { func (self *LocalCommitsController) moveDown(commit *models.Commit) error {
index := self.context().GetSelectedLineIdx() index := self.context().GetSelectedLineIdx()
commits := self.model.Commits commits := self.c.Model().Commits
// can't move past the initial commit // can't move past the initial commit
if index >= len(commits)-1 { if index >= len(commits)-1 {
@ -393,7 +441,7 @@ func (self *LocalCommitsController) moveDown(commit *models.Commit) error {
self.c.LogAction(self.c.Tr.Actions.MoveCommitDown) self.c.LogAction(self.c.Tr.Actions.MoveCommitDown)
self.c.LogCommand(fmt.Sprintf("Moving commit %s down", commit.ShortSha()), false) self.c.LogCommand(fmt.Sprintf("Moving commit %s down", commit.ShortSha()), false)
if err := self.git.Rebase.MoveTodoDown(commit); err != nil { if err := self.c.Git().Rebase.MoveTodoDown(commit); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
self.context().MoveSelectedLine(1) self.context().MoveSelectedLine(1)
@ -402,17 +450,17 @@ func (self *LocalCommitsController) moveDown(commit *models.Commit) error {
}) })
} }
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
} }
return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error { return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.MoveCommitDown) self.c.LogAction(self.c.Tr.Actions.MoveCommitDown)
err := self.git.Rebase.MoveCommitDown(self.model.Commits, index) err := self.c.Git().Rebase.MoveCommitDown(self.c.Model().Commits, index)
if err == nil { if err == nil {
self.context().MoveSelectedLine(1) self.context().MoveSelectedLine(1)
} }
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
}) })
} }
@ -431,7 +479,7 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error {
false, false,
) )
if err := self.git.Rebase.MoveTodoUp(self.model.Commits[index]); err != nil { if err := self.c.Git().Rebase.MoveTodoUp(self.c.Model().Commits[index]); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
self.context().MoveSelectedLine(-1) self.context().MoveSelectedLine(-1)
@ -440,29 +488,29 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error {
}) })
} }
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
} }
return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error { return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.MoveCommitUp) self.c.LogAction(self.c.Tr.Actions.MoveCommitUp)
err := self.git.Rebase.MoveCommitDown(self.model.Commits, index-1) err := self.c.Git().Rebase.MoveCommitDown(self.c.Model().Commits, index-1)
if err == nil { if err == nil {
self.context().MoveSelectedLine(-1) self.context().MoveSelectedLine(-1)
} }
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
}) })
} }
func (self *LocalCommitsController) amendTo(commit *models.Commit) error { func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
if self.isHeadCommit() { if self.isHeadCommit() {
if err := self.helpers.AmendHelper.AmendHead(); err != nil { if err := self.c.Helpers().AmendHelper.AmendHead(); err != nil {
return err return err
} }
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
} }
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
} }
@ -472,15 +520,15 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
HandleConfirm: func() error { HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error { return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.AmendCommit) self.c.LogAction(self.c.Tr.Actions.AmendCommit)
err := self.git.Rebase.AmendTo(commit) err := self.c.Git().Rebase.AmendTo(commit)
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
}) })
}, },
}) })
} }
func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error { func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error {
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && !self.isHeadCommit() { if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE && !self.isHeadCommit() {
return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
} }
@ -506,7 +554,7 @@ func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error
func (self *LocalCommitsController) resetAuthor() error { func (self *LocalCommitsController) resetAuthor() error {
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error { return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.ResetCommitAuthor) self.c.LogAction(self.c.Tr.Actions.ResetCommitAuthor)
if err := self.git.Rebase.ResetCommitAuthor(self.model.Commits, self.context().GetSelectedLineIdx()); err != nil { if err := self.c.Git().Rebase.ResetCommitAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx()); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -517,11 +565,11 @@ func (self *LocalCommitsController) resetAuthor() error {
func (self *LocalCommitsController) setAuthor() error { func (self *LocalCommitsController) setAuthor() error {
return self.c.Prompt(types.PromptOpts{ return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.SetAuthorPromptTitle, Title: self.c.Tr.SetAuthorPromptTitle,
FindSuggestionsFunc: self.helpers.Suggestions.GetAuthorsSuggestionsFunc(), FindSuggestionsFunc: self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc(),
HandleConfirm: func(value string) error { HandleConfirm: func(value string) error {
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error { return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.SetCommitAuthor) self.c.LogAction(self.c.Tr.Actions.SetCommitAuthor)
if err := self.git.Rebase.SetCommitAuthor(self.model.Commits, self.context().GetSelectedLineIdx(), value); err != nil { if err := self.c.Git().Rebase.SetCommitAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx(), value); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -544,7 +592,7 @@ func (self *LocalCommitsController) revert(commit *models.Commit) error {
}), }),
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.RevertCommit) self.c.LogAction(self.c.Tr.Actions.RevertCommit)
if err := self.git.Commit.Revert(commit.Sha); err != nil { if err := self.c.Git().Commit.Revert(commit.Sha); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.afterRevertCommit() return self.afterRevertCommit()
@ -557,7 +605,7 @@ func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.C
menuItems := make([]*types.MenuItem, len(commit.Parents)) menuItems := make([]*types.MenuItem, len(commit.Parents))
for i, parentSha := range commit.Parents { for i, parentSha := range commit.Parents {
i := i i := i
message, err := self.git.Commit.GetCommitMessageFirstLine(parentSha) message, err := self.c.Git().Commit.GetCommitMessageFirstLine(parentSha)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -567,7 +615,7 @@ func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.C
OnPress: func() error { OnPress: func() error {
parentNumber := i + 1 parentNumber := i + 1
self.c.LogAction(self.c.Tr.Actions.RevertCommit) self.c.LogAction(self.c.Tr.Actions.RevertCommit)
if err := self.git.Commit.RevertMerge(commit.Sha, parentNumber); err != nil { if err := self.c.Git().Commit.RevertMerge(commit.Sha, parentNumber); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.afterRevertCommit() return self.afterRevertCommit()
@ -598,7 +646,7 @@ func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) err
Prompt: prompt, Prompt: prompt,
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit) self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit)
if err := self.git.Commit.CreateFixupCommit(commit.Sha); err != nil { if err := self.c.Git().Commit.CreateFixupCommit(commit.Sha); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@ -619,15 +667,15 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co
HandleConfirm: func() error { HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error { return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits) self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits)
err := self.git.Rebase.SquashAllAboveFixupCommits(commit) err := self.c.Git().Rebase.SquashAllAboveFixupCommits(commit)
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
}) })
}, },
}) })
} }
func (self *LocalCommitsController) createTag(commit *models.Commit) error { func (self *LocalCommitsController) createTag(commit *models.Commit) error {
return self.helpers.Tags.CreateTagMenu(commit.Sha, func() {}) return self.c.Helpers().Tags.CreateTagMenu(commit.Sha, func() {})
} }
func (self *LocalCommitsController) openSearch() error { func (self *LocalCommitsController) openSearch() error {
@ -759,20 +807,36 @@ func (self *LocalCommitsController) checkSelected(callback func(*models.Commit)
} }
} }
func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error {
context := self.context()
if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() {
context.SetLimitCommits(false)
go utils.Safe(func() {
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}); err != nil {
_ = self.c.Error(err)
}
})
}
return nil
}
}
func (self *LocalCommitsController) Context() types.Context { func (self *LocalCommitsController) Context() types.Context {
return self.context() return self.context()
} }
func (self *LocalCommitsController) context() *context.LocalCommitsContext { func (self *LocalCommitsController) context() *context.LocalCommitsContext {
return self.contexts.LocalCommits return self.c.Contexts().LocalCommits
} }
func (self *LocalCommitsController) paste() error { func (self *LocalCommitsController) paste() error {
return self.helpers.CherryPick.Paste() return self.c.Helpers().CherryPick.Paste()
} }
func (self *LocalCommitsController) isHeadCommit() bool { func (self *LocalCommitsController) isHeadCommit() bool {
return models.IsHeadCommit(self.model.Commits, self.context().GetSelectedLineIdx()) return models.IsHeadCommit(self.c.Model().Commits, self.context().GetSelectedLineIdx())
} }
func isChangeOfRebaseTodoAllowed(action todo.TodoCommand) bool { func isChangeOfRebaseTodoAllowed(action todo.TodoCommand) bool {

View File

@ -7,17 +7,17 @@ import (
type MenuController struct { type MenuController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
var _ types.IController = &MenuController{} var _ types.IController = &MenuController{}
func NewMenuController( func NewMenuController(
common *controllerCommon, common *ControllerCommon,
) *MenuController { ) *MenuController {
return &MenuController{ return &MenuController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
@ -28,12 +28,16 @@ func (self *MenuController) GetKeybindings(opts types.KeybindingsOpts) []*types.
Handler: self.press, Handler: self.press,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Confirm), Key: opts.GetKey(opts.Config.Universal.Confirm),
Handler: self.press, Handler: self.press,
Description: self.c.Tr.LcExecute,
Display: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Return), Key: opts.GetKey(opts.Config.Universal.Return),
Handler: self.close, Handler: self.close,
Description: self.c.Tr.LcClose,
Display: true,
}, },
} }
@ -44,6 +48,14 @@ func (self *MenuController) GetOnClick() func() error {
return self.press return self.press
} }
func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error {
selectedMenuItem := self.context().GetSelected()
self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip)
return nil
}
}
func (self *MenuController) press() error { func (self *MenuController) press() error {
return self.context().OnMenuPress(self.context().GetSelected()) return self.context().OnMenuPress(self.context().GetSelected())
} }
@ -57,5 +69,5 @@ func (self *MenuController) Context() types.Context {
} }
func (self *MenuController) context() *context.MenuContext { func (self *MenuController) context() *context.MenuContext {
return self.contexts.Menu return self.c.Contexts().Menu
} }

View File

@ -11,17 +11,17 @@ import (
type MergeConflictsController struct { type MergeConflictsController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
var _ types.IController = &MergeConflictsController{} var _ types.IController = &MergeConflictsController{}
func NewMergeConflictsController( func NewMergeConflictsController(
common *controllerCommon, common *ControllerCommon,
) *MergeConflictsController { ) *MergeConflictsController {
return &MergeConflictsController{ return &MergeConflictsController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
@ -41,21 +41,25 @@ func (self *MergeConflictsController) GetKeybindings(opts types.KeybindingsOpts)
Key: opts.GetKey(opts.Config.Universal.PrevBlock), Key: opts.GetKey(opts.Config.Universal.PrevBlock),
Handler: self.withRenderAndFocus(self.PrevConflict), Handler: self.withRenderAndFocus(self.PrevConflict),
Description: self.c.Tr.PrevConflict, Description: self.c.Tr.PrevConflict,
Display: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.NextBlock), Key: opts.GetKey(opts.Config.Universal.NextBlock),
Handler: self.withRenderAndFocus(self.NextConflict), Handler: self.withRenderAndFocus(self.NextConflict),
Description: self.c.Tr.NextConflict, Description: self.c.Tr.NextConflict,
Display: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.PrevItem), Key: opts.GetKey(opts.Config.Universal.PrevItem),
Handler: self.withRenderAndFocus(self.PrevConflictHunk), Handler: self.withRenderAndFocus(self.PrevConflictHunk),
Description: self.c.Tr.SelectPrevHunk, Description: self.c.Tr.SelectPrevHunk,
Display: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.NextItem), Key: opts.GetKey(opts.Config.Universal.NextItem),
Handler: self.withRenderAndFocus(self.NextConflictHunk), Handler: self.withRenderAndFocus(self.NextConflictHunk),
Description: self.c.Tr.SelectNextHunk, Description: self.c.Tr.SelectNextHunk,
Display: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.PrevBlockAlt), Key: opts.GetKey(opts.Config.Universal.PrevBlockAlt),
@ -89,21 +93,24 @@ func (self *MergeConflictsController) GetKeybindings(opts types.KeybindingsOpts)
Key: opts.GetKey(opts.Config.Universal.Undo), Key: opts.GetKey(opts.Config.Universal.Undo),
Handler: self.withRenderAndFocus(self.HandleUndo), Handler: self.withRenderAndFocus(self.HandleUndo),
Description: self.c.Tr.LcUndo, Description: self.c.Tr.LcUndo,
Display: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Files.OpenMergeTool), Key: opts.GetKey(opts.Config.Files.OpenMergeTool),
Handler: self.helpers.WorkingTree.OpenMergeTool, Handler: self.c.Helpers().WorkingTree.OpenMergeTool,
Description: self.c.Tr.LcOpenMergeTool, Description: self.c.Tr.LcOpenMergeTool,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Select), Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.withRenderAndFocus(self.HandlePickHunk), Handler: self.withRenderAndFocus(self.HandlePickHunk),
Description: self.c.Tr.PickHunk, Description: self.c.Tr.PickHunk,
Display: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Main.PickBothHunks), Key: opts.GetKey(opts.Config.Main.PickBothHunks),
Handler: self.withRenderAndFocus(self.HandlePickAllHunks), Handler: self.withRenderAndFocus(self.HandlePickAllHunks),
Description: self.c.Tr.PickAllHunks, Description: self.c.Tr.PickAllHunks,
Display: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Return), Key: opts.GetKey(opts.Config.Universal.Return),
@ -134,6 +141,24 @@ func (self *MergeConflictsController) GetMouseKeybindings(opts types.Keybindings
} }
} }
func (self *MergeConflictsController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error {
self.c.Views().MergeConflicts.Wrap = false
return self.c.Helpers().MergeConflicts.Render(true)
}
}
func (self *MergeConflictsController) GetOnFocusLost() func(types.OnFocusLostOpts) error {
return func(types.OnFocusLostOpts) error {
self.context().SetUserScrolling(false)
self.context().GetState().ResetConflictSelection()
self.c.Views().MergeConflicts.Wrap = true
return nil
}
}
func (self *MergeConflictsController) HandleScrollUp() error { func (self *MergeConflictsController) HandleScrollUp() error {
self.context().SetUserScrolling(true) self.context().SetUserScrolling(true)
self.context().GetViewTrait().ScrollUp(self.c.UserConfig.Gui.ScrollHeight) self.context().GetViewTrait().ScrollUp(self.c.UserConfig.Gui.ScrollHeight)
@ -153,7 +178,7 @@ func (self *MergeConflictsController) Context() types.Context {
} }
func (self *MergeConflictsController) context() *context.MergeConflictsContext { func (self *MergeConflictsController) context() *context.MergeConflictsContext {
return self.contexts.MergeConflicts return self.c.Contexts().MergeConflicts
} }
func (self *MergeConflictsController) Escape() error { func (self *MergeConflictsController) Escape() error {
@ -162,11 +187,11 @@ func (self *MergeConflictsController) Escape() error {
func (self *MergeConflictsController) HandleEditFile() error { func (self *MergeConflictsController) HandleEditFile() error {
lineNumber := self.context().GetState().GetSelectedLine() lineNumber := self.context().GetState().GetSelectedLine()
return self.helpers.Files.EditFileAtLine(self.context().GetState().GetPath(), lineNumber) return self.c.Helpers().Files.EditFileAtLine(self.context().GetState().GetPath(), lineNumber)
} }
func (self *MergeConflictsController) HandleOpenFile() error { func (self *MergeConflictsController) HandleOpenFile() error {
return self.helpers.Files.OpenFile(self.context().GetState().GetPath()) return self.c.Helpers().Files.OpenFile(self.context().GetState().GetPath())
} }
func (self *MergeConflictsController) HandleScrollLeft() error { func (self *MergeConflictsController) HandleScrollLeft() error {

View File

@ -1,23 +1,52 @@
package gui package controllers
import ( import (
"log"
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo" "github.com/samber/lo"
) )
func (gui *Gui) getBindings(context types.Context) []*types.Binding { type OptionsMenuAction struct {
c *ControllerCommon
}
func (self *OptionsMenuAction) Call() error {
ctx := self.c.CurrentContext()
// Don't show menu while displaying popup.
if ctx.GetKind() == types.PERSISTENT_POPUP || ctx.GetKind() == types.TEMPORARY_POPUP {
return nil
}
bindings := self.getBindings(ctx)
menuItems := slices.Map(bindings, func(binding *types.Binding) *types.MenuItem {
return &types.MenuItem{
OpensMenu: binding.OpensMenu,
Label: binding.Description,
OnPress: func() error {
if binding.Handler == nil {
return nil
}
return binding.Handler()
},
Key: binding.Key,
Tooltip: binding.Tooltip,
}
})
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.MenuTitle,
Items: menuItems,
HideCancel: true,
})
}
func (self *OptionsMenuAction) getBindings(context types.Context) []*types.Binding {
var bindingsGlobal, bindingsPanel, bindingsNavigation []*types.Binding var bindingsGlobal, bindingsPanel, bindingsNavigation []*types.Binding
bindings, _ := gui.GetInitialKeybindings() bindings, _ := self.c.GetInitialKeybindingsWithCustomCommands()
customBindings, err := gui.CustomCommandsClient.GetCustomCommandKeybindings()
if err != nil {
log.Fatal(err)
}
bindings = append(customBindings, bindings...)
for _, binding := range bindings { for _, binding := range bindings {
if keybindings.LabelFromKey(binding.Key) != "" && binding.Description != "" { if keybindings.LabelFromKey(binding.Key) != "" && binding.Description != "" {
@ -48,35 +77,3 @@ func uniqueBindings(bindings []*types.Binding) []*types.Binding {
return binding.Description return binding.Description
}) })
} }
func (gui *Gui) handleCreateOptionsMenu() error {
ctx := gui.currentContext()
// Don't show menu while displaying popup.
if ctx.GetKind() == types.PERSISTENT_POPUP || ctx.GetKind() == types.TEMPORARY_POPUP {
return nil
}
bindings := gui.getBindings(ctx)
menuItems := slices.Map(bindings, func(binding *types.Binding) *types.MenuItem {
return &types.MenuItem{
OpensMenu: binding.OpensMenu,
Label: binding.Description,
OnPress: func() error {
if binding.Handler == nil {
return nil
}
return binding.Handler()
},
Key: binding.Key,
Tooltip: binding.Tooltip,
}
})
return gui.c.Menu(types.CreateMenuOptions{
Title: gui.c.Tr.MenuTitle,
Items: menuItems,
HideCancel: true,
})
}

View File

@ -8,17 +8,17 @@ import (
type PatchBuildingController struct { type PatchBuildingController struct {
baseController baseController
*controllerCommon c *ControllerCommon
} }
var _ types.IController = &PatchBuildingController{} var _ types.IController = &PatchBuildingController{}
func NewPatchBuildingController( func NewPatchBuildingController(
common *controllerCommon, common *ControllerCommon,
) *PatchBuildingController { ) *PatchBuildingController {
return &PatchBuildingController{ return &PatchBuildingController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: common, c: common,
} }
} }
@ -48,42 +48,63 @@ func (self *PatchBuildingController) GetKeybindings(opts types.KeybindingsOpts)
} }
func (self *PatchBuildingController) Context() types.Context { func (self *PatchBuildingController) Context() types.Context {
return self.contexts.CustomPatchBuilder return self.c.Contexts().CustomPatchBuilder
} }
func (self *PatchBuildingController) context() types.IPatchExplorerContext { func (self *PatchBuildingController) context() types.IPatchExplorerContext {
return self.contexts.CustomPatchBuilder return self.c.Contexts().CustomPatchBuilder
} }
func (self *PatchBuildingController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding { func (self *PatchBuildingController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{} return []*gocui.ViewMouseBinding{}
} }
func (self *PatchBuildingController) GetOnFocus() func(types.OnFocusOpts) error {
return func(opts types.OnFocusOpts) error {
// no need to change wrap on the secondary view because it can't be interacted with
self.c.Views().PatchBuilding.Wrap = false
return self.c.Helpers().PatchBuilding.RefreshPatchBuildingPanel(opts)
}
}
func (self *PatchBuildingController) GetOnFocusLost() func(types.OnFocusLostOpts) error {
return func(opts types.OnFocusLostOpts) error {
self.c.Views().PatchBuilding.Wrap = true
if self.c.Git().Patch.PatchBuilder.IsEmpty() {
self.c.Git().Patch.PatchBuilder.Reset()
}
return nil
}
}
func (self *PatchBuildingController) OpenFile() error { func (self *PatchBuildingController) OpenFile() error {
self.context().GetMutex().Lock() self.context().GetMutex().Lock()
defer self.context().GetMutex().Unlock() defer self.context().GetMutex().Unlock()
path := self.contexts.CommitFiles.GetSelectedPath() path := self.c.Contexts().CommitFiles.GetSelectedPath()
if path == "" { if path == "" {
return nil return nil
} }
return self.helpers.Files.OpenFile(path) return self.c.Helpers().Files.OpenFile(path)
} }
func (self *PatchBuildingController) EditFile() error { func (self *PatchBuildingController) EditFile() error {
self.context().GetMutex().Lock() self.context().GetMutex().Lock()
defer self.context().GetMutex().Unlock() defer self.context().GetMutex().Unlock()
path := self.contexts.CommitFiles.GetSelectedPath() path := self.c.Contexts().CommitFiles.GetSelectedPath()
if path == "" { if path == "" {
return nil return nil
} }
lineNumber := self.context().GetState().CurrentLineNumber() lineNumber := self.context().GetState().CurrentLineNumber()
return self.helpers.Files.EditFileAtLine(path, lineNumber) return self.c.Helpers().Files.EditFileAtLine(path, lineNumber)
} }
func (self *PatchBuildingController) ToggleSelectionAndRefresh() error { func (self *PatchBuildingController) ToggleSelectionAndRefresh() error {
@ -100,21 +121,21 @@ func (self *PatchBuildingController) toggleSelection() error {
self.context().GetMutex().Lock() self.context().GetMutex().Lock()
defer self.context().GetMutex().Unlock() defer self.context().GetMutex().Unlock()
toggleFunc := self.git.Patch.PatchBuilder.AddFileLineRange toggleFunc := self.c.Git().Patch.PatchBuilder.AddFileLineRange
filename := self.contexts.CommitFiles.GetSelectedPath() filename := self.c.Contexts().CommitFiles.GetSelectedPath()
if filename == "" { if filename == "" {
return nil return nil
} }
state := self.context().GetState() state := self.context().GetState()
includedLineIndices, err := self.git.Patch.PatchBuilder.GetFileIncLineIndices(filename) includedLineIndices, err := self.c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename)
if err != nil { if err != nil {
return err return err
} }
currentLineIsStaged := lo.Contains(includedLineIndices, state.GetSelectedLineIdx()) currentLineIsStaged := lo.Contains(includedLineIndices, state.GetSelectedLineIdx())
if currentLineIsStaged { if currentLineIsStaged {
toggleFunc = self.git.Patch.PatchBuilder.RemoveFileLineRange toggleFunc = self.c.Git().Patch.PatchBuilder.RemoveFileLineRange
} }
// add range of lines to those set for the file // add range of lines to those set for the file
@ -133,5 +154,5 @@ func (self *PatchBuildingController) toggleSelection() error {
} }
func (self *PatchBuildingController) Escape() error { func (self *PatchBuildingController) Escape() error {
return self.helpers.PatchBuilding.Escape() return self.c.Helpers().PatchBuilding.Escape()
} }

View File

@ -6,26 +6,26 @@ import (
) )
type PatchExplorerControllerFactory struct { type PatchExplorerControllerFactory struct {
*controllerCommon c *ControllerCommon
} }
func NewPatchExplorerControllerFactory(c *controllerCommon) *PatchExplorerControllerFactory { func NewPatchExplorerControllerFactory(c *ControllerCommon) *PatchExplorerControllerFactory {
return &PatchExplorerControllerFactory{ return &PatchExplorerControllerFactory{
controllerCommon: c, c: c,
} }
} }
func (self *PatchExplorerControllerFactory) Create(context types.IPatchExplorerContext) *PatchExplorerController { func (self *PatchExplorerControllerFactory) Create(context types.IPatchExplorerContext) *PatchExplorerController {
return &PatchExplorerController{ return &PatchExplorerController{
baseController: baseController{}, baseController: baseController{},
controllerCommon: self.controllerCommon, c: self.c,
context: context, context: context,
} }
} }
type PatchExplorerController struct { type PatchExplorerController struct {
baseController baseController
*controllerCommon c *ControllerCommon
context types.IPatchExplorerContext context types.IPatchExplorerContext
} }
@ -254,7 +254,7 @@ func (self *PatchExplorerController) CopySelectedToClipboard() error {
selected := self.context.GetState().PlainRenderSelected() selected := self.context.GetState().PlainRenderSelected()
self.c.LogAction(self.c.Tr.Actions.CopySelectedTextToClipboard) self.c.LogAction(self.c.Tr.Actions.CopySelectedTextToClipboard)
if err := self.os.CopyToClipboard(selected); err != nil { if err := self.c.OS().CopyToClipboard(selected); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }

View File

@ -0,0 +1,75 @@
package controllers
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type QuitActions struct {
c *ControllerCommon
}
func (self *QuitActions) Quit() error {
self.c.State().SetRetainOriginalDir(false)
return self.quitAux()
}
func (self *QuitActions) QuitWithoutChangingDirectory() error {
self.c.State().SetRetainOriginalDir(true)
return self.quitAux()
}
func (self *QuitActions) quitAux() error {
if self.c.State().GetUpdating() {
return self.confirmQuitDuringUpdate()
}
if self.c.UserConfig.ConfirmOnQuit {
return self.c.Confirm(types.ConfirmOpts{
Title: "",
Prompt: self.c.Tr.ConfirmQuit,
HandleConfirm: func() error {
return gocui.ErrQuit
},
})
}
return gocui.ErrQuit
}
func (self *QuitActions) confirmQuitDuringUpdate() error {
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.ConfirmQuitDuringUpdateTitle,
Prompt: self.c.Tr.ConfirmQuitDuringUpdate,
HandleConfirm: func() error {
return gocui.ErrQuit
},
})
}
func (self *QuitActions) Escape() error {
currentContext := self.c.CurrentContext()
parentContext, hasParent := currentContext.GetParentContext()
if hasParent && currentContext != nil && parentContext != nil {
// TODO: think about whether this should be marked as a return rather than adding to the stack
return self.c.PushContext(parentContext)
}
for _, mode := range self.c.Helpers().Mode.Statuses() {
if mode.IsActive() {
return mode.Reset()
}
}
repoPathStack := self.c.State().GetRepoPathStack()
if !repoPathStack.IsEmpty() {
return self.c.Helpers().Repos.DispatchSwitchToRepo(repoPathStack.Pop(), true)
}
if self.c.UserConfig.QuitOnTopLevelReturn {
return self.Quit()
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More