Compare commits

..
Author SHA1 Message Date
Jesse Duffield 7357c1daea Only apply directory-level actions to visible files 2026-02-09 08:12:50 +11:00
Jesse DuffieldandClaude Opus 4.6 75488f1041 Filter file views rather than search
Change working tree files and commit files panels to use filtering
(reducing the list) instead of search (highlighting matches). This
matches the behavior of other filterable views.

The text filter matches against the full file path, not just the
filename, which is more useful for navigating large directory trees.

When toggling a directory for a custom patch while a text filter is
active, all files in the directory are included (not just the visible
filtered ones), consistent with how staging a directory in the files
panel works.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 08:12:50 +11:00
Jesse DuffieldandClaude Opus 4.6 3db8175bb5 Rename GetFilter to GetStatusFilter on IFileTree
This avoids a naming collision with GetFilter from the
IFilterableContext interface, which will be implemented by
FileTreeViewModel in the next commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 08:12:50 +11:00
Jesse Duffield 5f8bca6508 Allow removing lines from patch directly
I always press 'd' in the patch building view, expecting that I can do
exactly what I can do in the staging view, to find out I need to go
space -> ctrl+p -> d and I think it's time to honour the muscle memory
and support this convenience keybinding.
2026-02-09 08:12:41 +11:00
Jesse Duffield 3b677cbcfe Fetch PRs after checkout (not blocking UI)
If you check out a new branch via the remote branches tab you'd want to
see its status
2026-02-08 10:14:14 +11:00
Jesse Duffield 658dbd2bdf Improve github repo picking UX
We now auto-select a remote if:
* it's the only remote
* it's named origin

Otherwise we prompt the user to specify the remote they want to use.
2026-02-08 09:53:39 +11:00
Jesse Duffield a9a13619fd Fetch PRs newest-first
Some branch names are re-used across PRs, so now we sort PRs by
newest-first when fetching so we don't get stale PRs.
2026-02-08 09:51:34 +11:00
Jesse Duffield 5d452122b3 Stop fetch github PRs when checking out a branch
They aren't going to change when you check out a branch, and they take a
long time to fetch.
2026-02-07 22:18:27 +11:00
Jesse Duffield f70b0a214b Cache Github PRs for snappier startup experience 2026-02-07 22:18:27 +11:00
Jesse DuffieldandClaude Opus 4.6 b81b6e3c2e Fix PR integration: nerd font guards, tests, generated files, typo
- Fix "if if" typo in enableGithubCli config description
- Guard nerd font icons in PR state badges and main view display
  behind icons.IsIconEnabled() so non-nerd-font users see plain text
- Add tests for GenerateGithubPullRequestMap and getRepoInfoFromURL
- Update branch display test expectations for new icon column
- Fix integration tests that matched "* branchname" patterns
  (now need to account for icon column between recency and name)
- Remove commented-out dead code in branches.go
- Regenerate docs and schema with corrected config description

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:18:27 +11:00
Stefan Haller 4bed4b8965 WIP Add draft state, iterate on UI design 2026-02-07 20:29:07 +11:00
Stefan Haller 4fab224ccd Show PR information in main view above the branch log when a branch is selected 2026-02-07 20:29:07 +11:00
Stefan Haller 95005999e6 Use github icon instead of just coloring the branch icon 2026-02-07 20:29:07 +11:00
Stefan Haller f84cff3ba5 WIP use origin remote
I don't know what I'm doing here, but this seems to be needed to make it work
for me.
2026-02-07 20:29:07 +11:00
Jesse Duffield 2ae87d3d0f WIP 2026-02-07 20:29:07 +11:00
Jesse Duffield a2b2fa7a5e Show github pull request status against branch 2026-02-07 20:29:07 +11:00
155 changed files with 38639 additions and 6489 deletions
+3 -17
View File
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -481,6 +481,10 @@ git:
# to 40 to disable truncation.
truncateCopiedCommitHashesTo: 12
# If true and if `gh` is installed and on version >=2, we will use `gh` to
# display pull requests against branches.
enableGithubCli: true
# Periodic update checks
update:
# One of: 'prompt' (default) | 'background' | 'never'
@@ -666,7 +670,6 @@ keybinding:
checkForUpdate: u
recentRepos: <enter>
allBranchesLogGraph: a
allBranchesLogGraphReverse: A
files:
commitChanges: c
commitChangesWithoutHook: w
+4 -5
View File
@@ -60,7 +60,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` <c-o> `` | Copy path to clipboard | |
| `` y `` | Copy to clipboard | |
| `` c `` | Checkout | Checkout file. This replaces the file in your working tree with the version from the selected commit. |
| `` d `` | Discard | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
| `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
| `` o `` | Open file | Open file in default application. |
| `` e `` | Edit | Open file in external editor. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
@@ -84,7 +84,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
| `` b `` | View bisect options | |
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
@@ -275,7 +275,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <space> `` | Checkout | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
@@ -353,14 +353,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` u `` | Check for update | |
| `` <enter> `` | Switch to a recent repo | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## Sub-commits
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <space> `` | Checkout | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
+4 -5
View File
@@ -64,7 +64,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | コミットハッシュをクリップボードにコピー | |
| `` <c-r> `` | コピーされた(チェリーピックされた)コミットの選択をリセット | |
| `` b `` | bisectオプションを表示 | |
| `` s `` | スカッシュ | 選択したコミットをその下のコミットにスカッシュします。スカッシュとは複数のコミットを1つにまとめる操作です。選択したコミットのメッセージが下のコミットに追加されます。 |
@@ -108,7 +108,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` <c-o> `` | パスをクリップボードにコピー | |
| `` y `` | クリップボードにコピー | |
| `` c `` | チェックアウト(ブランチの切り替え) | ファイルをチェックアウトします。これにより、作業ツリー内のファイルが選択したコミットのバージョンに置き換えられます。 |
| `` d `` | 破棄 | このコミットのこのファイルへの変更を破棄します。これはバックグラウンドで対話的なリベースを実行するため、後のコミットでもこのファイルが変更されている場合、マージコンフリクトが発生する可能性があります。 |
| `` d `` | 削除 | このコミットのこのファイルへの変更を破棄します。これはバックグラウンドで対話的なリベースを実行するため、後のコミットでもこのファイルが変更されている場合、マージコンフリクトが発生する可能性があります。 |
| `` o `` | ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
| `` e `` | 編集 | 外部エディタでファイルを開きます。 |
| `` <c-t> `` | 外部差分ツールを開く(git difftool) | |
@@ -132,7 +132,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | コミットハッシュをクリップボードにコピー | |
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したコミットをデタッチドヘッド(特定のブランチに属さない状態)としてチェックアウトします。 |
| `` y `` | コミット属性をクリップボードにコピー | コミット属性をクリップボードにコピーします(例:ハッシュ、URL、差分、メッセージ、作者)。 |
| `` o `` | ブラウザでコミットを開く | |
@@ -185,7 +185,6 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` u `` | 更新を確認 | |
| `` <enter> `` | 最近のリポジトリをチェックアウト | |
| `` a `` | ブランチログの表示モードを順に切り替え | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | メインビューにフォーカス | |
## セカンダリ
@@ -321,7 +320,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | コミットハッシュをクリップボードにコピー | |
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したコミットをデタッチドヘッド(特定のブランチに属さない状態)としてチェックアウトします。 |
| `` y `` | コミット属性をクリップボードにコピー | コミット属性をクリップボードにコピーします(例:ハッシュ、URL、差分、メッセージ、作者)。 |
| `` o `` | ブラウザでコミットを開く | |
+4 -5
View File
@@ -64,7 +64,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 커밋 해시를 클립보드에 복사 | |
| `` <space> `` | 체크아웃 | Checkout the selected commit as a detached HEAD. |
| `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 브라우저에서 커밋 열기 | |
@@ -106,7 +106,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 커밋 해시를 클립보드에 복사 | |
| `` <space> `` | 체크아웃 | Checkout the selected commit as a detached HEAD. |
| `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 브라우저에서 커밋 열기 | |
@@ -243,7 +243,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` u `` | 업데이트 확인 | |
| `` <enter> `` | 최근에 사용한 저장소로 전환 | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## 서브모듈
@@ -295,7 +294,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 커밋 해시를 클립보드에 복사 | |
| `` <c-r> `` | Reset cherry-picked (copied) commits selection | |
| `` b `` | Bisect 옵션 보기 | |
| `` s `` | 스쿼시 | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
@@ -339,7 +338,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` <c-o> `` | 파일명을 클립보드에 복사 | |
| `` y `` | 클립보드에 복사 | |
| `` c `` | 체크아웃 | Checkout file |
| `` d `` | View 'discard changes' options | Discard this commit's changes to this file |
| `` d `` | Remove | Discard this commit's changes to this file |
| `` o `` | 파일 닫기 | Open file in default application. |
| `` e `` | Edit | Open file in external editor. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
+4 -5
View File
@@ -138,7 +138,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` <c-o> `` | Kopieer de bestandsnaam naar het klembord | |
| `` y `` | Copy to clipboard | |
| `` c `` | Uitchecken | Bestand uitchecken |
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | Uitsluit deze commit zijn veranderingen aan dit bestand |
| `` d `` | Remove | Uitsluit deze commit zijn veranderingen aan dit bestand |
| `` o `` | Open bestand | Open file in default application. |
| `` e `` | Edit | Open file in external editor. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
@@ -155,7 +155,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopieer commit hash naar klembord | |
| `` <c-r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
| `` b `` | View bisect options | |
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
@@ -253,7 +253,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopieer commit hash naar klembord | |
| `` <space> `` | Uitchecken | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
@@ -353,14 +353,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` u `` | Check voor updates | |
| `` <enter> `` | Wissel naar een recente repo | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## Sub-commits
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopieer commit hash naar klembord | |
| `` <space> `` | Uitchecken | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
+4 -5
View File
@@ -57,7 +57,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopiuj hash commita do schowka | |
| `` <c-r> `` | Resetuj wybrane (cherry-picked) commity | |
| `` b `` | Zobacz opcje bisect | |
| `` s `` | Scal | Scal wybrany commit z commitami poniżej. Wiadomość wybranego commita zostanie dołączona do commita poniżej. |
@@ -269,7 +269,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` <c-o> `` | Kopiuj ścieżkę do schowka | |
| `` y `` | Kopiuj do schowka | |
| `` c `` | Przełącz | Przełącz plik. Zastępuje plik w twoim drzewie roboczym wersją z wybranego commita. |
| `` d `` | Odrzuć | Odrzuć zmiany w tym pliku z tego commita. Uruchamia interaktywny rebase w tle, więc możesz otrzymać konflikt scalania, jeśli późniejszy commit również zmienia ten plik. |
| `` d `` | Usuń | Odrzuć zmiany w tym pliku z tego commita. Uruchamia interaktywny rebase w tle, więc możesz otrzymać konflikt scalania, jeśli późniejszy commit również zmienia ten plik. |
| `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. |
| `` e `` | Edytuj | Otwórz plik w zewnętrznym edytorze. |
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
@@ -293,7 +293,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopiuj hash commita do schowka | |
| `` <space> `` | Przełącz | Przełącz wybrany commit jako odłączoną HEAD. |
| `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). |
| `` o `` | Otwórz commit w przeglądarce | |
@@ -332,14 +332,13 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` u `` | Sprawdź aktualizacje | |
| `` <enter> `` | Przełącz na ostatnie repozytorium | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## Sub-commity
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopiuj hash commita do schowka | |
| `` <space> `` | Przełącz | Przełącz wybrany commit jako odłączoną HEAD. |
| `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). |
| `` o `` | Otwórz commit w przeglądarce | |
+4 -5
View File
@@ -142,7 +142,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` <c-o> `` | Copy path to clipboard | |
| `` y `` | Copy to clipboard | |
| `` c `` | Verificar | Arquivo de check-out. Isso substitui o arquivo em sua árvore de trabalho com a versão do commit selecionado. |
| `` d `` | Descartar | Descartar as alterações desse commit para este arquivo. Isso executa uma rebase interativa em segundo plano, então você pode ter um conflito de merge se um commit posterior também alterar este arquivo. |
| `` d `` | Remover | Descartar as alterações desse commit para este arquivo. Isso executa uma rebase interativa em segundo plano, então você pode ter um conflito de merge se um commit posterior também alterar este arquivo. |
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
| `` e `` | Editar | Abrir arquivo no editor externo. |
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
@@ -159,7 +159,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
| `` b `` | View bisect options | |
| `` s `` | Squash | Squash o commit selecionado no commit abaixo dele. A mensagem do commit selecionado será anexada ao commit abaixo dele. |
@@ -303,7 +303,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <space> `` | Verificar | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
@@ -362,14 +362,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` u `` | Verificar atualização | |
| `` <enter> `` | Mudar para um repositório recente | |
| `` a `` | Mostrar/ciclo todos os logs de filiais | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## Sub-commits
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <space> `` | Verificar | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
+4 -5
View File
@@ -146,7 +146,7 @@ _Связки клавиш_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Скопировать hash коммита в буфер обмена | |
| `` <space> `` | Переключить | Checkout the selected commit as a detached HEAD. |
| `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Открыть коммит в браузере | |
@@ -166,7 +166,7 @@ _Связки клавиш_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Скопировать hash коммита в буфер обмена | |
| `` <c-r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
| `` b `` | Просмотреть параметры бинарного поиска | |
| `` s `` | Объединить коммиты (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
@@ -253,7 +253,7 @@ _Связки клавиш_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Скопировать hash коммита в буфер обмена | |
| `` <space> `` | Переключить | Checkout the selected commit as a detached HEAD. |
| `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Открыть коммит в браузере | |
@@ -297,7 +297,7 @@ _Связки клавиш_
| `` <c-o> `` | Скопировать название файла в буфер обмена | |
| `` y `` | Copy to clipboard | |
| `` c `` | Переключить | Переключить файл |
| `` d `` | Просмотреть параметры «отмены изменении» | Отменить изменения коммита в этом файле |
| `` d `` | Remove | Отменить изменения коммита в этом файле |
| `` o `` | Открыть файл | Open file in default application. |
| `` e `` | Edit | Open file in external editor. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
@@ -319,7 +319,6 @@ _Связки клавиш_
| `` u `` | Проверить обновления | |
| `` <enter> `` | Переключиться на последний репозиторий | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## Теги
+4 -5
View File
@@ -57,7 +57,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 复制提交哈希到剪贴板 | |
| `` <space> `` | 检出 | 检出所选择的提交作为分离HEAD。 |
| `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(如hash、URL、diff、消息、作者)。 |
| `` o `` | 在浏览器中打开提交 | |
@@ -101,7 +101,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 复制提交哈希到剪贴板 | |
| `` <space> `` | 检出 | 检出所选择的提交作为分离HEAD。 |
| `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(如hash、URL、diff、消息、作者)。 |
| `` o `` | 在浏览器中打开提交 | |
@@ -121,7 +121,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 复制提交哈希到剪贴板 | |
| `` <c-r> `` | 重置已拣选(复制)的提交 | |
| `` b `` | 查看二分查找选项 | |
| `` s `` | 压缩(Squash) | 将已选提交压缩到该提交之下。这些选定的提交的消息会附加到该提交的消息之下。 |
@@ -172,7 +172,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` <c-o> `` | 复制路径到剪贴板 | |
| `` y `` | 复制到剪贴板 | |
| `` c `` | 检出 | 检出文件 |
| `` d `` | 查看'放弃变更'选项 | 放弃对此文件的提交变更 |
| `` d `` | 删除 | 放弃对此文件的提交变更 |
| `` o `` | 打开文件 | 使用默认程序打开该文件 |
| `` e `` | 编辑(Edit) | 使用外部编辑器打开文件 |
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
@@ -345,7 +345,6 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` u `` | 检查更新 | |
| `` <enter> `` | 切换到最近的仓库 | |
| `` a `` | 显示/循环所有分支日志 | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | 聚焦主视图 | |
## 确认面板
+4 -5
View File
@@ -136,7 +136,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 複製提交 hash 到剪貼簿 | |
| `` <space> `` | 檢出 | Checkout the selected commit as a detached HEAD. |
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 在瀏覽器中開啟提交 | |
@@ -180,7 +180,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 複製提交 hash 到剪貼簿 | |
| `` <c-r> `` | 重設選定的揀選 (複製) 提交 | |
| `` b `` | 查看二分選項 | |
| `` s `` | 壓縮 (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
@@ -231,7 +231,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| `` <c-o> `` | 複製檔案名稱到剪貼簿 | |
| `` y `` | 複製到剪貼簿 | |
| `` c `` | 檢出 | 檢出檔案 |
| `` d `` | 捨棄 | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
| `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
| `` e `` | 編輯 | 使用外部編輯器開啟 |
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
@@ -262,7 +262,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 複製提交 hash 到剪貼簿 | |
| `` <space> `` | 檢出 | Checkout the selected commit as a detached HEAD. |
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 在瀏覽器中開啟提交 | |
@@ -374,7 +374,6 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| `` u `` | 檢查更新 | |
| `` <enter> `` | 切換到最近使用的版本庫 | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## 確認面板
+4 -1
View File
@@ -481,6 +481,10 @@ git:
# to 40 to disable truncation.
truncateCopiedCommitHashesTo: 12
# If true and if if `gh` is installed and on version >=2, we will use `gh` to
# display pull requests against branches.
enableGithubCli: true
# Periodic update checks
update:
# One of: 'prompt' (default) | 'background' | 'never'
@@ -666,7 +670,6 @@ keybinding:
checkForUpdate: u
recentRepos: <enter>
allBranchesLogGraph: a
allBranchesLogGraphReverse: A
files:
commitChanges: c
commitChangesWithoutHook: w
+6 -8
View File
@@ -60,7 +60,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` <c-o> `` | Copy path to clipboard | |
| `` y `` | Copy to clipboard | |
| `` c `` | Checkout | Checkout file. This replaces the file in your working tree with the version from the selected commit. |
| `` d `` | Discard | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
| `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
| `` o `` | Open file | Open file in default application. |
| `` e `` | Edit | Open file in external editor. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
@@ -71,7 +71,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
| `` / `` | Search the current view by text | |
## Commit summary
@@ -84,7 +84,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
| `` b `` | View bisect options | |
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
@@ -160,7 +160,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
| `` / `` | Search the current view by text | |
## Input prompt
@@ -237,7 +237,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` o `` | Open file | Open file in default application. |
| `` e `` | Edit file | Open file in external editor. |
| `` <space> `` | Toggle lines in patch | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Exit custom patch builder | |
| `` / `` | Search the current view by text | |
@@ -275,7 +274,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <space> `` | Checkout | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
@@ -353,14 +352,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` u `` | Check for update | |
| `` <enter> `` | Switch to a recent repo | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## Sub-commits
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <space> `` | Checkout | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
+6 -8
View File
@@ -64,7 +64,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | コミットハッシュをクリップボードにコピー | |
| `` <c-r> `` | コピーされた(チェリーピックされた)コミットの選択をリセット | |
| `` b `` | bisectオプションを表示 | |
| `` s `` | スカッシュ | 選択したコミットをその下のコミットにスカッシュします。スカッシュとは複数のコミットを1つにまとめる操作です。選択したコミットのメッセージが下のコミットに追加されます。 |
@@ -108,7 +108,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` <c-o> `` | パスをクリップボードにコピー | |
| `` y `` | クリップボードにコピー | |
| `` c `` | チェックアウト(ブランチの切り替え) | ファイルをチェックアウトします。これにより、作業ツリー内のファイルが選択したコミットのバージョンに置き換えられます。 |
| `` d `` | 破棄 | このコミットのこのファイルへの変更を破棄します。これはバックグラウンドで対話的なリベースを実行するため、後のコミットでもこのファイルが変更されている場合、マージコンフリクトが発生する可能性があります。 |
| `` d `` | 削除 | このコミットのこのファイルへの変更を破棄します。これはバックグラウンドで対話的なリベースを実行するため、後のコミットでもこのファイルが変更されている場合、マージコンフリクトが発生する可能性があります。 |
| `` o `` | ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
| `` e `` | 編集 | 外部エディタでファイルを開きます。 |
| `` <c-t> `` | 外部差分ツールを開く(git difftool) | |
@@ -119,7 +119,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます |
| `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します |
| `` 0 `` | メインビューにフォーカス | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
| `` / `` | 現在のビューをテキストで検索 | |
## コミット概要
@@ -132,7 +132,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | コミットハッシュをクリップボードにコピー | |
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したコミットをデタッチドヘッド(特定のブランチに属さない状態)としてチェックアウトします。 |
| `` y `` | コミット属性をクリップボードにコピー | コミット属性をクリップボードにコピーします(例:ハッシュ、URL、差分、メッセージ、作者)。 |
| `` o `` | ブラウザでコミットを開く | |
@@ -185,7 +185,6 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` u `` | 更新を確認 | |
| `` <enter> `` | 最近のリポジトリをチェックアウト | |
| `` a `` | ブランチログの表示モードを順に切り替え | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | メインビューにフォーカス | |
## セカンダリ
@@ -243,7 +242,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます |
| `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します |
| `` 0 `` | メインビューにフォーカス | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
| `` / `` | 現在のビューをテキストで検索 | |
## メインパネル(ステージング)
@@ -279,7 +278,6 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` o `` | ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
| `` <space> `` | パッチ内の行を切り替え | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | カスタムパッチビルダーを終了 | |
| `` / `` | 現在のビューをテキストで検索 | |
@@ -321,7 +319,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | コミットハッシュをクリップボードにコピー | |
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したコミットをデタッチドヘッド(特定のブランチに属さない状態)としてチェックアウトします。 |
| `` y `` | コミット属性をクリップボードにコピー | コミット属性をクリップボードにコピーします(例:ハッシュ、URL、差分、メッセージ、作者)。 |
| `` o `` | ブラウザでコミットを開く | |
+6 -8
View File
@@ -64,7 +64,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 커밋 해시를 클립보드에 복사 | |
| `` <space> `` | 체크아웃 | Checkout the selected commit as a detached HEAD. |
| `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 브라우저에서 커밋 열기 | |
@@ -106,7 +106,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 커밋 해시를 클립보드에 복사 | |
| `` <space> `` | 체크아웃 | Checkout the selected commit as a detached HEAD. |
| `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 브라우저에서 커밋 열기 | |
@@ -178,7 +178,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` o `` | 파일 닫기 | Open file in default application. |
| `` e `` | 파일 편집 | Open file in external editor. |
| `` <space> `` | Line(s)을 패치에 추가/삭제 | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Exit custom patch builder | |
| `` / `` | 검색 시작 | |
@@ -243,7 +242,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` u `` | 업데이트 확인 | |
| `` <enter> `` | 최근에 사용한 저장소로 전환 | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## 서브모듈
@@ -295,7 +293,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 커밋 해시를 클립보드에 복사 | |
| `` <c-r> `` | Reset cherry-picked (copied) commits selection | |
| `` b `` | Bisect 옵션 보기 | |
| `` s `` | 스쿼시 | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
@@ -339,7 +337,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` <c-o> `` | 파일명을 클립보드에 복사 | |
| `` y `` | 클립보드에 복사 | |
| `` c `` | 체크아웃 | Checkout file |
| `` d `` | View 'discard changes' options | Discard this commit's changes to this file |
| `` d `` | Remove | Discard this commit's changes to this file |
| `` o `` | 파일 닫기 | Open file in default application. |
| `` e `` | Edit | Open file in external editor. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
@@ -350,7 +348,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
| `` / `` | 검색 시작 | |
## 커밋메시지
@@ -406,7 +404,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
| `` / `` | 검색 시작 | |
## 확인 패널
+6 -8
View File
@@ -84,7 +84,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
| `` / `` | Start met zoeken | |
## Bevestigingspaneel
@@ -138,7 +138,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` <c-o> `` | Kopieer de bestandsnaam naar het klembord | |
| `` y `` | Copy to clipboard | |
| `` c `` | Uitchecken | Bestand uitchecken |
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | Uitsluit deze commit zijn veranderingen aan dit bestand |
| `` d `` | Remove | Uitsluit deze commit zijn veranderingen aan dit bestand |
| `` o `` | Open bestand | Open file in default application. |
| `` e `` | Edit | Open file in external editor. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
@@ -149,13 +149,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
| `` / `` | Start met zoeken | |
## Commits
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopieer commit hash naar klembord | |
| `` <c-r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
| `` b `` | View bisect options | |
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
@@ -245,7 +245,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` o `` | Open bestand | Open file in default application. |
| `` e `` | Verander bestand | Open file in external editor. |
| `` <space> `` | Voeg toe/verwijder lijn(en) in patch | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Sluit lijn-bij-lijn modus | |
| `` / `` | Start met zoeken | |
@@ -253,7 +252,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopieer commit hash naar klembord | |
| `` <space> `` | Uitchecken | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
@@ -353,14 +352,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` u `` | Check voor updates | |
| `` <enter> `` | Wissel naar een recente repo | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## Sub-commits
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopieer commit hash naar klembord | |
| `` <space> `` | Uitchecken | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
+6 -8
View File
@@ -57,7 +57,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopiuj hash commita do schowka | |
| `` <c-r> `` | Resetuj wybrane (cherry-picked) commity | |
| `` b `` | Zobacz opcje bisect | |
| `` s `` | Scal | Scal wybrany commit z commitami poniżej. Wiadomość wybranego commita zostanie dołączona do commita poniżej. |
@@ -124,7 +124,6 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. |
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
| `` <space> `` | Przełącz linie w łatce | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Wyjdź z budowniczego niestandardowej łatki | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
@@ -260,7 +259,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Filtruj bieżący widok po tekście | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Pliki commita
@@ -269,7 +268,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` <c-o> `` | Kopiuj ścieżkę do schowka | |
| `` y `` | Kopiuj do schowka | |
| `` c `` | Przełącz | Przełącz plik. Zastępuje plik w twoim drzewie roboczym wersją z wybranego commita. |
| `` d `` | Odrzuć | Odrzuć zmiany w tym pliku z tego commita. Uruchamia interaktywny rebase w tle, więc możesz otrzymać konflikt scalania, jeśli późniejszy commit również zmienia ten plik. |
| `` d `` | Usuń | Odrzuć zmiany w tym pliku z tego commita. Uruchamia interaktywny rebase w tle, więc możesz otrzymać konflikt scalania, jeśli późniejszy commit również zmienia ten plik. |
| `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. |
| `` e `` | Edytuj | Otwórz plik w zewnętrznym edytorze. |
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
@@ -280,7 +279,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Filtruj bieżący widok po tekście | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Podsumowanie commita
@@ -293,7 +292,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopiuj hash commita do schowka | |
| `` <space> `` | Przełącz | Przełącz wybrany commit jako odłączoną HEAD. |
| `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). |
| `` o `` | Otwórz commit w przeglądarce | |
@@ -332,14 +331,13 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` u `` | Sprawdź aktualizacje | |
| `` <enter> `` | Przełącz na ostatnie repozytorium | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## Sub-commity
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Kopiuj hash commita do schowka | |
| `` <space> `` | Przełącz | Przełącz wybrany commit jako odłączoną HEAD. |
| `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). |
| `` o `` | Otwórz commit w przeglądarce | |
+6 -8
View File
@@ -84,7 +84,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Recolher todos os arquivos | Recolher todos os diretórios na árvore de arquivos |
| `` = `` | Expandir todos os arquivos | Expandir todos os diretórios na árvore do arquivo |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
| `` / `` | Search the current view by text | |
## Branches locais
@@ -142,7 +142,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` <c-o> `` | Copy path to clipboard | |
| `` y `` | Copy to clipboard | |
| `` c `` | Verificar | Arquivo de check-out. Isso substitui o arquivo em sua árvore de trabalho com a versão do commit selecionado. |
| `` d `` | Descartar | Descartar as alterações desse commit para este arquivo. Isso executa uma rebase interativa em segundo plano, então você pode ter um conflito de merge se um commit posterior também alterar este arquivo. |
| `` d `` | Remover | Descartar as alterações desse commit para este arquivo. Isso executa uma rebase interativa em segundo plano, então você pode ter um conflito de merge se um commit posterior também alterar este arquivo. |
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
| `` e `` | Editar | Abrir arquivo no editor externo. |
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
@@ -153,13 +153,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Recolher todos os arquivos | Recolher todos os diretórios na árvore de arquivos |
| `` = `` | Expandir todos os arquivos | Expandir todos os diretórios na árvore do arquivo |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
| `` / `` | Search the current view by text | |
## Commits
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
| `` b `` | View bisect options | |
| `` s `` | Squash | Squash o commit selecionado no commit abaixo dele. A mensagem do commit selecionado será anexada ao commit abaixo dele. |
@@ -295,7 +295,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
| `` <space> `` | Alternar linhas no caminho | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Sair do construtor de patch personalizado | |
| `` / `` | Search the current view by text | |
@@ -303,7 +302,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <space> `` | Verificar | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
@@ -362,14 +361,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` u `` | Verificar atualização | |
| `` <enter> `` | Mudar para um repositório recente | |
| `` a `` | Mostrar/ciclo todos os logs de filiais | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## Sub-commits
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Copy commit hash to clipboard | |
| `` <space> `` | Verificar | Checkout the selected commit as a detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
+6 -8
View File
@@ -138,7 +138,6 @@ _Связки клавиш_
| `` o `` | Открыть файл | Open file in default application. |
| `` e `` | Редактировать файл | Open file in external editor. |
| `` <space> `` | Добавить/удалить строку(и) для патча | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Выйти из сборщика пользовательских патчей | |
| `` / `` | Найти | |
@@ -146,7 +145,7 @@ _Связки клавиш_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Скопировать hash коммита в буфер обмена | |
| `` <space> `` | Переключить | Checkout the selected commit as a detached HEAD. |
| `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Открыть коммит в браузере | |
@@ -166,7 +165,7 @@ _Связки клавиш_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Скопировать hash коммита в буфер обмена | |
| `` <c-r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
| `` b `` | Просмотреть параметры бинарного поиска | |
| `` s `` | Объединить коммиты (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
@@ -253,7 +252,7 @@ _Связки клавиш_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | Скопировать hash коммита в буфер обмена | |
| `` <space> `` | Переключить | Checkout the selected commit as a detached HEAD. |
| `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Открыть коммит в браузере | |
@@ -297,7 +296,7 @@ _Связки клавиш_
| `` <c-o> `` | Скопировать название файла в буфер обмена | |
| `` y `` | Copy to clipboard | |
| `` c `` | Переключить | Переключить файл |
| `` d `` | Просмотреть параметры «отмены изменении» | Отменить изменения коммита в этом файле |
| `` d `` | Remove | Отменить изменения коммита в этом файле |
| `` o `` | Открыть файл | Open file in default application. |
| `` e `` | Edit | Open file in external editor. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
@@ -308,7 +307,7 @@ _Связки клавиш_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
| `` / `` | Найти | |
## Статус
@@ -319,7 +318,6 @@ _Связки клавиш_
| `` u `` | Проверить обновления | |
| `` <enter> `` | Переключиться на последний репозиторий | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## Теги
@@ -400,7 +398,7 @@ _Связки клавиш_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
| `` / `` | Найти | |
## Хранилище
+6 -8
View File
@@ -57,7 +57,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 复制提交哈希到剪贴板 | |
| `` <space> `` | 检出 | 检出所选择的提交作为分离HEAD。 |
| `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(如hash、URL、diff、消息、作者)。 |
| `` o `` | 在浏览器中打开提交 | |
@@ -101,7 +101,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 复制提交哈希到剪贴板 | |
| `` <space> `` | 检出 | 检出所选择的提交作为分离HEAD。 |
| `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(如hash、URL、diff、消息、作者)。 |
| `` o `` | 在浏览器中打开提交 | |
@@ -121,7 +121,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 复制提交哈希到剪贴板 | |
| `` <c-r> `` | 重置已拣选(复制)的提交 | |
| `` b `` | 查看二分查找选项 | |
| `` s `` | 压缩(Squash) | 将已选提交压缩到该提交之下。这些选定的提交的消息会附加到该提交的消息之下。 |
@@ -172,7 +172,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` <c-o> `` | 复制路径到剪贴板 | |
| `` y `` | 复制到剪贴板 | |
| `` c `` | 检出 | 检出文件 |
| `` d `` | 查看'放弃变更'选项 | 放弃对此文件的提交变更 |
| `` d `` | 删除 | 放弃对此文件的提交变更 |
| `` o `` | 打开文件 | 使用默认程序打开该文件 |
| `` e `` | 编辑(Edit) | 使用外部编辑器打开文件 |
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
@@ -183,7 +183,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
| `` 0 `` | 聚焦主视图 | |
| `` / `` | 通过文本过滤当前视图 | |
| `` / `` | 开始搜索 | |
## 文件
@@ -216,7 +216,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
| `` 0 `` | 聚焦主视图 | |
| `` / `` | 通过文本过滤当前视图 | |
| `` / `` | 开始搜索 | |
## 本地分支
@@ -260,7 +260,6 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` o `` | 打开文件 | 使用默认程序打开该文件 |
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
| `` <space> `` | 添加/移除 行到补丁 | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | 退出逐行模式 | |
| `` / `` | 开始搜索 | |
@@ -345,7 +344,6 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` u `` | 检查更新 | |
| `` <enter> `` | 切换到最近的仓库 | |
| `` a `` | 显示/循环所有分支日志 | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | 聚焦主视图 | |
## 确认面板
+4 -6
View File
@@ -72,7 +72,6 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
| `` <space> `` | 向 (或從) 補丁中添加/刪除行 | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | 退出自訂補丁建立器 | |
| `` / `` | 搜尋 | |
@@ -136,7 +135,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 複製提交 hash 到剪貼簿 | |
| `` <space> `` | 檢出 | Checkout the selected commit as a detached HEAD. |
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 在瀏覽器中開啟提交 | |
@@ -180,7 +179,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 複製提交 hash 到剪貼簿 | |
| `` <c-r> `` | 重設選定的揀選 (複製) 提交 | |
| `` b `` | 查看二分選項 | |
| `` s `` | 壓縮 (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
@@ -231,7 +230,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| `` <c-o> `` | 複製檔案名稱到剪貼簿 | |
| `` y `` | 複製到剪貼簿 | |
| `` c `` | 檢出 | 檢出檔案 |
| `` d `` | 捨棄 | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
| `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
| `` e `` | 編輯 | 使用外部編輯器開啟 |
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
@@ -262,7 +261,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy abbreviated commit hash to clipboard | |
| `` <c-o> `` | 複製提交 hash 到剪貼簿 | |
| `` <space> `` | 檢出 | Checkout the selected commit as a detached HEAD. |
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 在瀏覽器中開啟提交 | |
@@ -374,7 +373,6 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| `` u `` | 檢查更新 | |
| `` <enter> `` | 切換到最近使用的版本庫 | |
| `` a `` | Show/cycle all branch logs | |
| `` A `` | Show/cycle all branch logs (reverse) | |
| `` 0 `` | Focus main view | |
## 確認面板
+9 -7
View File
@@ -10,6 +10,7 @@ require (
github.com/adrg/xdg v0.4.0
github.com/atotto/clipboard v0.1.4
github.com/aybabtme/humanlog v0.4.1
github.com/cli/go-gh/v2 v2.9.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/gdamore/tcell/v2 v2.13.8
@@ -18,7 +19,7 @@ require (
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
github.com/jesseduffield/gocui v0.3.1-0.20260308162933-5e45e57b5564
github.com/jesseduffield/gocui v0.3.1-0.20260128194906-9d8c3cdfac18
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3
@@ -39,7 +40,7 @@ require (
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/sync v0.19.0
golang.org/x/sys v0.42.0
golang.org/x/sys v0.40.0
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -49,7 +50,8 @@ require (
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cli/safeexec v1.0.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
@@ -66,8 +68,8 @@ require (
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
@@ -78,8 +80,8 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
+21 -16
View File
@@ -64,9 +64,13 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cli/go-gh/v2 v2.9.0 h1:D3lTjEneMYl54M+WjZ+kRPrR5CEJ5BHS05isBPOV3LI=
github.com/cli/go-gh/v2 v2.9.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE=
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
@@ -189,8 +193,8 @@ github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2Paiis
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c/go.mod h1:F2fEBk0ddf6ixrBrJjY7phfQ3hL9rXG0uSjvwYe50bE=
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd h1:ViKj6qth8FgcIWizn9KiACWwPemWSymx62OPN0tHT+Q=
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd/go.mod h1:lRhCiBr6XjQrvcQVa+UYsy/99d3wMXn/a0nSQlhnhlA=
github.com/jesseduffield/gocui v0.3.1-0.20260308162933-5e45e57b5564 h1:aB/Ytu+OCEpjft/BehqbH8/PTdgLREqbCvjK1JXctoo=
github.com/jesseduffield/gocui v0.3.1-0.20260308162933-5e45e57b5564/go.mod h1:lQCd2TvvNXVKFBowy4A7xxZbUp+1KEiGs4j0Q5Zt9gQ=
github.com/jesseduffield/gocui v0.3.1-0.20260128194906-9d8c3cdfac18 h1:+Q17GqvNaGzuvIR1JCdIS0khVjMzdwrhXBBDxnsdN8Y=
github.com/jesseduffield/gocui v0.3.1-0.20260128194906-9d8c3cdfac18/go.mod h1:lQCd2TvvNXVKFBowy4A7xxZbUp+1KEiGs4j0Q5Zt9gQ=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
@@ -223,13 +227,14 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
@@ -451,20 +456,20 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -475,8 +480,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+47 -41
View File
@@ -18,27 +18,29 @@ import (
// GitCommand is our main git interface
type GitCommand struct {
Blame *git_commands.BlameCommands
Branch *git_commands.BranchCommands
Commit *git_commands.CommitCommands
Config *git_commands.ConfigCommands
Custom *git_commands.CustomCommands
Diff *git_commands.DiffCommands
File *git_commands.FileCommands
Flow *git_commands.FlowCommands
Patch *git_commands.PatchCommands
Rebase *git_commands.RebaseCommands
Remote *git_commands.RemoteCommands
Stash *git_commands.StashCommands
Status *git_commands.StatusCommands
Submodule *git_commands.SubmoduleCommands
Sync *git_commands.SyncCommands
Tag *git_commands.TagCommands
WorkingTree *git_commands.WorkingTreeCommands
Bisect *git_commands.BisectCommands
Worktree *git_commands.WorktreeCommands
Version *git_commands.GitVersion
RepoPaths *git_commands.RepoPaths
Blame *git_commands.BlameCommands
Branch *git_commands.BranchCommands
Commit *git_commands.CommitCommands
Config *git_commands.ConfigCommands
Custom *git_commands.CustomCommands
Diff *git_commands.DiffCommands
File *git_commands.FileCommands
Flow *git_commands.FlowCommands
Patch *git_commands.PatchCommands
Rebase *git_commands.RebaseCommands
Remote *git_commands.RemoteCommands
Stash *git_commands.StashCommands
Status *git_commands.StatusCommands
Submodule *git_commands.SubmoduleCommands
Sync *git_commands.SyncCommands
Tag *git_commands.TagCommands
WorkingTree *git_commands.WorkingTreeCommands
Bisect *git_commands.BisectCommands
Worktree *git_commands.WorktreeCommands
Version *git_commands.GitVersion
RepoPaths *git_commands.RepoPaths
GitHub *git_commands.GitHubCommands
HostingService *git_commands.HostingService
Loaders Loaders
}
@@ -137,6 +139,8 @@ func NewGitCommandAux(
bisectCommands := git_commands.NewBisectCommands(gitCommon)
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
blameCommands := git_commands.NewBlameCommands(gitCommon)
gitHubCommands := git_commands.NewGitHubCommand(gitCommon)
hostingServiceCommands := git_commands.NewHostingServiceCommand(gitCommon)
branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands)
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
@@ -148,26 +152,28 @@ func NewGitCommandAux(
tagLoader := git_commands.NewTagLoader(cmn, cmd)
return &GitCommand{
Blame: blameCommands,
Branch: branchCommands,
Commit: commitCommands,
Config: configCommands,
Custom: customCommands,
Diff: diffCommands,
File: fileCommands,
Flow: flowCommands,
Patch: patchCommands,
Rebase: rebaseCommands,
Remote: remoteCommands,
Stash: stashCommands,
Status: statusCommands,
Submodule: submoduleCommands,
Sync: syncCommands,
Tag: tagCommands,
Bisect: bisectCommands,
WorkingTree: workingTreeCommands,
Worktree: worktreeCommands,
Version: version,
Blame: blameCommands,
Branch: branchCommands,
Commit: commitCommands,
Config: configCommands,
Custom: customCommands,
Diff: diffCommands,
File: fileCommands,
Flow: flowCommands,
Patch: patchCommands,
Rebase: rebaseCommands,
Remote: remoteCommands,
Stash: stashCommands,
Status: statusCommands,
Submodule: submoduleCommands,
Sync: syncCommands,
Tag: tagCommands,
Bisect: bisectCommands,
WorkingTree: workingTreeCommands,
Worktree: worktreeCommands,
Version: version,
GitHub: gitHubCommands,
HostingService: hostingServiceCommands,
Loaders: Loaders{
BranchLoader: branchLoader,
CommitFileLoader: commitFileLoader,
-6
View File
@@ -317,12 +317,6 @@ func (self *BranchCommands) RotateAllBranchesLogIdx() {
self.allBranchesLogCmdIndex = (i + 1) % n
}
func (self *BranchCommands) RotateAllBranchesLogIdxBackward() {
n := len(self.allBranchesLogCandidates())
i := self.allBranchesLogCmdIndex
self.allBranchesLogCmdIndex = (i - 1 + n) % n
}
func (self *BranchCommands) GetAllBranchesLogIdxAndCount() (int, int) {
n := len(self.allBranchesLogCandidates())
i := self.allBranchesLogCmdIndex
+426
View File
@@ -0,0 +1,426 @@
package git_commands
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/cli/go-gh/v2/pkg/auth"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
)
type GitHubCommands struct {
*GitCommon
}
func NewGitHubCommand(gitCommon *GitCommon) *GitHubCommands {
return &GitHubCommands{
GitCommon: gitCommon,
}
}
// https://github.com/cli/cli/issues/2300
func (self *GitHubCommands) BaseRepo() error {
cmdArgs := NewGitCmd("config").
Arg("--local", "--get-regexp", ".gh-resolved").
ToArgv()
return self.cmd.New(cmdArgs).DontLog().Run()
}
// Ex: git config --local --add "remote.origin.gh-resolved" "jesseduffield/lazygit"
func (self *GitHubCommands) SetBaseRepo(repository string) (string, error) {
cmdArgs := NewGitCmd("config").
Arg("--local", "--add", "remote.origin.gh-resolved", repository).
ToArgv()
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
}
type Response struct {
Data RepositoryQuery `json:"data"`
}
type RepositoryQuery struct {
Repository map[string]PullRequest `json:"repository"`
}
type PullRequest struct {
Edges []PullRequestEdge `json:"edges"`
}
type PullRequestEdge struct {
Node PullRequestNode `json:"node"`
}
type PullRequestNode struct {
Title string `json:"title"`
HeadRefName string `json:"headRefName"`
Number int `json:"number"`
Url string `json:"url"`
HeadRepositoryOwner GithubRepositoryOwner `json:"headRepositoryOwner"`
State string `json:"state"`
IsDraft bool `json:"isDraft"`
}
type GithubRepositoryOwner struct {
Login string `json:"login"`
}
func fetchPullRequestsQuery(branches []string, owner string, repo string) string {
queries := make([]string, 0, len(branches))
for i, branch := range branches {
// We're making a sub-query per branch, and arbitrarily labelling each subquery
// as a1, a2, etc.
fieldName := fmt.Sprintf("a%d", i+1)
// We fetch a few PRs per branch name because multiple forks may have PRs
// with the same head ref name. The mapping logic filters by owner later.
queries = append(queries, fmt.Sprintf(`%s: pullRequests(first: 5, headRefName: "%s", orderBy: {field: CREATED_AT, direction: DESC}) {
edges {
node {
title
headRefName
state
number
url
isDraft
headRepositoryOwner {
login
}
}
}
}`, fieldName, branch))
}
queryString := fmt.Sprintf(`{
repository(owner: "%s", name: "%s") {
%s
}
}`, owner, repo, strings.Join(queries, "\n"))
return queryString
}
// FetchRecentPRs fetches recent pull requests using GraphQL.
func (self *GitHubCommands) FetchRecentPRs(branches []string) ([]*models.GithubPullRequest, error) {
repoOwner, repoName, err := self.GetBaseRepoOwnerAndName()
if err != nil {
return nil, err
}
t := time.Now()
var g errgroup.Group
results := make(chan []*models.GithubPullRequest)
// We want at most 5 concurrent requests, but no less than 10 branches per request
concurrency := 5
minBranchesPerRequest := 10
branchesPerRequest := max(len(branches)/concurrency, minBranchesPerRequest)
for i := 0; i < len(branches); i += branchesPerRequest {
end := i + branchesPerRequest
if end > len(branches) {
end = len(branches)
}
branchChunk := branches[i:end]
// Launch a goroutine for each chunk of branches
g.Go(func() error {
prs, err := self.FetchRecentPRsAux(repoOwner, repoName, branchChunk)
if err != nil {
return err
}
results <- prs
return nil
})
}
// Close the results channel when all goroutines are done
go func() {
_ = g.Wait()
close(results)
}()
// Collect results from all goroutines
var allPRs []*models.GithubPullRequest
for prs := range results {
allPRs = append(allPRs, prs...)
}
if err := g.Wait(); err != nil {
return nil, err
}
self.Log.Warnf("Fetched PRs in %s", time.Since(t))
return allPRs, nil
}
func (self *GitHubCommands) FetchRecentPRsAux(repoOwner string, repoName string, branches []string) ([]*models.GithubPullRequest, error) {
queryString := fetchPullRequestsQuery(branches, repoOwner, repoName)
escapedQueryString := strconv.Quote(queryString)
body := fmt.Sprintf(`{"query": %s}`, escapedQueryString)
req, err := http.NewRequest("POST", "https://api.github.com/graphql", bytes.NewBuffer([]byte(body)))
if err != nil {
return nil, err
}
defaultHost, _ := auth.DefaultHost()
token, _ := auth.TokenForHost(defaultHost)
if token == "" {
return nil, fmt.Errorf("No token found for GitHub")
}
req.Header.Set("Authorization", "token "+token)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyStr := new(bytes.Buffer)
_, _ = bodyStr.ReadFrom(resp.Body)
return nil, fmt.Errorf("GraphQL query failed with status: %s. Body: %s", resp.Status, bodyStr.String())
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result Response
err = json.Unmarshal(bodyBytes, &result)
if err != nil {
return nil, err
}
prs := []*models.GithubPullRequest{}
for _, repoQuery := range result.Data.Repository {
for _, edge := range repoQuery.Edges {
node := edge.Node
pr := &models.GithubPullRequest{
HeadRefName: node.HeadRefName,
Number: node.Number,
Title: node.Title,
State: lo.Ternary(node.IsDraft && node.State != "CLOSED", "DRAFT", node.State),
Url: node.Url,
HeadRepositoryOwner: models.GithubRepositoryOwner{
Login: node.HeadRepositoryOwner.Login,
},
}
prs = append(prs, pr)
}
}
return prs, nil
}
// returns a map from branch name to pull request
func GenerateGithubPullRequestMap(
prs []*models.GithubPullRequest,
branches []*models.Branch,
remotes []*models.Remote,
) map[string]*models.GithubPullRequest {
res := map[string]*models.GithubPullRequest{}
if len(prs) == 0 {
return res
}
remotesToOwnersMap := getRemotesToOwnersMap(remotes)
// A PR can be identified by two things: the owner e.g. 'jesseduffield' and the
// branch name e.g. 'feature/my-feature'. The owner might be different
// to the owner of the repo if the PR is from a fork of that repo.
type prKey struct {
owner string
branchName string
}
prByKey := map[prKey]models.GithubPullRequest{}
for _, pr := range prs {
key := prKey{owner: pr.UserName(), branchName: pr.BranchName()}
// PRs are returned newest-first from the API, so the first one we
// see for each key is the most recent and therefore the most relevant.
if _, exists := prByKey[key]; !exists {
prByKey[key] = *pr
}
}
for _, branch := range branches {
if !branch.IsTrackingRemote() {
continue
}
owner, foundRemoteOwner := remotesToOwnersMap[branch.UpstreamRemote]
if !foundRemoteOwner {
// UpstreamRemote may be a full URL rather than a remote name;
// try parsing the owner directly from it.
repoInfo, err := hosting_service.GetRepoInfoFromURL(branch.UpstreamRemote)
if err != nil {
continue
}
owner = repoInfo.Owner
}
pr, hasPr := prByKey[prKey{owner: owner, branchName: branch.UpstreamBranch}]
if !hasPr {
continue
}
res[branch.Name] = &pr
}
return res
}
func getRemotesToOwnersMap(remotes []*models.Remote) map[string]string {
res := map[string]string{}
for _, remote := range remotes {
if len(remote.Urls) == 0 {
continue
}
repoInfo, err := hosting_service.GetRepoInfoFromURL(remote.Urls[0])
if err != nil {
continue
}
res[remote.Name] = repoInfo.Owner
}
return res
}
// return <installed>, <valid version>
func (self *GitHubCommands) DetermineGitHubCliState() (bool, bool) {
output, err := self.cmd.New([]string{"gh", "--version"}).DontLog().RunWithOutput()
if err != nil {
// assuming a failure here means that it's not installed
return false, false
}
if !isGhVersionValid(output) {
return true, false
}
return true, true
}
func isGhVersionValid(versionStr string) bool {
// output should be something like:
// gh version 2.0.0 (2021-08-23)
// https://github.com/cli/cli/releases/tag/v2.0.0
re := regexp.MustCompile(`[^\d]+([\d\.]+)`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) == 0 {
return false
}
ghVersion := matches[1]
majorVersion, err := strconv.Atoi(ghVersion[0:1])
if err != nil {
return false
}
if majorVersion < 2 {
return false
}
return true
}
func (self *GitHubCommands) InGithubRepo() bool {
remotes, err := self.repo.Remotes()
if err != nil {
self.Log.Error(err)
return false
}
if len(remotes) == 0 {
return false
}
remote := GetMainRemote(remotes)
if len(remote.Config().URLs) == 0 {
return false
}
url := remote.Config().URLs[0]
return strings.Contains(url, "github.com")
}
func GetMainRemote(remotes []*gogit.Remote) *gogit.Remote {
for _, remote := range remotes {
if remote.Config().Name == "origin" {
return remote
}
}
// need to sort remotes by name so that this is deterministic
return lo.MinBy(remotes, func(a, b *gogit.Remote) bool {
return a.Config().Name < b.Config().Name
})
}
func GetSuggestedRemoteName(remotes []*models.Remote) string {
if len(remotes) == 0 {
return "origin"
}
for _, remote := range remotes {
if remote.Name == "origin" {
return remote.Name
}
}
return remotes[0].Name
}
func (self *GitHubCommands) GetBaseRepoOwnerAndName() (string, string, error) {
remotes, err := self.repo.Remotes()
if err != nil {
return "", "", err
}
if len(remotes) == 0 {
return "", "", fmt.Errorf("No remotes found")
}
originRemote, ok := lo.Find(remotes, func(remote *gogit.Remote) bool {
return remote.Config().Name == "origin"
})
if !ok {
return "", "", fmt.Errorf("Origin remote not found")
}
if len(originRemote.Config().URLs) == 0 {
return "", "", fmt.Errorf("No URLs found for remote")
}
url := originRemote.Config().URLs[0]
repoInfo, err := hosting_service.GetRepoInfoFromURL(url)
if err != nil {
return "", "", err
}
return repoInfo.Owner, repoInfo.Repository, nil
}
+392
View File
@@ -0,0 +1,392 @@
package git_commands
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
func TestIsValidGhVersion(t *testing.T) {
type scenario struct {
versionStr string
expectedResult bool
}
scenarios := []scenario{
{
"",
false,
},
{
`gh version 1.0.0 (2020-08-23)
https://github.com/cli/cli/releases/tag/v1.0.0`,
false,
},
{
`gh version 2.0.0 (2021-08-23)
https://github.com/cli/cli/releases/tag/v2.0.0`,
true,
},
{
`gh version 1.1.0 (2021-10-14)
https://github.com/cli/cli/releases/tag/v1.1.0
A new release of gh is available: 1.1.0 → v2.2.0
To upgrade, run: brew update && brew upgrade gh
https://github.com/cli/cli/releases/tag/v2.2.0`,
false,
},
}
for _, s := range scenarios {
t.Run(s.versionStr, func(t *testing.T) {
result := isGhVersionValid(s.versionStr)
assert.Equal(t, result, s.expectedResult)
})
}
}
func TestGetSuggestedRemoteName(t *testing.T) {
cases := []struct {
remotes []*models.Remote
expected string
}{
{mkRemoteList(), "origin"},
{mkRemoteList("upstream", "origin", "foo"), "origin"},
{mkRemoteList("upstream", "foo", "bar"), "upstream"},
}
for _, c := range cases {
result := GetSuggestedRemoteName(c.remotes)
assert.EqualValues(t, c.expected, result)
}
}
func TestGetRepoInfoFromURL(t *testing.T) {
cases := []struct {
name string
url string
expected hosting_service.RepoInformation
}{
{
name: "SSH URL",
url: "git@github.com:jesseduffield/lazygit.git",
expected: hosting_service.RepoInformation{
Owner: "jesseduffield",
Repository: "lazygit",
},
},
{
name: "HTTPS URL",
url: "https://github.com/jesseduffield/lazygit.git",
expected: hosting_service.RepoInformation{
Owner: "jesseduffield",
Repository: "lazygit",
},
},
{
name: "HTTPS URL without .git",
url: "https://github.com/jesseduffield/lazygit",
expected: hosting_service.RepoInformation{
Owner: "jesseduffield",
Repository: "lazygit",
},
},
{
name: "SSH URL with org nesting",
url: "git@github.com:my-org/sub-group/lazygit.git",
expected: hosting_service.RepoInformation{
Owner: "my-org/sub-group",
Repository: "lazygit",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
result, err := hosting_service.GetRepoInfoFromURL(c.url)
assert.NoError(t, err)
assert.Equal(t, c.expected, result)
})
}
}
func TestGenerateGithubPullRequestMap(t *testing.T) {
cases := []struct {
name string
prs []*models.GithubPullRequest
branches []*models.Branch
remotes []*models.Remote
expected map[string]*models.GithubPullRequest
}{
{
name: "empty inputs",
prs: []*models.GithubPullRequest{},
branches: []*models.Branch{},
remotes: []*models.Remote{},
expected: map[string]*models.GithubPullRequest{},
},
{
name: "matches PR to branch tracking origin",
prs: []*models.GithubPullRequest{
{
HeadRefName: "feature-branch",
Number: 42,
Title: "Add feature",
State: "OPEN",
Url: "https://github.com/jesseduffield/lazygit/pull/42",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
},
},
branches: []*models.Branch{
{
Name: "feature-branch",
UpstreamRemote: "origin",
UpstreamBranch: "feature-branch",
},
},
remotes: []*models.Remote{
{
Name: "origin",
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
},
},
expected: map[string]*models.GithubPullRequest{
"feature-branch": {
HeadRefName: "feature-branch",
Number: 42,
Title: "Add feature",
State: "OPEN",
Url: "https://github.com/jesseduffield/lazygit/pull/42",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
},
},
},
{
name: "does not match branch without upstream",
prs: []*models.GithubPullRequest{
{
HeadRefName: "feature-branch",
Number: 42,
Title: "Add feature",
State: "OPEN",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
},
},
branches: []*models.Branch{
{
Name: "feature-branch",
// no upstream set
},
},
remotes: []*models.Remote{
{
Name: "origin",
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
},
},
expected: map[string]*models.GithubPullRequest{},
},
{
name: "matches fork PR to branch tracking fork remote",
prs: []*models.GithubPullRequest{
{
HeadRefName: "fix-bug",
Number: 99,
Title: "Fix bug",
State: "OPEN",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "contributor"},
},
},
branches: []*models.Branch{
{
Name: "fix-bug",
UpstreamRemote: "contributor",
UpstreamBranch: "fix-bug",
},
},
remotes: []*models.Remote{
{
Name: "origin",
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
},
{
Name: "contributor",
Urls: []string{"git@github.com:contributor/lazygit.git"},
},
},
expected: map[string]*models.GithubPullRequest{
"fix-bug": {
HeadRefName: "fix-bug",
Number: 99,
Title: "Fix bug",
State: "OPEN",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "contributor"},
},
},
},
{
name: "does not match when owner differs",
prs: []*models.GithubPullRequest{
{
HeadRefName: "feature-branch",
Number: 42,
Title: "Add feature",
State: "OPEN",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "someone-else"},
},
},
branches: []*models.Branch{
{
Name: "feature-branch",
UpstreamRemote: "origin",
UpstreamBranch: "feature-branch",
},
},
remotes: []*models.Remote{
{
Name: "origin",
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
},
},
expected: map[string]*models.GithubPullRequest{},
},
{
name: "matches when UpstreamRemote is a full URL",
prs: []*models.GithubPullRequest{
{
HeadRefName: "my-branch",
Number: 55,
Title: "Full URL upstream",
State: "OPEN",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "contributor"},
},
},
branches: []*models.Branch{
{
Name: "my-branch",
UpstreamRemote: "git@github.com:contributor/lazygit.git",
UpstreamBranch: "my-branch",
},
},
remotes: []*models.Remote{
{
Name: "origin",
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
},
},
expected: map[string]*models.GithubPullRequest{
"my-branch": {
HeadRefName: "my-branch",
Number: 55,
Title: "Full URL upstream",
State: "OPEN",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "contributor"},
},
},
},
{
name: "uses first PR when branch name is reused (API returns newest first)",
prs: []*models.GithubPullRequest{
// API returns newest first (CREATED_AT DESC)
{
HeadRefName: "update-sponsors",
Number: 50,
Title: "Newest PR",
State: "CLOSED",
Url: "https://github.com/jesseduffield/lazygit/pull/50",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
},
{
HeadRefName: "update-sponsors",
Number: 30,
Title: "Middle PR",
State: "OPEN",
Url: "https://github.com/jesseduffield/lazygit/pull/30",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
},
{
HeadRefName: "update-sponsors",
Number: 10,
Title: "Oldest PR",
State: "CLOSED",
Url: "https://github.com/jesseduffield/lazygit/pull/10",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
},
},
branches: []*models.Branch{
{
Name: "update-sponsors",
UpstreamRemote: "origin",
UpstreamBranch: "update-sponsors",
},
},
remotes: []*models.Remote{
{
Name: "origin",
Urls: []string{"git@github.com:jesseduffield/lazygit.git"},
},
},
expected: map[string]*models.GithubPullRequest{
"update-sponsors": {
HeadRefName: "update-sponsors",
Number: 50,
Title: "Newest PR",
State: "CLOSED",
Url: "https://github.com/jesseduffield/lazygit/pull/50",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
},
},
},
{
name: "matches with HTTPS remote URL",
prs: []*models.GithubPullRequest{
{
HeadRefName: "my-pr",
Number: 10,
Title: "My PR",
State: "MERGED",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
},
},
branches: []*models.Branch{
{
Name: "my-pr",
UpstreamRemote: "origin",
UpstreamBranch: "my-pr",
},
},
remotes: []*models.Remote{
{
Name: "origin",
Urls: []string{"https://github.com/jesseduffield/lazygit.git"},
},
},
expected: map[string]*models.GithubPullRequest{
"my-pr": {
HeadRefName: "my-pr",
Number: 10,
Title: "My PR",
State: "MERGED",
HeadRepositoryOwner: models.GithubRepositoryOwner{Login: "jesseduffield"},
},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
result := GenerateGithubPullRequestMap(c.prs, c.branches, c.remotes)
assert.Equal(t, c.expected, result)
})
}
}
func mkRemoteList(names ...string) []*models.Remote {
return lo.Map(names, func(name string, _ int) *models.Remote {
return &models.Remote{Name: name}
})
}
@@ -0,0 +1,34 @@
package git_commands
import "github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
// a hosting service is something like github, gitlab, bitbucket etc
type HostingService struct {
*GitCommon
}
func NewHostingServiceCommand(gitCommon *GitCommon) *HostingService {
return &HostingService{
GitCommon: gitCommon,
}
}
func (self *HostingService) GetPullRequestURL(from string, to string) (string, error) {
return self.getHostingServiceMgr(self.config.GetRemoteURL()).GetPullRequestURL(from, to)
}
func (self *HostingService) GetCommitURL(commitSha string) (string, error) {
return self.getHostingServiceMgr(self.config.GetRemoteURL()).GetCommitURL(commitSha)
}
func (self *HostingService) GetRepoNameFromRemoteURL(remoteURL string) (string, error) {
return self.getHostingServiceMgr(remoteURL).GetRepoName()
}
// getting this on every request rather than storing it in state in case our remoteURL changes
// from one invocation to the next. Note however that we're currently caching config
// results so we might want to invalidate the cache here if it becomes a problem.
func (self *HostingService) getHostingServiceMgr(remoteURL string) *hosting_service.HostingServiceMgr {
configServices := self.UserConfig().Services
return hosting_service.NewHostingServiceMgr(self.Log, self.Tr, remoteURL, configServices)
}
+1 -5
View File
@@ -246,11 +246,7 @@ func (self *WorkingTreeCommands) Ignore(filename string) error {
// Exclude adds a file to the .git/info/exclude for the repo
func (self *WorkingTreeCommands) Exclude(filename string) error {
infoDir := filepath.Join(self.repoPaths.repoGitDirPath, "info")
if err := os.MkdirAll(infoDir, 0o755); err != nil {
return err
}
excludeFile := filepath.Join(infoDir, "exclude")
excludeFile := filepath.Join(self.repoPaths.repoGitDirPath, "info", "exclude")
return self.os.AppendLineToFile(excludeFile, escapeFilename(filename))
}
@@ -67,8 +67,6 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
// we can parallelize the calls to git rev-parse
GitDir: "",
}
} else if strings.HasPrefix(splitLine, "HEAD ") {
current.Head = strings.SplitN(splitLine, " ", 2)[1]
} else if strings.HasPrefix(splitLine, "branch ") {
branch := strings.SplitN(splitLine, " ", 2)[1]
current.Branch = strings.TrimPrefix(branch, "refs/heads/")
@@ -46,7 +46,6 @@ branch refs/heads/mybranch
IsPathMissing: false,
GitDir: "/path/to/repo/.git",
Branch: "mybranch",
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
Name: "repo",
},
},
@@ -87,7 +86,6 @@ branch refs/heads/mybranch-worktree
IsPathMissing: false,
GitDir: "/path/to/repo/.git",
Branch: "mybranch",
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
Name: "repo",
},
{
@@ -97,7 +95,6 @@ branch refs/heads/mybranch-worktree
IsPathMissing: false,
GitDir: "/path/to/repo/.git/worktrees/repo-worktree",
Branch: "mybranch-worktree",
Head: "775955775e79b8f5b4c4b56f82fbf657e2d5e4de",
Name: "repo-worktree",
},
},
@@ -127,7 +124,6 @@ branch refs/heads/missingbranch
IsPathMissing: true,
GitDir: "",
Branch: "missingbranch",
Head: "775955775e79b8f5b4c4b56f82fbf657e2d5e4de",
Name: "worktree",
},
},
@@ -168,7 +164,6 @@ branch refs/heads/mybranch-worktree
IsPathMissing: false,
GitDir: "/path/to/repo/.git/worktrees/repo-worktree",
Branch: "mybranch-worktree",
Head: "775955775e79b8f5b4c4b56f82fbf657e2d5e4de",
Name: "repo-worktree",
},
{
@@ -178,62 +173,11 @@ branch refs/heads/mybranch-worktree
IsPathMissing: false,
GitDir: "/path/to/repo/.git",
Branch: "mybranch",
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
Name: "repo",
},
},
expectedErr: "",
},
{
testName: "Detached HEAD worktree",
repoPaths: &RepoPaths{
repoPath: "/path/to/repo",
worktreePath: "/path/to/repo",
},
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
`worktree /path/to/repo
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
branch refs/heads/mybranch
worktree /path/to/repo-worktree
HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de
detached
`,
nil)
gitArgsMainWorktree := append(append([]string{"-C", "/path/to/repo"}, getRevParseArgs()...), "--absolute-git-dir")
runner.ExpectGitArgs(gitArgsMainWorktree, "/path/to/repo/.git", nil)
gitArgsLinkedWorktree := append(append([]string{"-C", "/path/to/repo-worktree"}, getRevParseArgs()...), "--absolute-git-dir")
runner.ExpectGitArgs(gitArgsLinkedWorktree, "/path/to/repo/.git/worktrees/repo-worktree", nil)
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
_ = fs.MkdirAll("/path/to/repo-worktree", 0o755)
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/repo-worktree", 0o755)
},
expectedWorktrees: []*models.Worktree{
{
IsMain: true,
IsCurrent: true,
Path: "/path/to/repo",
IsPathMissing: false,
GitDir: "/path/to/repo/.git",
Branch: "mybranch",
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
Name: "repo",
},
{
IsMain: false,
IsCurrent: false,
Path: "/path/to/repo-worktree",
IsPathMissing: false,
GitDir: "/path/to/repo/.git/worktrees/repo-worktree",
Branch: "",
Head: "775955775e79b8f5b4c4b56f82fbf657e2d5e4de",
Name: "repo-worktree",
},
},
expectedErr: "",
},
}
for _, s := range scenarios {
+13 -2
View File
@@ -6,7 +6,11 @@ var defaultUrlRegexStrings = []string{
`^(?:https?|ssh)://[^/]+/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^(.*?@)?.*:/*(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
}
var defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
var (
defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
defaultRepoNameTemplate = "{{.owner}}/{{.repo}}"
)
// we've got less type safety using go templates but this lends itself better to
// users adding custom service definitions in their config
@@ -17,6 +21,7 @@ var githubServiceDef = ServiceDefinition{
commitURL: "/commit/{{.CommitHash}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var bitbucketServiceDef = ServiceDefinition{
@@ -28,7 +33,8 @@ var bitbucketServiceDef = ServiceDefinition{
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^.*@.*:/*(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
},
repoURLTemplate: defaultRepoURLTemplate,
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var gitLabServiceDef = ServiceDefinition{
@@ -38,6 +44,7 @@ var gitLabServiceDef = ServiceDefinition{
commitURL: "/-/commit/{{.CommitHash}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var azdoServiceDef = ServiceDefinition{
@@ -52,6 +59,8 @@ var azdoServiceDef = ServiceDefinition{
`^https://.*/(?P<org>.*?)/(?P<project>.*?)/_git/(?P<repo>.*?)(?:\.git)?$`,
},
repoURLTemplate: "https://{{.webDomain}}/{{.org}}/{{.project}}/_git/{{.repo}}",
// TODO: verify this is actually correct
repoNameTemplate: "{{.org}}/{{.project}}/{{.repo}}",
}
var bitbucketServerServiceDef = ServiceDefinition{
@@ -64,6 +73,8 @@ var bitbucketServerServiceDef = ServiceDefinition{
`^https://.*/scm/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
},
repoURLTemplate: "https://{{.webDomain}}/projects/{{.project}}/repos/{{.repo}}",
// TODO: verify this is actually correct
repoNameTemplate: "{{.project}}/{{.repo}}",
}
var giteaServiceDef = ServiceDefinition{
@@ -61,6 +61,18 @@ func (self *HostingServiceMgr) GetCommitURL(commitHash string) (string, error) {
return pullRequestURL, nil
}
// e.g. 'jesseduffield/lazygit'
func (self *HostingServiceMgr) GetRepoName() (string, error) {
gitService, err := self.getService()
if err != nil {
return "", err
}
repoName := gitService.repoName
return repoName, nil
}
func (self *HostingServiceMgr) getService() (*Service, error) {
serviceDomain, err := self.getServiceDomain(self.remoteURL)
if err != nil {
@@ -72,8 +84,14 @@ func (self *HostingServiceMgr) getService() (*Service, error) {
return nil, err
}
repoName, err := serviceDomain.serviceDefinition.getRepoNameFromRemoteURL(self.remoteURL)
if err != nil {
return nil, err
}
return &Service{
repoURL: repoURL,
repoName: repoName,
ServiceDefinition: serviceDomain.serviceDefinition,
}, nil
}
@@ -144,24 +162,68 @@ type ServiceDefinition struct {
regexStrings []string
// can expect 'webdomain' to be passed in. Otherwise, you get to pick what we match in the regex
repoURLTemplate string
repoURLTemplate string
repoNameTemplate string
}
func (self ServiceDefinition) getRepoURLFromRemoteURL(url string, webDomain string) (string, error) {
matches, err := self.parseRemoteUrl(url)
if err != nil {
return "", err
}
matches["webDomain"] = webDomain
return utils.ResolvePlaceholderString(self.repoURLTemplate, matches), nil
}
func (self ServiceDefinition) getRepoNameFromRemoteURL(url string) (string, error) {
matches, err := self.parseRemoteUrl(url)
if err != nil {
return "", err
}
return utils.ResolvePlaceholderString(self.repoNameTemplate, matches), nil
}
func (self ServiceDefinition) parseRemoteUrl(url string) (map[string]string, error) {
for _, regexStr := range self.regexStrings {
re := regexp.MustCompile(regexStr)
input := utils.FindNamedMatches(re, url)
if input != nil {
input["webDomain"] = webDomain
return utils.ResolvePlaceholderString(self.repoURLTemplate, input), nil
matches := utils.FindNamedMatches(re, url)
if matches != nil {
return matches, nil
}
}
return "", errors.New("Failed to parse repo information from url")
return nil, errors.New("Failed to parse repo information from url")
}
// RepoInformation holds the owner and repository name parsed from a remote URL.
type RepoInformation struct {
Owner string
Repository string
}
// GetRepoInfoFromURL parses a remote URL (SSH or HTTPS) and extracts the
// owner and repository name using the default URL regex patterns.
func GetRepoInfoFromURL(url string) (RepoInformation, error) {
for _, regexStr := range defaultUrlRegexStrings {
re := regexp.MustCompile(regexStr)
matches := utils.FindNamedMatches(re, url)
if matches != nil {
return RepoInformation{
Owner: matches["owner"],
Repository: matches["repo"],
}, nil
}
}
return RepoInformation{}, errors.New("Failed to parse repo information from url")
}
type Service struct {
repoURL string
// e.g. 'jesseduffield/lazygit'
repoName string
ServiceDefinition
}
+25
View File
@@ -0,0 +1,25 @@
package models
// TODO: see if I need to store the head repo name in case it differs from the base repo
type GithubPullRequest struct {
HeadRefName string `json:"headRefName"`
Number int `json:"number"`
Title string `json:"title"`
State string `json:"state"` // "MERGED", "OPEN", "CLOSED", "DRAFT"
Url string `json:"url"`
HeadRepositoryOwner GithubRepositoryOwner `json:"headRepositoryOwner"`
}
func (pr *GithubPullRequest) UserName() string {
// e.g. 'jesseduffield'
return pr.HeadRepositoryOwner.Login
}
func (pr *GithubPullRequest) BranchName() string {
// e.g. 'feature/my-feature'
return pr.HeadRefName
}
type GithubRepositoryOwner struct {
Login string `json:"login"`
}
-3
View File
@@ -19,9 +19,6 @@ type Worktree struct {
// * the worktree is mid-rebase on the branch
// * the worktree is mid-bisect on the branch
Branch string
// The HEAD sha of the worktree. Always populated (even when Branch is set).
// Used for display when Branch is empty (detached HEAD state).
Head string
// based on the path, but uniquified. Not the same name that git uses in the worktrees/ folder (no good reason for this,
// I just prefer my naming convention better)
Name string
+18 -1
View File
@@ -704,10 +704,27 @@ type AppState struct {
ShellCommandsHistory []string `yaml:"customcommandshistory"`
HideCommandLog bool
// Cache of GitHub pull requests per repo path, so that PR info can be
// shown instantly on startup before the async refresh completes.
GithubPullRequests map[string][]CachedPullRequest `yaml:"githubPullRequests"`
}
// CachedPullRequest stores the essential fields of a GitHub pull request
// for persisting in the app state cache.
type CachedPullRequest struct {
HeadRefName string `yaml:"headRefName"`
Number int `yaml:"number"`
Title string `yaml:"title"`
State string `yaml:"state"`
Url string `yaml:"url"`
HeadRepositoryOwner string `yaml:"headRepositoryOwner"`
}
func getDefaultAppState() *AppState {
return &AppState{}
return &AppState{
GithubPullRequests: make(map[string][]CachedPullRequest),
}
}
func LogPath() (string, error) {
+9 -8
View File
@@ -321,6 +321,8 @@ type GitConfig struct {
RemoteBranchSortOrder string `yaml:"remoteBranchSortOrder" jsonschema:"enum=date,enum=alphabetical"`
// When copying commit hashes to the clipboard, truncate them to this length. Set to 40 to disable truncation.
TruncateCopiedCommitHashesTo int `yaml:"truncateCopiedCommitHashesTo"`
// If true and if `gh` is installed and on version >=2, we will use `gh` to display pull requests against branches.
EnableGithubCli bool `yaml:"enableGithubCli"`
}
type PagerType string
@@ -493,10 +495,9 @@ type KeybindingUniversalConfig struct {
}
type KeybindingStatusConfig struct {
CheckForUpdate string `yaml:"checkForUpdate"`
RecentRepos string `yaml:"recentRepos"`
AllBranchesLogGraph string `yaml:"allBranchesLogGraph"`
AllBranchesLogGraphReverse string `yaml:"allBranchesLogGraphReverse"`
CheckForUpdate string `yaml:"checkForUpdate"`
RecentRepos string `yaml:"recentRepos"`
AllBranchesLogGraph string `yaml:"allBranchesLogGraph"`
}
type KeybindingFilesConfig struct {
@@ -858,6 +859,7 @@ func GetDefaultConfig() *UserConfig {
BranchPrefix: "",
ParseEmoji: false,
TruncateCopiedCommitHashesTo: 12,
EnableGithubCli: true,
},
Refresher: RefresherConfig{
RefreshInterval: 10,
@@ -956,10 +958,9 @@ func GetDefaultConfig() *UserConfig {
OpenDiffTool: "<c-t>",
},
Status: KeybindingStatusConfig{
CheckForUpdate: "u",
RecentRepos: "<enter>",
AllBranchesLogGraph: "a",
AllBranchesLogGraphReverse: "A",
CheckForUpdate: "u",
RecentRepos: "<enter>",
AllBranchesLogGraph: "a",
},
Files: KeybindingFilesConfig{
CommitChanges: "c",
+13 -17
View File
@@ -52,7 +52,7 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() {
}
if self.gui.Config.GetDebug() {
self.goEvery(time.Second*time.Duration(10), self.gui.stopChan, func(_ bool) error {
self.goEvery(time.Second*time.Duration(10), self.gui.stopChan, func() error {
formatBytes := func(b uint64) string {
const unit = 1000
if b < unit {
@@ -78,7 +78,7 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() {
func (self *BackgroundRoutineMgr) startBackgroundFetch() {
self.gui.waitForIntro.Wait()
fetch := func(firstTimeOrRetriggered bool) error {
fetch := func() error {
// Do this on the UI thread so that we don't have to deal with synchronization around the
// access of the repo state.
self.gui.onUIThread(func() error {
@@ -89,18 +89,14 @@ func (self *BackgroundRoutineMgr) startBackgroundFetch() {
return nil
})
if self.gui.UserConfig().Gui.ShowBottomLine || firstTimeOrRetriggered {
return self.gui.helpers.AppStatus.WithWaitingStatusImpl(self.gui.Tr.FetchingStatus, func(gocui.Task) error {
return self.backgroundFetch()
}, nil)
}
return self.backgroundFetch()
return self.gui.helpers.AppStatus.WithWaitingStatusImpl(self.gui.Tr.FetchingStatus, func(gocui.Task) error {
return self.backgroundFetch()
}, nil)
}
// We want an immediate fetch at startup, and since goEvery starts by
// waiting for the interval, we need to trigger one manually first
_ = fetch(true)
_ = fetch()
userConfig := self.gui.UserConfig()
self.triggerFetch = self.goEvery(userConfig.Refresher.FetchIntervalDuration(), self.gui.stopChan, fetch)
@@ -110,25 +106,25 @@ func (self *BackgroundRoutineMgr) startBackgroundFilesRefresh() {
self.gui.waitForIntro.Wait()
userConfig := self.gui.UserConfig()
self.goEvery(userConfig.Refresher.RefreshIntervalDuration(), self.gui.stopChan, func(_ bool) error {
self.goEvery(userConfig.Refresher.RefreshIntervalDuration(), self.gui.stopChan, func() error {
self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
return nil
})
}
// returns a channel that can be used to trigger the callback immediately
func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan struct{}, function func(bool) error) chan struct{} {
func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan struct{}, function func() error) chan struct{} {
done := make(chan struct{})
retrigger := make(chan struct{})
go utils.Safe(func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
doit := func(retriggered bool) {
doit := func() {
if self.pauseBackgroundRefreshes {
return
}
self.gui.c.OnWorker(func(gocui.Task) error {
_ = function(retriggered)
_ = function()
done <- struct{}{}
return nil
})
@@ -140,10 +136,10 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
for {
select {
case <-ticker.C:
doit(false)
doit()
case <-retrigger:
ticker.Reset(interval)
doit(true)
doit()
case <-stop:
return
}
@@ -155,7 +151,7 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
func (self *BackgroundRoutineMgr) backgroundFetch() (err error) {
err = self.gui.git.Sync.FetchBackground()
self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.SYNC})
self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS, types.PULL_REQUESTS}, Mode: types.SYNC})
if err == nil {
err = self.gui.helpers.BranchesHelper.AutoForwardBranches()
+2
View File
@@ -28,6 +28,8 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
return presentation.GetBranchListDisplayStrings(
viewModel.GetItems(),
c.State().GetItemOperation,
c.Model().PullRequests,
c.Model().Remotes,
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().Diffing.Ref,
c.Views().Branches.InnerWidth()+c.Views().Branches.OriginX(),
+1
View File
@@ -68,6 +68,7 @@ func (gui *Gui) resetHelpersAndControllers() {
mergeConflictsHelper,
worktreeHelper,
searchHelper,
suggestionsHelper,
)
diffHelper := helpers.NewDiffHelper(helperCommon)
cherryPickHelper := helpers.NewCherryPickHelper(
+96 -1
View File
@@ -3,12 +3,16 @@ package controllers
import (
"errors"
"fmt"
"strings"
"github.com/gookit/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
@@ -192,7 +196,27 @@ func (self *BranchesController) GetOnRenderToMain() func() {
} else {
cmdObj := self.c.Git().Branch.GetGraphCmdObj(branch.FullRefName())
task = types.NewRunPtyTask(cmdObj.GetCmd())
ptyTask := types.NewRunPtyTask(cmdObj.GetCmd())
task = ptyTask
// Shouldn't we hold on to the map for longer instead of generating it every time?
// It is also generated every time we render the branches list.
prs := git_commands.GenerateGithubPullRequestMap(
self.c.Model().PullRequests,
self.c.Model().Branches,
self.c.Model().Remotes,
)
if pr, ok := prs[branch.Name]; ok {
icon := lo.Ternary(icons.IsIconEnabled(), icons.IconForRemoteUrl(pr.Url)+" ", "")
ptyTask.Prefix = style.PrintHyperlink(fmt.Sprintf("%s%s %s %s\n",
icon,
coloredStateText(pr.State),
pr.Title,
style.FgCyan.Sprintf("#%d", pr.Number)),
pr.Url)
ptyTask.Prefix += strings.Repeat("─", self.c.Contexts().Normal.GetView().InnerWidth()) + "\n"
}
}
self.c.RenderToMainViews(types.RefreshMainOpts{
@@ -206,6 +230,77 @@ func (self *BranchesController) GetOnRenderToMain() func() {
}
}
func stateText(state string) string {
if icons.IsIconEnabled() {
switch state {
case "OPEN":
return " Open"
case "CLOSED":
return " Closed"
case "MERGED":
return " Merged"
case "DRAFT":
return " Draft"
default:
return ""
}
}
switch state {
case "OPEN":
return "Open"
case "CLOSED":
return "Closed"
case "MERGED":
return "Merged"
case "DRAFT":
return "Draft"
default:
return ""
}
}
func coloredStateText(state string) string {
if icons.IsIconEnabled() {
return fmt.Sprintf("%s%s%s",
withPrFgColor(state, ""),
withPrBgColor(state, style.FgWhite.Sprint(stateText(state))),
withPrFgColor(state, ""))
}
return withPrFgColor(state, stateText(state))
}
func withPrFgColor(state string, text string) string {
switch state {
case "OPEN":
return style.FgGreen.Sprint(text)
case "CLOSED":
return style.FgRed.Sprint(text)
case "MERGED":
return style.FgMagenta.Sprint(text)
case "DRAFT":
return color.RGB(0x66, 0x66, 0x66, false).Sprint(text)
default:
return style.FgDefault.Sprint(text)
}
}
func withPrBgColor(state string, text string) string {
switch state {
case "OPEN":
return style.BgGreen.Sprint(text)
case "CLOSED":
return style.BgRed.Sprint(text)
case "MERGED":
return style.BgMagenta.Sprint(text)
case "DRAFT":
return color.RGB(0x66, 0x66, 0x66, true).Sprint(text)
default:
return style.BgDefault.Sprint(text)
}
}
func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branch) error {
upstream := lo.Ternary(selectedBranch.RemoteBranchStoredLocally(),
selectedBranch.ShortUpstreamRefName(),
+12 -33
View File
@@ -62,8 +62,8 @@ func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []
{
Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.withItems(self.discard),
GetDisabledReason: self.require(self.itemsSelected(self.canDiscardFileChanges)),
Description: self.c.Tr.Discard,
GetDisabledReason: self.require(self.itemsSelected()),
Description: self.c.Tr.Remove,
Tooltip: self.c.Tr.DiscardOldFileChangeTooltip,
DisplayOnScreen: true,
},
@@ -302,13 +302,18 @@ func (self *CommitFilesController) checkout(node *filetree.CommitFileNode) error
}
func (self *CommitFilesController) discard(selectedNodes []*filetree.CommitFileNode) error {
prompt := lo.Ternary(self.c.Git().Patch.PatchBuilder.Active(),
self.c.Tr.DiscardFileChangesPromptResetPatch,
self.c.Tr.DiscardFileChangesPrompt)
parentContext := self.c.Context().Current().GetParentContext()
if parentContext == nil || parentContext.GetKey() != context.LOCAL_COMMITS_CONTEXT_KEY {
return errors.New(self.c.Tr.CanOnlyDiscardFromLocalCommits)
}
if ok, err := self.c.Helpers().PatchBuilding.ValidateNormalWorkingTreeState(); !ok {
return err
}
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DiscardFileChangesTitle,
Prompt: prompt,
Prompt: self.c.Tr.DiscardFileChangesPrompt,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
var filePaths []string
@@ -343,32 +348,6 @@ func (self *CommitFilesController) discard(selectedNodes []*filetree.CommitFileN
return nil
}
func (self *CommitFilesController) canDiscardFileChanges(nodes []*filetree.CommitFileNode) *types.DisabledReason {
parentContext := self.c.Context().Current().GetParentContext()
if parentContext == nil || parentContext.GetKey() != context.LOCAL_COMMITS_CONTEXT_KEY {
return &types.DisabledReason{
Text: self.c.Tr.CanOnlyDiscardFromLocalCommits,
ShowErrorInPanel: true,
}
}
if self.c.Contexts().LocalCommits.AreMultipleItemsSelected() {
return &types.DisabledReason{
Text: self.c.Tr.CannotDiscardFromMultipleCommits,
ShowErrorInPanel: true,
}
}
if self.c.Git().Status.WorkingTreeState().Any() {
return &types.DisabledReason{
Text: self.c.Tr.CantPatchWhileRebasingError,
ShowErrorInPanel: true,
}
}
return nil
}
func (self *CommitFilesController) open(node *filetree.CommitFileNode) error {
return self.c.Helpers().Files.OpenFile(node.GetPath())
}
@@ -578,7 +557,7 @@ func (self *CommitFilesController) GetOnClickFocusedMainView() func(mainViewName
func (self *CommitFilesController) pathsForDiff(node *filetree.CommitFileNode) []string {
if !node.IsFile() && self.context().IsFiltering() {
var paths []string
_ = node.ForEachFile(func(file *models.CommitFile) error {
node.ForEachFile(func(file *models.CommitFile) error {
paths = append(paths, file.Path)
return nil
})
+55 -13
View File
@@ -7,10 +7,25 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
// This controller lets you change the context size for diffs. The 'context' in 'context size' refers to the conventional meaning of the word 'context' in a diff, as opposed to lazygit's own idea of a 'context'.
var CONTEXT_KEYS_SHOWING_DIFFS = []types.ContextKey{
context.FILES_CONTEXT_KEY,
context.COMMIT_FILES_CONTEXT_KEY,
context.STASH_CONTEXT_KEY,
context.LOCAL_COMMITS_CONTEXT_KEY,
context.SUB_COMMITS_CONTEXT_KEY,
context.STAGING_MAIN_CONTEXT_KEY,
context.STAGING_SECONDARY_CONTEXT_KEY,
context.PATCH_BUILDING_MAIN_CONTEXT_KEY,
context.PATCH_BUILDING_SECONDARY_CONTEXT_KEY,
context.NORMAL_MAIN_CONTEXT_KEY,
context.NORMAL_SECONDARY_CONTEXT_KEY,
}
type ContextLinesController struct {
baseController
c *ControllerCommon
@@ -51,31 +66,39 @@ func (self *ContextLinesController) Context() types.Context {
}
func (self *ContextLinesController) Increase() error {
if err := self.checkCanChangeContext(); err != nil {
return err
if self.isShowingDiff() {
if err := self.checkCanChangeContext(); err != nil {
return err
}
if self.c.UserConfig().Git.DiffContextSize < math.MaxUint64 {
self.c.UserConfig().Git.DiffContextSize++
}
return self.applyChange()
}
if self.c.UserConfig().Git.DiffContextSize < math.MaxUint64 {
self.c.UserConfig().Git.DiffContextSize++
}
return self.applyChange()
return nil
}
func (self *ContextLinesController) Decrease() error {
if err := self.checkCanChangeContext(); err != nil {
return err
if self.isShowingDiff() {
if err := self.checkCanChangeContext(); err != nil {
return err
}
if self.c.UserConfig().Git.DiffContextSize > 0 {
self.c.UserConfig().Git.DiffContextSize--
}
return self.applyChange()
}
if self.c.UserConfig().Git.DiffContextSize > 0 {
self.c.UserConfig().Git.DiffContextSize--
}
return self.applyChange()
return nil
}
func (self *ContextLinesController) applyChange() error {
self.c.Toast(fmt.Sprintf(self.c.Tr.DiffContextSizeChanged, self.c.UserConfig().Git.DiffContextSize))
currentContext := self.c.Context().CurrentSide()
currentContext := self.currentSidePanel()
switch currentContext.GetKey() {
// we make an exception for our staging and patch building contexts because they actually need to refresh their state afterwards.
case context.PATCH_BUILDING_MAIN_CONTEXT_KEY:
@@ -95,3 +118,22 @@ func (self *ContextLinesController) checkCanChangeContext() error {
return nil
}
func (self *ContextLinesController) isShowingDiff() bool {
return lo.Contains(
CONTEXT_KEYS_SHOWING_DIFFS,
self.currentSidePanel().GetKey(),
)
}
func (self *ContextLinesController) currentSidePanel() types.Context {
currentContext := self.c.Context().CurrentStatic()
if currentContext.GetKey() == context.NORMAL_MAIN_CONTEXT_KEY ||
currentContext.GetKey() == context.NORMAL_SECONDARY_CONTEXT_KEY {
if sidePanelContext := self.c.Context().NextInStack(currentContext); sidePanelContext != nil {
return sidePanelContext
}
}
return currentContext
}
@@ -123,6 +123,13 @@ func (self *CustomPatchOptionsMenuAction) getPatchCommitIndex() int {
return -1
}
func (self *CustomPatchOptionsMenuAction) validateNormalWorkingTreeState() (bool, error) {
if self.c.Git().Status.WorkingTreeState().Any() {
return false, errors.New(self.c.Tr.CantPatchWhileRebasingError)
}
return true, nil
}
func (self *CustomPatchOptionsMenuAction) returnFocusFromPatchExplorerIfNecessary() {
if self.c.Context().Current().GetKey() == self.c.Contexts().CustomPatchBuilder.GetKey() {
self.c.Helpers().PatchBuilding.Escape()
@@ -130,6 +137,10 @@ func (self *CustomPatchOptionsMenuAction) returnFocusFromPatchExplorerIfNecessar
}
func (self *CustomPatchOptionsMenuAction) handleDeletePatchFromCommit() error {
if ok, err := self.validateNormalWorkingTreeState(); !ok {
return err
}
self.returnFocusFromPatchExplorerIfNecessary()
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
@@ -141,6 +152,10 @@ func (self *CustomPatchOptionsMenuAction) handleDeletePatchFromCommit() error {
}
func (self *CustomPatchOptionsMenuAction) handleMovePatchToSelectedCommit() error {
if ok, err := self.validateNormalWorkingTreeState(); !ok {
return err
}
self.returnFocusFromPatchExplorerIfNecessary()
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
@@ -152,6 +167,10 @@ func (self *CustomPatchOptionsMenuAction) handleMovePatchToSelectedCommit() erro
}
func (self *CustomPatchOptionsMenuAction) handleMovePatchIntoWorkingTree() error {
if ok, err := self.validateNormalWorkingTreeState(); !ok {
return err
}
self.returnFocusFromPatchExplorerIfNecessary()
mustStash := self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules()
@@ -170,6 +189,10 @@ func (self *CustomPatchOptionsMenuAction) handleMovePatchIntoWorkingTree() error
}
func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error {
if ok, err := self.validateNormalWorkingTreeState(); !ok {
return err
}
self.returnFocusFromPatchExplorerIfNecessary()
commitIndex := self.getPatchCommitIndex()
@@ -201,6 +224,10 @@ func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error {
}
func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommitBefore() error {
if ok, err := self.validateNormalWorkingTreeState(); !ok {
return err
}
self.returnFocusFromPatchExplorerIfNecessary()
commitIndex := self.getPatchCommitIndex()
+4 -4
View File
@@ -441,7 +441,7 @@ func (self *FilesController) pressWithLock(selectedNodes []*filetree.FileNode) e
if self.context().IsFiltering() {
var paths []string
for _, node := range nodes {
_ = node.ForEachFile(func(file *models.File) error {
node.ForEachFile(func(file *models.File) error {
paths = append(paths, file.Path)
return nil
})
@@ -529,7 +529,7 @@ func (self *FilesController) press(nodes []*filetree.FileNode) error {
func (self *FilesController) pathOverridesForDiff(node *filetree.FileNode) []string {
if !node.IsFile() && self.context().IsFiltering() {
var paths []string
_ = node.ForEachFile(func(file *models.File) error {
node.ForEachFile(func(file *models.File) error {
paths = append(paths, file.Path)
return nil
})
@@ -543,7 +543,7 @@ func (self *FilesController) pathOverridesForDiff(node *filetree.FileNode) []str
func (self *FilesController) unstageFilteredFiles(nodes []*filetree.FileNode) error {
var trackedPaths, untrackedPaths []string
for _, node := range nodes {
_ = node.ForEachFile(func(file *models.File) error {
node.ForEachFile(func(file *models.File) error {
if file.Tracked || file.HasStagedChanges {
trackedPaths = append(trackedPaths, file.Path)
} else {
@@ -713,7 +713,7 @@ func (self *FilesController) toggleStagedAllWithLock() error {
if self.context().IsFiltering() {
// When filtering, only stage visible files
var paths []string
_ = root.ForEachFile(func(file *models.File) error {
root.ForEachFile(func(file *models.File) error {
paths = append(paths, file.Path)
return nil
})
@@ -102,13 +102,11 @@ func getMessageHeight(wrap bool, editable bool, message string, width int, tabWi
return len(wrappedLines)
}
func (self *ConfirmationHelper) getPopupPanelDimensionsForContentHeight(contentWidth, contentHeight int, parentPopupContext types.Context) (int, int, int, int) {
return self.getPopupPanelDimensionsAux(contentWidth, contentHeight, parentPopupContext)
func (self *ConfirmationHelper) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int, parentPopupContext types.Context) (int, int, int, int) {
return self.getPopupPanelDimensionsAux(panelWidth, contentHeight, parentPopupContext)
}
func (self *ConfirmationHelper) getPopupPanelDimensionsAux(contentWidth int, contentHeight int, parentPopupContext types.Context) (int, int, int, int) {
panelWidth := contentWidth + 2 // plus 2 for the frame
panelHeight := contentHeight + 2
func (self *ConfirmationHelper) getPopupPanelDimensionsAux(panelWidth int, panelHeight int, parentPopupContext types.Context) (int, int, int, int) {
width, height := self.c.GocuiGui().Size()
if panelHeight > height*3/4 {
panelHeight = height * 3 / 4
@@ -119,19 +117,14 @@ func (self *ConfirmationHelper) getPopupPanelDimensionsAux(contentWidth int, con
x0, y0, _, _ := parentPopupContext.GetView().Dimensions()
x0 += 2
y0 += 1
return x0, y0, x0 + panelWidth - 1, y0 + panelHeight - 1
return x0, y0, x0 + panelWidth, y0 + panelHeight + 1
}
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2,
// Currently, X1/Y1 of a gocui view is one less than you would expect based on its
// width/height, so we need to subtract 1 here. See
// https://github.com/jesseduffield/lazygit/commit/f6f2a52dee8bba3ebd7e3b34b4b7c7d3e3795f3e
width/2 + panelWidth/2 - 1,
height/2 + panelHeight/2 - 1
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
// Returns the outer width of the view, including its frame. To decide how to wrap text, subtract 2.
// Also, note that X1-X0 of the view is one less than this.
func (self *ConfirmationHelper) getPopupPanelWidth(maxWidth int) int {
width, _ := self.c.GocuiGui().Size()
// we want a minimum width up to a point, then we do it based on ratio, but only up to the given max width
@@ -338,7 +331,7 @@ func (self *ConfirmationHelper) resizeMenu(parentPopupContext types.Context) {
panelWidth := self.getPopupPanelWidth(90)
contentWidth := panelWidth - 2 // minus 2 for the frame
promptLinesCount := self.layoutMenuPrompt(contentWidth)
x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(contentWidth, itemCount+offset+promptLinesCount, parentPopupContext)
x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset+promptLinesCount, parentPopupContext)
menuBottom := y1 - offset
_, _ = self.c.GocuiGui().SetView(self.c.Views().Menu.Name(), x0, y0, x1, menuBottom, 0)
@@ -386,8 +379,8 @@ func (self *ConfirmationHelper) resizeConfirmationPanel(parentPopupContext types
contentWidth := panelWidth - 2 // minus 2 for the frame
confirmationView := self.c.Views().Confirmation
prompt := confirmationView.Buffer()
contentHeight := getMessageHeight(true, false, prompt, contentWidth, confirmationView.TabWidth)
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(contentWidth, contentHeight, parentPopupContext)
panelHeight := getMessageHeight(true, false, prompt, contentWidth, confirmationView.TabWidth)
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight, parentPopupContext)
_, _ = self.c.GocuiGui().SetView(confirmationView.Name(), x0, y0, x1, y1, 0)
}
@@ -400,8 +393,8 @@ func (self *ConfirmationHelper) resizePromptPanel(parentPopupContext types.Conte
contentWidth := panelWidth - 2 // minus 2 for the frame
promptView := self.c.Views().Prompt
prompt := promptView.TextArea.GetContent()
contentHeight := getMessageHeight(false, true, prompt, contentWidth, promptView.TabWidth) + suggestionsViewHeight
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(contentWidth, contentHeight, parentPopupContext)
panelHeight := getMessageHeight(false, true, prompt, contentWidth, promptView.TabWidth) + suggestionsViewHeight
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight, parentPopupContext)
promptViewBottom := y1 - suggestionsViewHeight
_, _ = self.c.GocuiGui().SetView(promptView.Name(), x0, y0, x1, promptViewBottom, 0)
@@ -417,17 +410,14 @@ func (self *ConfirmationHelper) ResizeCommitMessagePanels(parentPopupContext typ
maxWidth = self.c.UserConfig().Git.Commit.AutoWrapWidth + 25
}
panelWidth := self.getPopupPanelWidth(maxWidth)
contentWidth := panelWidth - 2 // minus 2 for the frame
content := self.c.Views().CommitDescription.TextArea.GetContent()
summaryViewHeight := 3
// The width we pass to getMessageHeight is irrelevant because we don't wrap the text to the
// view width. If we did, we'd have to subtract 2 for the view frame.
contentHeight := getMessageHeight(false, true, content, contentWidth, self.c.Views().CommitDescription.TabWidth)
panelHeight := getMessageHeight(false, true, content, panelWidth, self.c.Views().CommitDescription.TabWidth)
minHeight := 7
if contentHeight < minHeight {
contentHeight = minHeight
if panelHeight < minHeight {
panelHeight = minHeight
}
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(contentWidth, contentHeight, parentPopupContext)
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight, parentPopupContext)
_, _ = 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)
+1 -1
View File
@@ -27,7 +27,6 @@ type Helpers struct {
MergeAndRebase *MergeAndRebaseHelper
MergeConflicts *MergeConflictsHelper
CherryPick *CherryPickHelper
Host *HostHelper
PatchBuilding *PatchBuildingHelper
Staging *StagingHelper
GPG *GpgHelper
@@ -53,6 +52,7 @@ type Helpers struct {
Search *SearchHelper
Worktree *WorktreeHelper
SubCommits *SubCommitsHelper
Host *HostHelper
}
func NewStubHelpers() *Helpers {
@@ -1,6 +1,7 @@
package helpers
import (
"errors"
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
@@ -21,6 +22,13 @@ func NewPatchBuildingHelper(
}
}
func (self *PatchBuildingHelper) ValidateNormalWorkingTreeState() (bool, error) {
if self.c.Git().Status.WorkingTreeState().Any() {
return false, errors.New(self.c.Tr.CantPatchWhileRebasingError)
}
return true, nil
}
func (self *PatchBuildingHelper) ShowHunkStagingHint() {
if !self.c.AppState.DidShowHunkStagingHint && self.c.UserConfig().Gui.UseHunkModeInStagingView {
self.c.AppState.DidShowHunkStagingHint = true
@@ -1,6 +1,7 @@
package helpers
import (
"fmt"
"strings"
"sync"
"time"
@@ -9,6 +10,7 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
@@ -27,6 +29,7 @@ type RefreshHelper struct {
mergeConflictsHelper *MergeConflictsHelper
worktreeHelper *WorktreeHelper
searchHelper *SearchHelper
suggestionsHelper *SuggestionsHelper
}
func NewRefreshHelper(
@@ -38,6 +41,7 @@ func NewRefreshHelper(
mergeConflictsHelper *MergeConflictsHelper,
worktreeHelper *WorktreeHelper,
searchHelper *SearchHelper,
suggestionsHelper *SuggestionsHelper,
) *RefreshHelper {
return &RefreshHelper{
c: c,
@@ -48,6 +52,7 @@ func NewRefreshHelper(
mergeConflictsHelper: mergeConflictsHelper,
worktreeHelper: worktreeHelper,
searchHelper: searchHelper,
suggestionsHelper: suggestionsHelper,
}
}
@@ -91,6 +96,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) {
types.STATUS,
types.BISECT_INFO,
types.STAGING,
types.PULL_REQUESTS,
})
} else {
scopeSet = set.NewFromSlice(options.Scope)
@@ -117,6 +123,10 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) {
}
}
if scopeSet.Includes(types.PULL_REQUESTS) {
refresh("pull requests", func() { _ = self.refreshGithubPullRequests() })
}
includeWorktreesWithBranches := false
if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) {
// whenever we change commits, we should update branches because the upstream/downstream
@@ -757,3 +767,188 @@ func (self *RefreshHelper) refreshView(context types.Context) {
return nil
})
}
func (self *RefreshHelper) refreshGithubPullRequests() error {
self.c.Mutexes().RefreshingPullRequestsMutex.Lock()
defer self.c.Mutexes().RefreshingPullRequestsMutex.Unlock()
if !self.c.UserConfig().Git.EnableGithubCli {
return nil
}
if !self.c.Git().GitHub.InGithubRepo() {
self.c.Model().PullRequests = []*models.GithubPullRequest{}
return nil
}
// Load cached PRs for instant display while we fetch fresh data
self.loadCachedPullRequests()
switch self.c.State().GetGitHubCliState() {
case types.UNKNOWN:
state := self.determineGithubCliState()
self.c.State().SetGitHubCliState(state)
if state != types.VALID {
if state == types.INVALID_VERSION {
// todo: i18n
self.c.LogAction("gh version is too old (must be version 2 or greater), so pull requests will not be shown against branches.")
}
return nil
}
case types.VALID:
// continue on
default:
return nil
}
if err := self.c.Git().GitHub.BaseRepo(); err != nil {
ok, err := self.promptForBaseGithubRepo()
if err != nil {
return err
}
if !ok {
return nil
}
}
if err := self.setGithubPullRequests(); err != nil {
self.c.LogAction(fmt.Sprintf("Error fetching pull requests from GitHub: %s", err.Error()))
}
return nil
}
func (self *RefreshHelper) promptForBaseGithubRepo() (bool, error) {
err := self.refreshRemotes()
if err != nil {
return false, err
}
remotes := self.c.Model().Remotes
if len(remotes) == 0 {
return false, nil
}
// If there's only one remote, or if 'origin' exists, use it automatically
remote, hasOrigin := lo.Find(remotes, func(r *models.Remote) bool {
return r.Name == "origin"
})
if !hasOrigin {
if len(remotes) == 1 {
remote = remotes[0]
} else {
// Multiple remotes with no 'origin': prompt the user
self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.SelectRemoteRepository,
InitialContent: "",
FindSuggestionsFunc: self.suggestionsHelper.GetRemoteRepoSuggestionsFunc(),
HandleConfirm: func(repository string) error {
return self.c.WithWaitingStatus(self.c.Tr.LcSelectingRemote, func(gocui.Task) error {
// `repository` is something like 'jesseduffield/lazygit'
_, err := self.c.Git().GitHub.SetBaseRepo(repository)
if err != nil {
return err
}
return self.refreshGithubPullRequests()
})
},
})
return false, nil
}
}
if len(remote.Urls) == 0 {
return false, nil
}
repoName, err := self.c.Git().HostingService.GetRepoNameFromRemoteURL(remote.Urls[0])
if err != nil {
self.c.Log.Error(err)
return false, nil
}
_, err = self.c.Git().GitHub.SetBaseRepo(repoName)
if err != nil {
self.c.Log.Error(err)
}
return true, nil
}
func (self *RefreshHelper) determineGithubCliState() types.GitHubCliState {
installed, validVersion := self.c.Git().GitHub.DetermineGitHubCliState()
if validVersion {
return types.VALID
} else if installed {
return types.INVALID_VERSION
}
return types.NOT_INSTALLED
}
func (self *RefreshHelper) setGithubPullRequests() error {
branches := lo.Filter(self.c.Model().Branches, func(branch *models.Branch, _ int) bool {
return branch.IsTrackingRemote()
})
branchNames := lo.Map(branches, func(branch *models.Branch, _ int) string {
return branch.UpstreamBranch
})
prs, err := self.c.Git().GitHub.FetchRecentPRs(branchNames)
if err != nil {
return err
}
self.c.Model().PullRequests = prs
self.savePullRequestsToCache(prs)
self.c.PostRefreshUpdate(self.c.Contexts().Branches)
return nil
}
func (self *RefreshHelper) loadCachedPullRequests() {
repoPath := self.c.Git().RepoPaths.RepoPath()
cachedPRs := self.c.GetAppState().GithubPullRequests[repoPath]
if len(cachedPRs) == 0 {
return
}
prs := lo.Map(cachedPRs, func(cached config.CachedPullRequest, _ int) *models.GithubPullRequest {
return &models.GithubPullRequest{
HeadRefName: cached.HeadRefName,
Number: cached.Number,
Title: cached.Title,
State: cached.State,
Url: cached.Url,
HeadRepositoryOwner: models.GithubRepositoryOwner{
Login: cached.HeadRepositoryOwner,
},
}
})
self.c.Model().PullRequests = prs
self.c.PostRefreshUpdate(self.c.Contexts().Branches)
}
func (self *RefreshHelper) savePullRequestsToCache(prs []*models.GithubPullRequest) {
repoPath := self.c.Git().RepoPaths.RepoPath()
cached := lo.Map(prs, func(pr *models.GithubPullRequest, _ int) config.CachedPullRequest {
return config.CachedPullRequest{
HeadRefName: pr.HeadRefName,
Number: pr.Number,
Title: pr.Title,
State: pr.State,
Url: pr.Url,
HeadRepositoryOwner: pr.HeadRepositoryOwner.Login,
}
})
appState := self.c.GetAppState()
if appState.GithubPullRequests == nil {
appState.GithubPullRequests = make(map[string][]config.CachedPullRequest)
}
appState.GithubPullRequests[repoPath] = cached
self.c.SaveAppStateAndLogError()
}
+18 -1
View File
@@ -54,7 +54,24 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions
// loading a heap of commits is slow so we limit them whenever doing a reset
self.c.Contexts().LocalCommits.SetLimitCommits(true)
self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI, KeepBranchSelectionIndex: true})
self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI, KeepBranchSelectionIndex: true, Scope: []types.RefreshableView{
types.COMMITS,
types.BRANCHES,
types.FILES,
types.STASH,
types.REFLOG,
types.TAGS,
types.REMOTES,
types.WORKTREES,
types.STATUS,
types.BISECT_INFO,
types.STAGING,
}, Then: func() {
// Fetch PRs in the background after checkout; the new branch may
// have a PR that we haven't fetched yet. We do this in Then so
// that branches are loaded first (PRs are matched against branches).
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.PULL_REQUESTS}})
}})
}
localBranch, found := lo.Find(self.c.Model().Branches, func(branch *models.Branch) bool {
@@ -64,6 +64,30 @@ func (self *SuggestionsHelper) getBranchNames() []string {
})
}
func (self *SuggestionsHelper) GetRemoteRepoSuggestionsFunc() func(string) []*types.Suggestion {
repoNames := self.getRemoteRepoNames()
return FilterFunc(repoNames, self.c.UserConfig().Gui.UseFuzzySearch())
}
func (self *SuggestionsHelper) getRemoteRepoNames() []string {
remotes := self.c.Model().Remotes
result := make([]string, 0, len(remotes))
for _, remote := range remotes {
if len(remote.Urls) == 0 {
continue
}
repoName, err := self.c.Git().HostingService.GetRepoNameFromRemoteURL(remote.Urls[0])
if err != nil {
self.c.Log.Error(err)
continue
}
result = append(result, repoName)
}
return result
}
func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*types.Suggestion {
branchNames := self.getBranchNames()
+2 -15
View File
@@ -4,6 +4,7 @@ import (
"errors"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@@ -60,19 +61,5 @@ func (self *UpstreamHelper) PromptForUpstreamWithoutInitialContent(_ *models.Bra
}
func (self *UpstreamHelper) GetSuggestedRemote() string {
return getSuggestedRemote(self.c.Model().Remotes)
}
func getSuggestedRemote(remotes []*models.Remote) string {
if len(remotes) == 0 {
return "origin"
}
for _, remote := range remotes {
if remote.Name == "origin" {
return remote.Name
}
}
return remotes[0].Name
return git_commands.GetSuggestedRemoteName(self.c.Model().Remotes)
}
@@ -1,31 +0,0 @@
package helpers
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
func TestGetSuggestedRemote(t *testing.T) {
cases := []struct {
remotes []*models.Remote
expected string
}{
{mkRemoteList(), "origin"},
{mkRemoteList("upstream", "origin", "foo"), "origin"},
{mkRemoteList("upstream", "foo", "bar"), "upstream"},
}
for _, c := range cases {
result := getSuggestedRemote(c.remotes)
assert.EqualValues(t, c.expected, result)
}
}
func mkRemoteList(names ...string) []*models.Remote {
return lo.Map(names, func(name string, _ int) *models.Remote {
return &models.Remote{Name: name}
})
}
@@ -47,7 +47,7 @@ func (self *PatchBuildingController) GetKeybindings(opts types.KeybindingsOpts)
},
{
Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.discardSelection,
Handler: self.DiscardSelection,
GetDisabledReason: self.getDisabledReasonForDiscard,
Description: self.c.Tr.RemoveSelectionFromPatch,
Tooltip: self.c.Tr.RemoveSelectionFromPatchTooltip,
@@ -181,42 +181,55 @@ func (self *PatchBuildingController) toggleSelection() error {
func (self *PatchBuildingController) getDisabledReasonForDiscard() *types.DisabledReason {
if !self.c.Git().Patch.PatchBuilder.CanRebase {
return &types.DisabledReason{Text: self.c.Tr.CanOnlyDiscardFromLocalCommits, ShowErrorInPanel: true}
return &types.DisabledReason{Text: self.c.Tr.CanOnlyRemoveLinesFromLocalCommits}
}
if self.c.Git().Status.WorkingTreeState().Any() {
return &types.DisabledReason{Text: self.c.Tr.CantPatchWhileRebasingError, ShowErrorInPanel: true}
}
if self.c.UserConfig().Git.DiffContextSize == 0 {
text := fmt.Sprintf(self.c.Tr.Actions.NotEnoughContextToRemoveLines,
keybindings.Label(self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView))
return &types.DisabledReason{Text: text, ShowErrorInPanel: true}
if !self.c.Git().Patch.PatchBuilder.IsEmpty() {
return &types.DisabledReason{Text: self.c.Tr.MustClearPatchBeforeRemovingLines}
}
return nil
}
func (self *PatchBuildingController) discardSelection() error {
prompt := lo.Ternary(self.c.Git().Patch.PatchBuilder.IsEmpty(),
self.c.Tr.DiscardLinesFromCommitPrompt,
self.c.Tr.DiscardLinesFromCommitPromptWithReset)
func (self *PatchBuildingController) DiscardSelection() error {
if self.c.UserConfig().Git.DiffContextSize == 0 {
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToRemoveLines,
keybindings.Label(self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView))
}
if ok, err := self.c.Helpers().PatchBuilding.ValidateNormalWorkingTreeState(); !ok {
return err
}
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DiscardLinesFromCommitTitle,
Prompt: prompt,
Title: self.c.Tr.RemoveLinesFromCommitTitle,
Prompt: self.c.Tr.RemoveLinesFromCommitPrompt,
HandleConfirm: func() error {
return self.discardSelectionFromCommit()
return self.removeSelectionFromCommit()
},
})
return nil
}
func (self *PatchBuildingController) discardSelectionFromCommit() error {
// Reset the current patch if there is one.
if !self.c.Git().Patch.PatchBuilder.IsEmpty() {
self.c.Git().Patch.PatchBuilder.Reset()
func (self *PatchBuildingController) addSelectionToPatch() error {
self.context().GetMutex().Lock()
defer self.context().GetMutex().Unlock()
filename := self.c.Contexts().CommitFiles.GetSelectedPath()
if filename == "" {
return nil
}
if err := self.toggleSelection(); err != nil {
state := self.context().GetState()
lineIndicesToToggle := state.LineIndicesOfAddedOrDeletedLinesInSelectedPatchRange()
if len(lineIndicesToToggle) == 0 {
return nil
}
return self.c.Git().Patch.PatchBuilder.AddFileLineRange(filename, lineIndicesToToggle)
}
func (self *PatchBuildingController) removeSelectionFromCommit() error {
if err := self.addSelectionToPatch(); err != nil {
return err
}
@@ -224,13 +237,13 @@ func (self *PatchBuildingController) discardSelectionFromCommit() error {
return nil
}
return self.c.WithWaitingStatusSync(self.c.Tr.RebasingStatus, func() error {
self.c.Helpers().PatchBuilding.Escape()
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
commitIndex := self.getPatchCommitIndex()
self.c.LogAction(self.c.Tr.Actions.RemovePatchFromCommit)
err := self.c.Git().Patch.DeletePatchesFromCommit(self.c.Model().Commits, commitIndex)
self.c.Helpers().PatchBuilding.Escape()
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
err, types.RefreshOptions{Mode: types.SYNC})
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
})
}
@@ -5,10 +5,20 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
// This controller lets you change the similarity threshold for detecting renames.
var CONTEXT_KEYS_SHOWING_RENAMES = []types.ContextKey{
context.FILES_CONTEXT_KEY,
context.SUB_COMMITS_CONTEXT_KEY,
context.LOCAL_COMMITS_CONTEXT_KEY,
context.STASH_CONTEXT_KEY,
context.NORMAL_MAIN_CONTEXT_KEY,
context.NORMAL_SECONDARY_CONTEXT_KEY,
}
type RenameSimilarityThresholdController struct {
baseController
c *ControllerCommon
@@ -51,27 +61,29 @@ func (self *RenameSimilarityThresholdController) Context() types.Context {
func (self *RenameSimilarityThresholdController) Increase() error {
old_size := self.c.UserConfig().Git.RenameSimilarityThreshold
if old_size < 100 {
if self.isShowingRenames() && old_size < 100 {
self.c.UserConfig().Git.RenameSimilarityThreshold = min(100, old_size+5)
return self.applyChange()
}
return self.applyChange()
return nil
}
func (self *RenameSimilarityThresholdController) Decrease() error {
old_size := self.c.UserConfig().Git.RenameSimilarityThreshold
if old_size > 5 {
if self.isShowingRenames() && old_size > 5 {
self.c.UserConfig().Git.RenameSimilarityThreshold = max(5, old_size-5)
return self.applyChange()
}
return self.applyChange()
return nil
}
func (self *RenameSimilarityThresholdController) applyChange() error {
self.c.Toast(fmt.Sprintf(self.c.Tr.RenameSimilarityThresholdChanged, self.c.UserConfig().Git.RenameSimilarityThreshold))
currentContext := self.c.Context().CurrentSide()
currentContext := self.currentSidePanel()
switch currentContext.GetKey() {
// we make an exception for our files context, because it actually need to refresh its state afterwards.
case context.FILES_CONTEXT_KEY:
@@ -81,3 +93,22 @@ func (self *RenameSimilarityThresholdController) applyChange() error {
}
return nil
}
func (self *RenameSimilarityThresholdController) isShowingRenames() bool {
return lo.Contains(
CONTEXT_KEYS_SHOWING_RENAMES,
self.currentSidePanel().GetKey(),
)
}
func (self *RenameSimilarityThresholdController) currentSidePanel() types.Context {
currentContext := self.c.Context().CurrentStatic()
if currentContext.GetKey() == context.NORMAL_MAIN_CONTEXT_KEY ||
currentContext.GetKey() == context.NORMAL_SECONDARY_CONTEXT_KEY {
if sidePanelContext := self.c.Context().NextInStack(currentContext); sidePanelContext != nil {
return sidePanelContext
}
}
return currentContext
}
@@ -41,11 +41,6 @@ func (self *ScreenModeActions) rerenderViewsWithScreenModeDependentContent() {
self.rerenderView(context.GetView())
}
}
// Rerender the main view; for views that display a diff this is necessary in case a custom
// pager depends on the width of the view. For other views it isn't needed, but we don't bother
// making a distinction here, as rerendering the main view unnecessarily is not a big deal.
self.c.Context().CurrentSide().HandleRenderToMain()
}
func (self *ScreenModeActions) rerenderView(view *gocui.View) {
-17
View File
@@ -63,11 +63,6 @@ func (self *StatusController) GetKeybindings(opts types.KeybindingsOpts) []*type
Handler: func() error { self.switchToOrRotateAllBranchesLogs(); return nil },
Description: self.c.Tr.AllBranchesLogGraph,
},
{
Key: opts.GetKey(opts.Config.Status.AllBranchesLogGraphReverse),
Handler: func() error { self.switchToOrRotateAllBranchesLogsBackward(); return nil },
Description: self.c.Tr.AllBranchesLogGraphReverse,
},
}
return bindings
@@ -211,18 +206,6 @@ func (self *StatusController) switchToOrRotateAllBranchesLogs() {
self.showAllBranchLogs()
}
// Switches to the all branches view, or, if already on that view,
// rotates to the previous command in the list, and then renders it.
func (self *StatusController) switchToOrRotateAllBranchesLogsBackward() {
// A bit of a hack to ensure we only rotate to the previous branch log command
// if we currently are looking at a branch log. Otherwise, we should just show
// the current index (if we are coming from the dashboard).
if self.c.Views().Main.Title != self.c.Tr.StatusTitle {
self.c.Git().Branch.RotateAllBranchesLogIdxBackward()
}
self.showAllBranchLogs()
}
func (self *StatusController) showDashboard() {
versionStr := "master"
version, err := types.ParseVersionNumber(self.c.GetConfig().GetVersion())
+1 -6
View File
@@ -10,7 +10,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type WorktreesController struct {
@@ -96,11 +95,7 @@ func (self *WorktreesController) GetOnRenderToMain() func() {
var builder strings.Builder
w := tabwriter.NewWriter(&builder, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "%s:\t%s%s\n", self.c.Tr.Name, style.FgGreen.Sprint(worktree.Name), main)
branch := style.FgYellow.Sprint(worktree.Branch)
if worktree.Branch == "" && worktree.Head != "" {
branch = style.FgYellow.Sprintf("HEAD detached at %s", utils.ShortHash(worktree.Head))
}
_, _ = fmt.Fprintf(w, "%s:\t%s\n", self.c.Tr.Branch, branch)
_, _ = fmt.Fprintf(w, "%s:\t%s\n", self.c.Tr.Branch, style.FgYellow.Sprint(worktree.Branch))
_, _ = fmt.Fprintf(w, "%s:\t%s%s\n", self.c.Tr.Path, style.FgCyan.Sprint(worktree.Path), missing)
_ = w.Flush()
+11 -20
View File
@@ -148,6 +148,7 @@ type Gui struct {
integrationTest integrationTypes.IntegrationTest
afterLayoutFuncs chan func() error
gitHubCliState types.GitHubCliState
}
type StateAccessor struct {
@@ -221,6 +222,14 @@ func (self *StateAccessor) ClearItemOperation(item types.HasUrn) {
delete(self.gui.itemOperations, item.URN())
}
func (self *StateAccessor) GetGitHubCliState() types.GitHubCliState {
return self.gui.gitHubCliState
}
func (self *StateAccessor) SetGitHubCliState(value types.GitHubCliState) {
self.gui.gitHubCliState = value
}
// we keep track of some stuff from one render to the next to see if certain
// things have changed
type PrevLayout struct {
@@ -377,7 +386,7 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context
gui.g.SetOpenHyperlinkFunc(func(url string, viewname string) error {
if strings.HasPrefix(url, "lazygit-edit:") {
re := regexp.MustCompile(`^lazygit-edit://(.+?)(?::(\d*))?$`)
re := regexp.MustCompile(`^lazygit-edit://(.+?)(?::(\d+))?$`)
matches := re.FindStringSubmatch(url)
if matches == nil {
return fmt.Errorf(gui.Tr.InvalidLazygitEditURL, url)
@@ -581,6 +590,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
Authors: map[string]*models.Author{},
MainBranches: git_commands.NewMainBranches(gui.c.Common, gui.os.Cmd),
HashPool: &utils.StringPool{},
PullRequests: make([]*models.GithubPullRequest, 0),
},
Modes: &types.Modes{
Filtering: filtering.New(startArgs.FilterPath, ""),
@@ -842,25 +852,6 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
g.ErrorHandler = gui.PopupHandler.ErrorHandler
gui.g.ShouldHandleMouseEvent = func(view *gocui.View, key gocui.Key) bool {
if gui.helpers.Confirmation.IsPopupPanelFocused() && gui.currentViewName() != view.Name() &&
!gocui.IsMouseScrollKey(key) {
// we ignore click events on views that aren't popup panels, when a popup panel is focused.
// Unless both the current view and the clicked-on view are either commit message or commit
// description, or a prompt and the suggestions view, because we want to allow switching
// between those two views by clicking.
isCommitMessageOrSuggestionsView := func(viewName string) bool {
return viewName == "commitMessage" || viewName == "commitDescription" ||
viewName == "prompt" || viewName == "suggestions"
}
if !isCommitMessageOrSuggestionsView(gui.currentViewName()) || !isCommitMessageOrSuggestionsView(view.Name()) {
return false
}
}
return true
}
// if the deadlock package wants to report a deadlock, we first need to
// close the gui so that we can actually read what it prints.
deadlock.Opts.LogBuf = utils.NewOnceWriter(os.Stderr, func() {
+22
View File
@@ -499,7 +499,29 @@ func (gui *Gui) SetKeybinding(binding *types.Binding) error {
return gui.g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, gui.wrappedHandler(handler))
}
// warning: mutates the binding
func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error {
baseHandler := binding.Handler
newHandler := func(opts gocui.ViewMouseBindingOpts) error {
if gui.helpers.Confirmation.IsPopupPanelFocused() && gui.currentViewName() != binding.ViewName &&
!gocui.IsMouseScrollKey(opts.Key) {
// we ignore click events on views that aren't popup panels, when a popup panel is focused.
// Unless both the current view and the clicked-on view are either commit message or commit
// description, or a prompt and the suggestions view, because we want to allow switching
// between those two views by clicking.
isCommitMessageOrSuggestionsView := func(viewName string) bool {
return viewName == "commitMessage" || viewName == "commitDescription" ||
viewName == "prompt" || viewName == "suggestions"
}
if !isCommitMessageOrSuggestionsView(gui.currentViewName()) || !isCommitMessageOrSuggestionsView(binding.ViewName) {
return nil
}
}
return baseHandler(opts)
}
binding.Handler = newHandler
return gui.g.SetViewClickBinding(binding)
}
+51 -37
View File
@@ -28,6 +28,8 @@ var colorPatterns *colorMatcher
func GetBranchListDisplayStrings(
branches []*models.Branch,
getItemOperation func(item types.HasUrn) types.ItemOperation,
pullRequests []*models.GithubPullRequest,
remotes []*models.Remote,
fullDescription bool,
diffName string,
viewWidth int,
@@ -35,9 +37,15 @@ func GetBranchListDisplayStrings(
userConfig *config.UserConfig,
worktrees []*models.Worktree,
) [][]string {
prs := git_commands.GenerateGithubPullRequestMap(
pullRequests,
branches,
remotes,
)
return lo.Map(branches, func(branch *models.Branch, _ int) []string {
diffed := branch.Name == diffName
return getBranchDisplayStrings(branch, getItemOperation(branch), fullDescription, diffed, viewWidth, tr, userConfig, worktrees, time.Now())
return getBranchDisplayStrings(branch, getItemOperation(branch), fullDescription, diffed, viewWidth, tr, userConfig, worktrees, time.Now(), prs)
})
}
@@ -52,11 +60,13 @@ func getBranchDisplayStrings(
userConfig *config.UserConfig,
worktrees []*models.Worktree,
now time.Time,
prs map[string]*models.GithubPullRequest,
) []string {
checkedOutByWorkTree := git_commands.CheckedOutByOtherWorktree(b, worktrees)
showCommitHash := fullDescription || userConfig.Gui.ShowBranchCommitHash
branchStatus := BranchStatus(b, itemOperation, tr, now, userConfig)
divergence := divergenceStr(b, itemOperation, tr, userConfig)
worktreeIcon := lo.Ternary(icons.IsIconEnabled(), icons.LINKED_WORKTREE_ICON, fmt.Sprintf("(%s)", tr.LcWorktree))
// Recency is always three characters, plus one for the space
availableWidth := viewWidth - 4
@@ -71,42 +81,17 @@ func getBranchDisplayStrings(
}
paddingNeededForDivergence := availableWidth
displayName := b.Name
if b.DisplayName != "" {
displayName = b.DisplayName
if checkedOutByWorkTree {
availableWidth -= utils.StringWidth(worktreeIcon) + 1
}
if len(branchStatus) > 0 {
availableWidth -= utils.StringWidth(utils.Decolorise(branchStatus)) + 1
}
worktreeIcon := ""
if checkedOutByWorkTree {
if wt, ok := git_commands.WorktreeForBranch(b, worktrees); ok && wt.Name != b.Name {
if icons.IsIconEnabled() {
worktreeIcon = fmt.Sprintf("(%s %s)", icons.LINKED_WORKTREE_ICON, wt.Name)
} else {
worktreeIcon = fmt.Sprintf("(%s %s)", tr.LcWorktree, wt.Name)
}
// If the worktree name doesn't fit in the available width, omit it
remaining := availableWidth - utils.StringWidth(worktreeIcon) - 1
if remaining < utils.StringWidth(displayName) {
if icons.IsIconEnabled() {
worktreeIcon = icons.LINKED_WORKTREE_ICON
} else {
worktreeIcon = fmt.Sprintf("(%s)", tr.LcWorktree)
}
}
} else {
if icons.IsIconEnabled() {
worktreeIcon = icons.LINKED_WORKTREE_ICON
} else {
worktreeIcon = fmt.Sprintf("(%s)", tr.LcWorktree)
}
}
availableWidth -= utils.StringWidth(worktreeIcon) + 1
displayName := b.Name
if b.DisplayName != "" {
displayName = b.DisplayName
}
nameTextStyle := GetBranchTextStyle(b.Name)
@@ -124,6 +109,7 @@ func getBranchDisplayStrings(
if checkedOutByWorkTree {
coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon))
}
if len(branchStatus) > 0 {
coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus)
}
@@ -134,14 +120,22 @@ func getBranchDisplayStrings(
}
res := make([]string, 0, 6)
res = append(res, recencyColor.Sprint(b.Recency))
if icons.IsIconEnabled() {
res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b)))
}
if showCommitHash {
res = append(res, utils.ShortHash(b.CommitHash))
pr, hasPr := prs[b.Name]
if hasPr {
if icons.IsIconEnabled() {
res = append(res, prColor(pr.State).Sprint(icons.IconForRemoteUrl(pr.Url)))
} else {
res = append(res, prColor(pr.State).Sprint("●"))
}
} else {
if icons.IsIconEnabled() {
res = append(res, style.FgDefault.Sprint(icons.IconForBranch(b)))
} else {
res = append(res, style.FgDefault.Sprint("●"))
}
}
if divergence != "" {
@@ -151,8 +145,13 @@ func getBranchDisplayStrings(
coloredName += style.FgCyan.Sprint(divergence)
}
}
res = append(res, coloredName)
if showCommitHash {
res = append(res, utils.ShortHash(b.CommitHash))
}
if fullDescription {
res = append(
res,
@@ -252,3 +251,18 @@ func SetCustomBranches(customBranchColors map[string]string, isRegex bool) {
isRegex: isRegex,
}
}
func prColor(state string) style.TextStyle {
switch state {
case "OPEN":
return style.FgGreen
case "CLOSED":
return style.FgRed
case "MERGED":
return style.FgMagenta
case "DRAFT":
return style.FgBlackLighter
default:
return style.FgDefault
}
}
+24 -24
View File
@@ -42,7 +42,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "branch_name"},
expected: []string{"1m", "●", "branch_name"},
},
{
branch: &models.Branch{Name: "🍉_special_char", Recency: "1m"},
@@ -52,7 +52,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "🍉_special_char"},
expected: []string{"1m", "●", "🍉_special_char"},
},
{
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
@@ -62,7 +62,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: true,
showDivergenceCfg: "none",
expected: []string{"1m", "branch_name (worktree other-worktree)"},
expected: []string{"1m", "●", "branch_name (worktree)"},
},
{
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
@@ -72,7 +72,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: true,
checkedOutByWorktree: true,
showDivergenceCfg: "none",
expected: []string{"1m", "󰘬", "branch_name (󰌹 other-worktree)"},
expected: []string{"1m", "󰘬", "branch_name 󰌹"},
},
{
branch: &models.Branch{
@@ -88,7 +88,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "branch_name ✓"},
expected: []string{"1m", "●", "branch_name ✓"},
},
{
branch: &models.Branch{
@@ -104,7 +104,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: true,
showDivergenceCfg: "none",
expected: []string{"1m", "branch_name (worktree other-worktree) ↓5↑3"},
expected: []string{"1m", "●", "branch_name (worktree) ↓5↑3"},
},
{
branch: &models.Branch{
@@ -118,7 +118,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "onlyArrow",
expected: []string{"1m", "branch_name ↓"},
expected: []string{"1m", "●", "branch_name ↓"},
},
{
branch: &models.Branch{
@@ -135,7 +135,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "arrowAndNumber",
expected: []string{"1m", "branch_name ✓ ↓2"},
expected: []string{"1m", "●", "branch_name ✓ ↓2"},
},
{
branch: &models.Branch{
@@ -152,7 +152,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "arrowAndNumber",
expected: []string{"1m", "branch_name ↓5↑3 ↓2"},
expected: []string{"1m", "●", "branch_name ↓5↑3 ↓2"},
},
{
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
@@ -162,7 +162,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "branch_name Pushing |"},
expected: []string{"1m", "●", "branch_name Pushing |"},
},
{
branch: &models.Branch{
@@ -181,7 +181,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "12345678", "branch_name ✓", "origin branch_name", "commit title"},
expected: []string{"1m", "●", "branch_name ✓", "12345678", "origin branch_name", "commit title"},
},
// Now tests for how we truncate the branch name when there's not enough room:
@@ -193,7 +193,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "branch_na…"},
expected: []string{"1m", "●", "branch_na…"},
},
{
branch: &models.Branch{Name: "🍉_special_char", Recency: "1m"},
@@ -203,7 +203,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "🍉_special_ch…"},
expected: []string{"1m", "●", "🍉_special_ch…"},
},
{
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
@@ -213,7 +213,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: true,
showDivergenceCfg: "none",
expected: []string{"1m", "bra… (worktree)"},
expected: []string{"1m", "●", "bra… (worktree)"},
},
{
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
@@ -239,7 +239,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "branch_… ✓"},
expected: []string{"1m", "●", "branch_… ✓"},
},
{
branch: &models.Branch{
@@ -256,7 +256,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "arrowAndNumber",
expected: []string{"1m", "branch_n… ↓5↑3 ↓4"},
expected: []string{"1m", "●", "branch_n… ↓5↑3 ↓4"},
},
{
branch: &models.Branch{
@@ -272,7 +272,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: true,
showDivergenceCfg: "none",
expected: []string{"1m", "branch_na… (worktree) ↓5↑3"},
expected: []string{"1m", "●", "branch_na… (worktree) ↓5↑3"},
},
{
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
@@ -282,7 +282,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "branc… Pushing |"},
expected: []string{"1m", "●", "branc… Pushing |"},
},
{
branch: &models.Branch{Name: "abc", Recency: "1m"},
@@ -292,7 +292,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "abc Pushing |"},
expected: []string{"1m", "●", "abc Pushing |"},
},
{
branch: &models.Branch{Name: "ab", Recency: "1m"},
@@ -302,7 +302,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "ab Pushing |"},
expected: []string{"1m", "●", "ab Pushing |"},
},
{
branch: &models.Branch{Name: "a", Recency: "1m"},
@@ -312,7 +312,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "a Pushing |"},
expected: []string{"1m", "●", "a Pushing |"},
},
{
branch: &models.Branch{
@@ -331,7 +331,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
useIcons: false,
checkedOutByWorktree: false,
showDivergenceCfg: "none",
expected: []string{"1m", "12345678", "bran… ✓", "origin branch_name", "commit title"},
expected: []string{"1m", "●", "bran… ✓", "12345678", "origin branch_name", "commit title"},
},
}
@@ -347,11 +347,11 @@ func Test_getBranchDisplayStrings(t *testing.T) {
worktrees := []*models.Worktree{}
if s.checkedOutByWorktree {
worktrees = append(worktrees, &models.Worktree{Branch: s.branch.Name, Name: "other-worktree"})
worktrees = append(worktrees, &models.Worktree{Branch: s.branch.Name})
}
t.Run(fmt.Sprintf("getBranchDisplayStrings_%d", i), func(t *testing.T) {
strings := getBranchDisplayStrings(s.branch, s.itemOperation, s.fullDescription, false, s.viewWidth, c.Tr, c.UserConfig(), worktrees, time.Time{})
strings := getBranchDisplayStrings(s.branch, s.itemOperation, s.fullDescription, false, s.viewWidth, c.Tr, c.UserConfig(), worktrees, time.Time{}, map[string]*models.GithubPullRequest{})
assert.Equal(t, s.expected, strings)
})
}
+9
View File
@@ -79,6 +79,15 @@ func IconForRemote(remote *models.Remote) string {
return DEFAULT_REMOTE_ICON
}
func IconForRemoteUrl(url string) string {
for domain, icon := range remoteIcons {
if strings.Contains(url, domain) {
return icon
}
}
return DEFAULT_REMOTE_ICON
}
func IconForStash(stash *models.StashEntry) string {
return STASH_ICON
}
+3 -15
View File
@@ -6,7 +6,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
@@ -41,23 +40,12 @@ func GetWorktreeDisplayString(tr *i18n.TranslationSet, worktree *models.Worktree
}
name := worktree.Name
if worktree.IsMain {
name += " " + tr.MainWorktree
}
if worktree.IsPathMissing && !icons.IsIconEnabled() {
name += " " + tr.MissingWorktree
}
res = append(res, textStyle.Sprint(name))
var branch string
if worktree.Branch != "" {
branch = style.FgCyan.Sprint(worktree.Branch)
} else if worktree.Head != "" {
branch = style.FgYellow.Sprint("HEAD detached at " + utils.ShortHash(worktree.Head))
}
res = append(res, branch+mainWorktreeLabel(tr, worktree))
return res
}
func mainWorktreeLabel(tr *i18n.TranslationSet, worktree *models.Worktree) string {
if worktree.IsMain {
return style.FgDefault.Sprint(" " + tr.MainWorktree)
}
return ""
}
-111
View File
@@ -1,111 +0,0 @@
package presentation
import (
"testing"
"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/stretchr/testify/assert"
"github.com/xo/terminfo"
)
func Test_GetWorktreeDisplayString(t *testing.T) {
tr := &i18n.TranslationSet{
MainWorktree: "(main worktree)",
MissingWorktree: "(missing)",
}
scenarios := []struct {
testName string
worktree *models.Worktree
expected []string
}{
{
testName: "Current worktree on a branch",
worktree: &models.Worktree{
Name: "my-worktree",
Branch: "my-branch",
IsCurrent: true,
},
expected: []string{" *", "my-worktree", "my-branch"},
},
{
testName: "Non-current worktree on a branch",
worktree: &models.Worktree{
Name: "my-worktree",
Branch: "feature-branch",
},
expected: []string{"", "my-worktree", "feature-branch"},
},
{
testName: "Main worktree on a branch",
worktree: &models.Worktree{
Name: "repo",
Branch: "main",
IsMain: true,
},
expected: []string{"", "repo", "main (main worktree)"},
},
{
testName: "Detached HEAD worktree",
worktree: &models.Worktree{
Name: "my-worktree",
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
},
expected: []string{"", "my-worktree", "HEAD detached at d85cc9d2"},
},
{
testName: "Detached HEAD on main worktree",
worktree: &models.Worktree{
Name: "repo",
IsMain: true,
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
},
expected: []string{"", "repo", "HEAD detached at d85cc9d2 (main worktree)"},
},
{
testName: "Worktree with branch takes precedence over head",
worktree: &models.Worktree{
Name: "my-worktree",
Branch: "my-branch",
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
},
expected: []string{"", "my-worktree", "my-branch"},
},
{
testName: "Missing worktree",
worktree: &models.Worktree{
Name: "my-worktree",
IsPathMissing: true,
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
},
expected: []string{"", "my-worktree (missing)", "HEAD detached at d85cc9d2"},
},
{
testName: "Worktree with no branch and no head",
worktree: &models.Worktree{
Name: "my-worktree",
},
expected: []string{"", "my-worktree", ""},
},
{
testName: "Main worktree with no branch and no head",
worktree: &models.Worktree{
Name: "my-worktree",
IsMain: true,
},
expected: []string{"", "my-worktree", " (main worktree)"},
},
}
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone)
defer color.ForceSetColorLevel(oldColorLevel)
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
result := GetWorktreeDisplayString(tr, s.worktree)
assert.Equal(t, s.expected, result)
})
}
}
+23 -9
View File
@@ -296,6 +296,7 @@ type Model struct {
SubCommits []*models.Commit
Remotes []*models.Remote
Worktrees []*models.Worktree
PullRequests []*models.GithubPullRequest
// FilteredReflogCommits are the ones that appear in the reflog panel.
// When in filtering mode we only include the ones that match the given path
@@ -326,15 +327,16 @@ type Model struct {
}
type Mutexes struct {
RefreshingFilesMutex deadlock.Mutex
RefreshingBranchesMutex deadlock.Mutex
RefreshingStatusMutex deadlock.Mutex
LocalCommitsMutex deadlock.Mutex
SubCommitsMutex deadlock.Mutex
AuthorsMutex deadlock.Mutex
SubprocessMutex deadlock.Mutex
PopupMutex deadlock.Mutex
PtyMutex deadlock.Mutex
RefreshingFilesMutex deadlock.Mutex
RefreshingBranchesMutex deadlock.Mutex
RefreshingStatusMutex deadlock.Mutex
RefreshingPullRequestsMutex deadlock.Mutex
LocalCommitsMutex deadlock.Mutex
SubCommitsMutex deadlock.Mutex
AuthorsMutex deadlock.Mutex
SubprocessMutex deadlock.Mutex
PopupMutex deadlock.Mutex
PtyMutex deadlock.Mutex
}
// A long-running operation associated with an item. For example, we'll show
@@ -373,6 +375,8 @@ type IStateAccessor interface {
GetItemOperation(item HasUrn) ItemOperation
SetItemOperation(item HasUrn, operation ItemOperation)
ClearItemOperation(item HasUrn)
GetGitHubCliState() GitHubCliState
SetGitHubCliState(GitHubCliState)
}
type IRepoStateAccessor interface {
@@ -409,3 +413,13 @@ const (
SCREEN_HALF
SCREEN_FULL
)
// for keeping track of whether our github CLI is installed and on a valid version
type GitHubCliState int
const (
UNKNOWN GitHubCliState = iota
VALID
NOT_INSTALLED
INVALID_VERSION
)
+1
View File
@@ -22,6 +22,7 @@ const (
COMMIT_FILES
// not actually a view. Will refactor this later
BISECT_INFO
PULL_REQUESTS
)
type RefreshMode int
+19 -15
View File
@@ -279,7 +279,6 @@ type TranslationSet struct {
ConfirmQuit string
SwitchRepo string
AllBranchesLogGraph string
AllBranchesLogGraphReverse string
UnsupportedGitService string
CopyPullRequestURL string
NoBranchOnRemote string
@@ -437,12 +436,12 @@ type TranslationSet struct {
CheckoutCommitFileTooltip string
CannotCheckoutWithModifiedFilesErr string
CanOnlyDiscardFromLocalCommits string
CannotDiscardFromMultipleCommits string
CanOnlyRemoveLinesFromLocalCommits string
MustClearPatchBeforeRemovingLines string
Remove string
DiscardOldFileChangeTooltip string
DiscardFileChangesTitle string
DiscardFileChangesPrompt string
DiscardFileChangesPromptResetPatch string
DisabledForGPG string
CreateRepo string
BareRepo string
@@ -609,6 +608,8 @@ type TranslationSet struct {
CyclePagersDisabledReason string
StartSearch string
StartFilter string
SelectRemoteRepository string
LcSelectingRemote string
Keybindings string
KeybindingsLegend string
KeybindingsMenuSectionLocal string
@@ -691,9 +692,8 @@ type TranslationSet struct {
BranchUnknown string
DiscardChangeTitle string
DiscardChangePrompt string
DiscardLinesFromCommitTitle string
DiscardLinesFromCommitPrompt string
DiscardLinesFromCommitPromptWithReset string
RemoveLinesFromCommitTitle string
RemoveLinesFromCommitPrompt string
CreateNewBranchFromCommit string
BuildingPatch string
ViewCommits string
@@ -705,6 +705,8 @@ type TranslationSet struct {
BackToParentRepo string
Enter string
CopySubmoduleNameToClipboard string
MinGhVersionError string
FailedToObtainGhVersionError string
RemoveSubmodule string
RemoveSubmoduleTooltip string
RemoveSubmodulePrompt string
@@ -1390,7 +1392,6 @@ func EnglishTranslationSet() *TranslationSet {
ConfirmQuit: `Are you sure you want to quit?`,
SwitchRepo: `Switch to a recent repo`,
AllBranchesLogGraph: `Show/cycle all branch logs`,
AllBranchesLogGraphReverse: `Show/cycle all branch logs (reverse)`,
UnsupportedGitService: `Unsupported git service`,
CreatePullRequest: `Create pull request`,
CopyPullRequestURL: `Copy pull request URL to clipboard`,
@@ -1554,12 +1555,12 @@ func EnglishTranslationSet() *TranslationSet {
CheckoutCommitFileTooltip: "Checkout file. This replaces the file in your working tree with the version from the selected commit.",
CannotCheckoutWithModifiedFilesErr: "You have local modifications for the file(s) you are trying to check out. You need to stash or discard these first.",
CanOnlyDiscardFromLocalCommits: "Changes can only be discarded from local commits",
CannotDiscardFromMultipleCommits: "Changes cannot be discarded from a multiselection of commits",
CanOnlyRemoveLinesFromLocalCommits: "Lines can only be removed from local commits",
MustClearPatchBeforeRemovingLines: "Clear the current custom patch first before removing lines from the commit",
Remove: "Remove",
DiscardOldFileChangeTooltip: "Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file.",
DiscardFileChangesTitle: "Discard file changes",
DiscardFileChangesPrompt: "Are you sure you want to discard changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.",
DiscardFileChangesPromptResetPatch: "Are you sure you want to discard changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.\n\nNote: This will reset the active custom patch!",
DiscardFileChangesPrompt: "Are you sure you want to remove changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.\nNote: This will also reset any active custom patches.",
DisabledForGPG: "Feature not available for users using GPG.\n\nIf you are using a passphrase agent (e.g. gpg-agent) so that you don't have to type your passphrase when signing, you can enable this feature by adding\n\ngit:\n overrideGpg: true\n\nto your lazygit config file.",
CreateRepo: "Not in a git repository. Create a new git repository? (y/N): ",
BareRepo: "You've attempted to open Lazygit in a bare repo but Lazygit does not yet support bare repos. Open most recent repo? (y/n) ",
@@ -1729,6 +1730,8 @@ func EnglishTranslationSet() *TranslationSet {
CyclePagersDisabledReason: "No other pagers configured",
StartSearch: "Search the current view by text",
StartFilter: "Filter the current view by text",
SelectRemoteRepository: "Select base repository for pull requests",
LcSelectingRemote: "Selecting remote",
KeybindingsLegend: "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
RenameBranch: "Rename branch",
BranchUpstreamOptionsTitle: "Upstream options",
@@ -1784,7 +1787,7 @@ func EnglishTranslationSet() *TranslationSet {
ShowingGitDiff: "Showing output for:",
ShowingDiffForRange: "Showing diff for range",
CommitDiff: "Commit diff",
CopyCommitHashToClipboard: "Copy abbreviated commit hash to clipboard",
CopyCommitHashToClipboard: "Copy commit hash to clipboard",
CommitHash: "Commit hash",
CommitURL: "Commit URL",
PasteCommitMessageFromClipboard: "Paste commit message from clipboard",
@@ -1808,9 +1811,8 @@ func EnglishTranslationSet() *TranslationSet {
BranchUnknown: "Branch unknown",
DiscardChangeTitle: "Discard change",
DiscardChangePrompt: "Are you sure you want to discard this change (git reset)? It is irreversible.\nTo disable this dialogue set the config key of 'gui.skipDiscardChangeWarning' to true",
DiscardLinesFromCommitTitle: "Discard lines from commit",
DiscardLinesFromCommitPrompt: "Are you sure you want to discard the selected lines from this commit?",
DiscardLinesFromCommitPromptWithReset: "Are you sure you want to discard the selected lines from this commit?\n\nNote: This will reset the active custom patch!",
RemoveLinesFromCommitTitle: "Remove lines from commit",
RemoveLinesFromCommitPrompt: "Are you sure you want to remove the selected lines from this commit?",
CreateNewBranchFromCommit: "Create new branch off of commit",
BuildingPatch: "Building patch",
ViewCommits: "View commits",
@@ -1822,6 +1824,8 @@ func EnglishTranslationSet() *TranslationSet {
EnterSubmoduleTooltip: "Enter submodule. After entering the submodule, you can press `{{.escape}}` to escape back to the parent repo.",
BackToParentRepo: "Back to parent repo",
CopySubmoduleNameToClipboard: "Copy submodule name to clipboard",
MinGhVersionError: "GH version must be at least 2.0. Please upgrade your gh version. Alternatively raise an issue at https://github.com/jesseduffield/lazygit/issues for lazygit to be more backwards compatible.",
FailedToObtainGhVersionError: "Failed to obtain gh version. Output from running 'gh --version' was: %s",
RemoveSubmodule: "Remove submodule",
RemoveSubmodulePrompt: "Are you sure you want to remove submodule '%s' and its corresponding directory? This is irreversible.",
RemoveSubmoduleTooltip: "Remove the selected submodule and its corresponding directory.",
@@ -1990,7 +1994,7 @@ func EnglishTranslationSet() *TranslationSet {
CantDeleteMainWorktree: "You cannot remove the main worktree!",
NoWorktreesThisRepo: "No worktrees",
MissingWorktree: "(missing)",
MainWorktree: "(main worktree)",
MainWorktree: "(main)",
NewWorktree: "New worktree",
NewWorktreePath: "New worktree path",
NewWorktreeBase: "New worktree base ref",
+3 -1
View File
@@ -133,7 +133,6 @@
"NewBranchFromStashTooltip": "選択したスタッシュエントリから新しいブランチを作成します。これは、スタッシュエントリが作成されたコミットをgitがチェックアウトし、そのコミットから新しいブランチを作成した後、スタッシュエントリを追加のコミットとして新しいブランチに適用することで機能します。",
"MoveCommitsToNewBranch": "コミットを新しいブランチに移動",
"MoveCommitsToNewBranchFromBaseItem": "ベースブランチから新規ブランチ (%s)",
"CannotMoveCommitsBehindUpstream": "アップストリームブランチに遅れているブランチからコミットを移動することはできません",
"CannotMoveCommitsNoUnpushedCommits": "新しいブランチに移動するプッシュされていないコミットはありません",
"NoBranchesThisRepo": "このリポジトリにはブランチがありません",
"CommitWithoutMessageErr": "コミットメッセージなしでコミットすることはできません",
@@ -390,6 +389,7 @@
"Remove": "削除",
"DiscardOldFileChangeTooltip": "このコミットのこのファイルへの変更を破棄します。これはバックグラウンドで対話的なリベースを実行するため、後のコミットでもこのファイルが変更されている場合、マージコンフリクトが発生する可能性があります。",
"DiscardFileChangesTitle": "ファイルの変更を破棄",
"DiscardFileChangesPrompt": "このコミットから選択したファイルの変更を削除してよろしいですか?\n\nこのアクションはリベースを開始し、これらのファイル変更を元に戻します。後続のコミットがこれらの変更に依存している場合、コンフリクトを解決する必要があるかもしれないことに注意してください。\n注意:これにより、アクティブなカスタムパッチもリセットされます。",
"DisabledForGPG": "GPGを使用しているユーザーには利用できない機能です。\n\nパスフレーズエージェント(gpg-agentなど)を使用して署名時にパスフレーズを入力しなくても済むようにしている場合は、lazygitの設定ファイルに\n\ngit:\n overrideGpg: true\n\nを追加することでこの機能を有効にできます。",
"CreateRepo": "Gitリポジトリがありません。新しいgitリポジトリを作成しますか? (y/N): ",
"BareRepo": "ベアリポジトリでLazygitを開こうとしましたが、Lazygitはまだベアリポジトリをサポートしていません。最近のリポジトリを開いてよろしいですか? (y/n) ",
@@ -596,6 +596,7 @@
"ShowingGitDiff": "出力を表示:",
"ShowingDiffForRange": "範囲の差分を表示",
"CommitDiff": "コミットの差分",
"CopyCommitHashToClipboard": "コミットハッシュをクリップボードにコピー",
"CommitHash": "コミットハッシュ",
"CommitURL": "コミットURL",
"PasteCommitMessageFromClipboard": "クリップボードからコミットメッセージを貼り付け",
@@ -779,6 +780,7 @@
"CantDeleteMainWorktree": "メインのワークツリーは削除できません!",
"NoWorktreesThisRepo": "ワークツリーがありません",
"MissingWorktree": "(見つかりません)",
"MainWorktree": "(メイン)",
"NewWorktree": "新しいワークツリー",
"NewWorktreePath": "新しいワークツリーのパス",
"NewWorktreeBase": "新しいワークツリーのベース参照",
+2
View File
@@ -178,6 +178,7 @@
"CheckoutCommitFileTooltip": "Checkout file",
"DiscardOldFileChangeTooltip": "Discard this commit's changes to this file",
"DiscardFileChangesTitle": "파일 변경 사항 버리기",
"DiscardFileChangesPrompt": "Are you sure you want to discard this commit's changes to this file? If this file was created in this commit, it will be deleted",
"Discard": "View 'discard changes' options",
"Cancel": "취소",
"DiscardAllChanges": "모든 변경사항 버리기",
@@ -242,6 +243,7 @@
"ViewDiffingOptions": "Diff 메뉴 열기",
"OpenCommandLogMenu": "명령어 로그 메뉴 열기",
"CommitDiff": "커밋의 iff",
"CopyCommitHashToClipboard": "커밋 해시를 클립보드에 복사",
"CommitHash": "커밋 해시",
"CommitURL": "커밋 URL",
"CommitMessage": "커밋 메시지",
+2
View File
@@ -151,6 +151,7 @@
"CheckoutCommitFileTooltip": "Bestand uitchecken",
"DiscardOldFileChangeTooltip": "Uitsluit deze commit zijn veranderingen aan dit bestand",
"DiscardFileChangesTitle": "Uitsluit bestand zijn veranderingen",
"DiscardFileChangesPrompt": "Weet je zeker dat je de wijzigingen van deze commit in dit bestand wilt weggooien? Als dit bestand is gecreëerd in deze commit dan zal dit bestand worden verwijdert",
"AutoStashPrompt": "Je moet je veranderingen stashen en poppen om ze over te brengen. Dit automatisch doen? (enter/esc)",
"Discard": "Bekijk 'veranderingen ongedaan maken' opties",
"Cancel": "Annuleren",
@@ -240,6 +241,7 @@
"SwapDiff": "Keer diff richting om",
"ViewDiffingOptions": "Open diff menu",
"ShowingGitDiff": "Laat output zien voor:",
"CopyCommitHashToClipboard": "Kopieer commit hash naar klembord",
"CopyBranchNameToClipboard": "Kopieer branch name naar klembord",
"CopyPathToClipboard": "Kopieer de bestandsnaam naar het klembord",
"CommitPrefixPatternError": "Fout in commitPrefix patroon",
+3
View File
@@ -299,6 +299,7 @@
"Remove": "Usuń",
"DiscardOldFileChangeTooltip": "Odrzuć zmiany w tym pliku z tego commita. Uruchamia interaktywny rebase w tle, więc możesz otrzymać konflikt scalania, jeśli późniejszy commit również zmienia ten plik.",
"DiscardFileChangesTitle": "Odrzuć zmiany w pliku",
"DiscardFileChangesPrompt": "Czy na pewno chcesz usunąć zmiany w wybranym pliku/ach z tego commita?\n\nTa akcja uruchomi rebase, cofając te zmiany w pliku. Pamiętaj, że jeśli późniejsze commity zależą od tych zmian, możesz potrzebować rozwiązać konflikty.\nUwaga: Spowoduje to również zresetowanie wszelkich aktywnych niestandardowych łatek.",
"BareRepo": "Próbujesz otworzyć Lazygit w gołym repozytorium, ale Lazygit jeszcze nie obsługuje gołych repozytoriów. Otworzyć najnowsze repozytorium? (t/n) ",
"InitialBranch": "Nazwa gałęzi? (pozostaw puste dla domyślnej gita): ",
"NoRecentRepositories": "Musisz otworzyć lazygit w repozytorium git. Brak ważnych ostatnich repozytoriów. Wyjście.",
@@ -475,6 +476,7 @@
"OpenCommandLogMenuTooltip": "Pokaż opcje dla dziennika poleceń, np. pokazywanie/ukrywanie dziennika poleceń i skupienie na dzienniku poleceń.",
"ShowingGitDiff": "Pokazuje wynik dla:",
"CommitDiff": "Różnice commita",
"CopyCommitHashToClipboard": "Kopiuj hash commita do schowka",
"CommitHash": "hash commita",
"CommitURL": "URL commita",
"CommitMessage": "Wiadomość commita",
@@ -635,6 +637,7 @@
"CantDeleteMainWorktree": "Nie możesz usunąć głównego drzewa pracy!",
"NoWorktreesThisRepo": "Brak drzew pracy",
"MissingWorktree": "(brakujące)",
"MainWorktree": "(główne)",
"NewWorktree": "Nowe drzewo pracy",
"NewWorktreePath": "Nowa ścieżka drzewa pracy",
"NewWorktreeBase": "Nowa bazowa ref drzewa pracy",
+1
View File
@@ -367,6 +367,7 @@
"Remove": "Remover",
"DiscardOldFileChangeTooltip": "Descartar as alterações desse commit para este arquivo. Isso executa uma rebase interativa em segundo plano, então você pode ter um conflito de merge se um commit posterior também alterar este arquivo.",
"DiscardFileChangesTitle": "Descartar alterações de arquivo",
"DiscardFileChangesPrompt": "Você tem certeza de que deseja remover as alterações do(s) arquivo(s) selecionado(s) deste commit?",
"BareRepo": "Você tentou abrir Lazygit em um repositório puro, mas Lazygit ainda não suporta repositórios vazios. Abrir os repositórios mais recentes? (y/n) ",
"InitialBranch": "Nome da branch? (deixe vazio para o padrão do git): ",
"NoRecentRepositories": "É necessário abrir lazygit em um repositório git. Nenhum repositório recente válido. Saindo do sistema.",
+2 -1
View File
@@ -18,7 +18,6 @@
"CredentialsPassword": "Пароль",
"CredentialsPassphrase": "Введите пароль для SSH ключа",
"CredentialsPIN": "Введите PIN-код для SSH ключа",
"CredentialsToken": "Введите токен для SSH-ключа",
"PassUnameWrong": "Неверный пароль, кодовая фраза и/или имя пользователя",
"Commit": "Сохранить изменения",
"AmendLastCommit": "Правка последнего коммита",
@@ -226,6 +225,7 @@
"CanOnlyDiscardFromLocalCommits": "Изменения можно отменить только из локальных коммитов.",
"DiscardOldFileChangeTooltip": "Отменить изменения коммита в этом файле",
"DiscardFileChangesTitle": "Отменить изменения файла",
"DiscardFileChangesPrompt": "Вы уверены, что хотите удалить изменения в выбранных файлах из этого коммита?\n\nЭто действие запустит перебазирование и отменит изменения в этих файлах. Обратите внимание, что если последующие коммиты зависят от этих изменений, вам, возможно, придется разрешить конфликты.\nПримечание: это также сбросит все активные пользовательские патчи.",
"BareRepo": "Вы пытались открыть Lazygit в пустом репозитории, но Lazygit ещё не поддерживает пустые репозитории. Открыть последний репозиторий? (y/n)",
"InitialBranch": "Название ветки? (оставьте пустым для git по умолчанию):",
"NoRecentRepositories": "Необходимо открыть lazygit в git репозитории. Нет валидных последних репозиториев. Выход.",
@@ -342,6 +342,7 @@
"OpenCommandLogMenu": "Открыть меню журнала команд",
"ShowingGitDiff": "Показывает вывод для:",
"CommitDiff": "Разница коммита",
"CopyCommitHashToClipboard": "Скопировать hash коммита в буфер обмена",
"CommitHash": "hash коммита",
"CommitURL": "URL коммита",
"CommitMessage": "Полное сообщение коммита",
+3
View File
@@ -417,6 +417,7 @@
"Remove": "删除",
"DiscardOldFileChangeTooltip": "放弃对此文件的提交变更",
"DiscardFileChangesTitle": "放弃文件变更",
"DiscardFileChangesPrompt": "您确定要舍弃此提交对该文件的变更吗?如果此文件是在此提交中创建的,它将被删除",
"DisabledForGPG": "使用GPG的用户无法使用此功能。\n\n如果您正在使用密码代理(如gpg-agent)以避免每次签名时输入密码,可以通过在lazygit配置文件中添加\n\ngit:\n overrideGpg: true\n\n来启用此功能。",
"CreateRepo": "不在 git 仓库中。创建一个新的 git 仓库吗?(y/N): ",
"BareRepo": "您已经尝试在空仓库中打开Lazygit,但是Lazygit还不支持空仓库。打开最近的仓库吗?(y / n) ",
@@ -641,6 +642,7 @@
"ShowingGitDiff": "显示输出:",
"ShowingDiffForRange": "显示范围差异",
"CommitDiff": "比较提交差异",
"CopyCommitHashToClipboard": "复制提交哈希到剪贴板",
"CommitHash": "提交的 hash",
"CommitURL": "提交URL",
"PasteCommitMessageFromClipboard": "粘贴提交信息自剪贴板",
@@ -844,6 +846,7 @@
"CantDeleteMainWorktree": "您不能移除主工作树!",
"NoWorktreesThisRepo": "没有工作区",
"MissingWorktree": "(缺失)",
"MainWorktree": "(主要)",
"NewWorktree": "新建工作树",
"NewWorktreePath": "新建工作树路径",
"NewWorktreeBase": "新建工作树基于ref",
+3
View File
@@ -258,6 +258,7 @@
"CommitFilesTitle": "提交檔案",
"CheckoutCommitFileTooltip": "檢出檔案",
"DiscardFileChangesTitle": "捨棄檔案更改",
"DiscardFileChangesPrompt": "是否捨棄此提交?如果這個檔案是在此提交中創建的,它將被刪除",
"BareRepo": "你嘗試在裸版本庫中開啟 Lazygit,但 Lazygit 尚未支援裸版本庫。是否開啟最新版本庫? (y/n) ",
"InitialBranch": "分支名稱?(留空使用 git 的預設值):",
"NoRecentRepositories": "必須在 git 版本庫中開啟 lazygit。沒有有效的最近版本庫。退出。",
@@ -396,6 +397,7 @@
"OpenCommandLogMenu": "開啟命令記錄選單",
"ShowingGitDiff": "顯示輸出:",
"CommitDiff": "提交差異",
"CopyCommitHashToClipboard": "複製提交 hash 到剪貼簿",
"CommitHash": "提交 hash",
"CommitURL": "提交 URL",
"CommitMessage": "提交訊息",
@@ -537,6 +539,7 @@
"CantDeleteMainWorktree": "無法刪除主要工作目錄!",
"NoWorktreesThisRepo": "無工作目錄",
"MissingWorktree": "(失蹤)",
"MainWorktree": "(主要)",
"NewWorktreePath": "工作目錄路徑",
"NewWorktreeBase": "工作目錄來源",
"NewBranchName": "分支名稱",
+1 -1
View File
@@ -20,7 +20,7 @@ var CreateTag = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Branches().
Focus().
Lines(
MatchesRegexp(`\*\s*new-branch`).IsSelected(),
MatchesRegexp(`\*.*new-branch`).IsSelected(),
MatchesRegexp(`master`),
).
SelectNextItem().
@@ -31,7 +31,7 @@ var DetachedHead = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Branches().
Lines(
MatchesRegexp(`\* new-branch`).IsSelected(),
MatchesRegexp(`\*.*new-branch`).IsSelected(),
MatchesRegexp(`master`),
)
+1 -1
View File
@@ -45,7 +45,7 @@ var Checkout = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Branches().
IsFocused().
Lines(
Contains("* (HEAD detached at").IsSelected(),
MatchesRegexp(`\*.*\(HEAD detached at`).IsSelected(),
Contains("branch1"),
Contains("branch2"),
Contains("master"),
@@ -57,7 +57,7 @@ var DiscardOldFileChanges = NewIntegrationTest(NewIntegrationTestArgs{
t.ExpectPopup().Confirmation().
Title(Equals("Discard file changes")).
Content(Equals("Are you sure you want to discard changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.")).
Content(Equals("Are you sure you want to remove changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.\nNote: This will also reset any active custom patches.")).
Confirm()
t.Views().CommitFiles().
@@ -99,7 +99,7 @@ var DiscardOldFileChanges = NewIntegrationTest(NewIntegrationTestArgs{
t.ExpectPopup().Confirmation().
Title(Equals("Discard file changes")).
Content(Equals("Are you sure you want to discard changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.")).
Content(Equals("Are you sure you want to remove changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.\nNote: This will also reset any active custom patches.")).
Confirm()
t.Views().CommitFiles().
@@ -157,7 +157,7 @@ var DiscardOldFileChanges = NewIntegrationTest(NewIntegrationTestArgs{
t.ExpectPopup().Confirmation().
Title(Equals("Discard file changes")).
Content(Equals("Are you sure you want to discard changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.\n\nNote: This will reset the active custom patch!")).
Content(Equals("Are you sure you want to remove changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.\nNote: This will also reset any active custom patches.")).
Confirm()
// "Building patch" will still be in this view if the patch isn't reset properly
@@ -45,7 +45,7 @@ var DiscardSubmoduleChanges = NewIntegrationTest(NewIntegrationTestArgs{
t.ExpectPopup().Confirmation().
Title(Equals("Discard file changes")).
Content(Contains("Are you sure you want to discard changes to the selected file(s) from this commit?")).
Content(Contains("Are you sure you want to remove changes to the selected file(s) from this commit?")).
Confirm()
t.Shell().RunCommand([]string{"git", "submodule", "update"})
@@ -1,32 +0,0 @@
package file
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ExcludeWithoutInfoDir = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Exclude a file when .git/info directory does not exist",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
},
SetupRepo: func(shell *Shell) {
// Remove .git/info directory to reproduce #5302
shell.RunCommand([]string{"rm", "-rf", ".git/info"})
shell.CreateFile("toExclude", "")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Focus().
NavigateToLine(Contains("toExclude")).
Press(keys.Files.IgnoreFile).
Tap(func() {
t.ExpectPopup().Menu().Title(Equals("Ignore or exclude file")).Select(Contains("Add to .git/info/exclude")).Confirm()
// Should succeed without error, creating .git/info/ directory automatically
t.FileSystem().FileContent(".git/info/exclude", Contains("/toExclude"))
})
},
})
@@ -28,25 +28,25 @@ var FilterCommitFilesToggleDirectory = NewIntegrationTest(NewIntegrationTestArgs
t.Views().CommitFiles().
IsFocused().
Lines(
Equals("▼ dir1").IsSelected(),
Equals(" A apple-grape"),
Equals(" A apple-orange"),
Equals(" A grape-orange"),
Contains("dir1").IsSelected(),
Contains("apple-grape"),
Contains("apple-orange"),
Contains("grape-orange"),
).
// Filter to show only "apple" files (staying in tree view)
FilterOrSearch("apple").
Lines(
// first item is always selected after filtering
Equals("▼ dir1").IsSelected(),
Equals(" A apple-grape"),
Equals(" A apple-orange"),
Contains("dir1").IsSelected(),
Contains("apple-grape"),
Contains("apple-orange"),
).
// dir1 is already selected; toggle for patch
PressPrimaryAction().
Lines(
Equals("▼ dir1").IsSelected(),
Equals(" ● apple-grape"),
Equals(" ● apple-orange"),
Contains("dir1").IsSelected(),
Contains("● apple-grape"),
Contains("● apple-orange"),
)
t.Views().Information().Content(Contains("Building patch"))
@@ -20,18 +20,18 @@ var FilterFilesStageAll = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Files().
Focus().
Lines(
Equals("▼ dir1").IsSelected(),
Equals(" ?? apple-grape"),
Equals(" ?? apple-orange"),
Equals(" ?? grape-orange"),
Contains("dir1").IsSelected(),
Contains("apple-grape"),
Contains("apple-orange"),
Contains("grape-orange"),
).
// Filter to show only "apple" files
FilterOrSearch("apple").
Lines(
// first item is always selected after filtering
Equals("▼ dir1").IsSelected(),
Equals(" ?? apple-grape"),
Equals(" ?? apple-orange"),
Contains("dir1").IsSelected(),
Contains("apple-grape"),
Contains("apple-orange"),
).
// Stage all visible files
Press(keys.Files.ToggleStagedAll).
@@ -41,10 +41,10 @@ var FilterFilesStageAll = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Files().
IsFocused().
Lines(
Equals("▼ dir1").IsSelected(),
Equals(" A apple-grape"),
Equals(" A apple-orange"),
Equals(" ?? grape-orange"),
Contains("dir1").IsSelected(),
Contains("A apple-grape"),
Contains("A apple-orange"),
Contains("?? grape-orange"),
)
},
})
@@ -20,18 +20,18 @@ var FilterFilesStageDirectory = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Files().
Focus().
Lines(
Equals("▼ dir1").IsSelected(),
Equals(" ?? apple-grape"),
Equals(" ?? apple-orange"),
Equals(" ?? grape-orange"),
Contains("dir1").IsSelected(),
Contains("apple-grape"),
Contains("apple-orange"),
Contains("grape-orange"),
).
// Filter to show only "apple" files
FilterOrSearch("apple").
Lines(
// first item is always selected after filtering
Equals("▼ dir1").IsSelected(),
Equals(" ?? apple-grape"),
Equals(" ?? apple-orange"),
Contains("dir1").IsSelected(),
Contains("apple-grape"),
Contains("apple-orange"),
).
// dir1 is already selected; stage it
PressPrimaryAction().
@@ -41,10 +41,10 @@ var FilterFilesStageDirectory = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Files().
IsFocused().
Lines(
Equals("▼ dir1"),
Equals(" A apple-grape"),
Equals(" A apple-orange"),
Equals(" ?? grape-orange"),
Contains("dir1"),
Contains("A apple-grape"),
Contains("A apple-orange"),
Contains("?? grape-orange"),
)
},
})
@@ -1,63 +0,0 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterPreservesSelectionOnModelChange = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that when a filter is active and the model changes, the selection is preserved",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("first commit")
shell.NewBranch("branch-alpha")
shell.NewBranch("branch-beta")
shell.NewBranch("branch-gamma")
shell.NewBranch("checked-out-branch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("checked-out-branch").IsSelected(),
Contains("branch-alpha"),
Contains("branch-beta"),
Contains("branch-gamma"),
Contains("master"),
).
FilterOrSearch("branch-").
Lines(
Contains("branch-alpha").IsSelected(),
Contains("branch-beta"),
Contains("branch-gamma"),
).
// Move cursor to a non-zero position
SelectNextItem().
SelectNextItem().
Lines(
Contains("branch-alpha"),
Contains("branch-beta"),
Contains("branch-gamma").IsSelected(),
)
// Trigger a model update while staying on the Branches view.
// Using a shell command that creates a new branch sorting after
// branch-gamma, so the selection index still points to the same item.
t.GlobalPress(keys.Universal.ExecuteShellCommand)
t.ExpectPopup().Prompt().
Title(Equals("Shell command:")).
Type("git branch branch-zeta").
Confirm()
// Verify that the selection is still on branch-gamma (not reset to 0)
t.Views().Branches().
Lines(
Contains("branch-alpha"),
Contains("branch-beta"),
Contains("branch-gamma").IsSelected(),
Contains("branch-zeta"),
)
},
})
@@ -70,7 +70,7 @@ var NestedFilter = NewIntegrationTest(NewIntegrationTestArgs{
).
FilterOrSearch("grape").
Lines(
Equals("A grape").IsSelected(),
Contains("grape").IsSelected(),
).
PressEnter()
@@ -88,7 +88,7 @@ var NestedFilter = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().CommitFiles().
IsFocused().
Lines(
Equals("A grape").IsSelected(),
Contains("grape").IsSelected(),
).
Tap(func() {
t.Views().Search().IsVisible().Content(Contains("matches for 'grape'"))
@@ -75,7 +75,7 @@ var NestedFilterTransient = NewIntegrationTest(NewIntegrationTestArgs{
).
FilterOrSearch("two").
Lines(
Equals("A file-two").IsSelected(),
Contains("file-two").IsSelected(),
)
t.Views().Branches().
@@ -5,8 +5,8 @@ import (
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DiscardLinesFromCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Discard specific lines from a commit using the 'd' shortcut in the patch building view",
var RemoveLinesFromCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Remove specific lines from a commit using the 'd' shortcut in the patch building view",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
@@ -42,8 +42,8 @@ var DiscardLinesFromCommit = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Universal.Remove)
t.ExpectPopup().Confirmation().
Title(Equals("Discard lines from commit")).
Content(Equals("Are you sure you want to discard the selected lines from this commit?")).
Title(Equals("Remove lines from commit")).
Content(Equals("Are you sure you want to remove the selected lines from this commit?")).
Confirm()
// After the rebase, we should be back at the commit files view
@@ -10,11 +10,7 @@ var LogCmdStatusPanelAllBranchesLog = NewIntegrationTest(NewIntegrationTestArgs{
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.AllBranchesLogCmds = []string{
`echo "view1"`,
`echo "view2"`,
`echo "view3"`,
}
config.GetUserConfig().Git.AllBranchesLogCmds = []string{`echo "view1"`, `echo "view2"`}
config.GetUserConfig().Gui.StatusPanelView = "allBranchesLog"
},
SetupRepo: func(shell *Shell) {},
@@ -29,30 +25,14 @@ var LogCmdStatusPanelAllBranchesLog = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Status().
Focus()
t.Views().Main().Content(Contains("view1"))
t.Views().Main().Content(Contains("view1").DoesNotContain("view2"))
t.Views().Status().
Press(keys.Status.AllBranchesLogGraph)
t.Views().Main().Content(Contains("view2"))
t.Views().Main().Content(Contains("view2").DoesNotContain("view1"))
t.Views().Status().
Press(keys.Status.AllBranchesLogGraph)
t.Views().Main().Content(Contains("view3"))
t.Views().Status().
Press(keys.Status.AllBranchesLogGraph)
t.Views().Main().Content(Contains("view1"))
t.Views().Status().
Press(keys.Status.AllBranchesLogGraphReverse)
t.Views().Main().Content(Contains("view3"))
t.Views().Status().
Press(keys.Status.AllBranchesLogGraphReverse)
t.Views().Main().Content(Contains("view2"))
t.Views().Status().
Press(keys.Status.AllBranchesLogGraphReverse)
t.Views().Main().Content(Contains("view1"))
t.Views().Main().Content(Contains("view1").DoesNotContain("view2"))
},
})
@@ -38,7 +38,7 @@ var FetchAndAutoForwardBranchesAllBranchesCheckedOutInOtherWorktree = NewIntegra
Contains("checked-out").IsSelected(),
Contains("diverged ↓2↑1"),
Contains("feature ↓2").DoesNotContain("↑"),
Contains("master (worktree linked-worktree) ↓1").DoesNotContain("↑"),
Contains("master (worktree) ↓1").DoesNotContain("↑"),
)
t.Views().Files().
@@ -51,7 +51,7 @@ var FetchAndAutoForwardBranchesAllBranchesCheckedOutInOtherWorktree = NewIntegra
Contains("checked-out").IsSelected(),
Contains("diverged ↓2↑1"),
Contains("feature ✓"),
Contains("master (worktree linked-worktree) ↓1"),
Contains("master (worktree) ↓1"),
)
},
})
@@ -34,14 +34,14 @@ var FetchWhenSortedByDate = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Branches().
Lines(
Contains("* branch1").IsSelected(),
MatchesRegexp(`\*.*branch1`).IsSelected(),
Contains("branch2"),
Contains("master ↓1"),
).
NavigateToLine(Contains("master")).
Press(keys.Branches.FetchRemote).
Lines(
Contains("* branch1"),
MatchesRegexp(`\*.*branch1`),
Contains("master").IsSelected(),
Contains("branch2"),
)
+1 -3
View File
@@ -221,7 +221,6 @@ var tests = []*components.IntegrationTest{
file.DiscardUnstagedRangeSelect,
file.DiscardVariousChanges,
file.DiscardVariousChangesRangeSelect,
file.ExcludeWithoutInfoDir,
file.Gitignore,
file.GitignoreSpecialCharacters,
file.RememberCommitMessageAfterFail,
@@ -242,7 +241,6 @@ var tests = []*components.IntegrationTest{
filter_and_search.FilterMenuByKeybinding,
filter_and_search.FilterMenuCancelFilterWithEscape,
filter_and_search.FilterMenuWithNoKeybindings,
filter_and_search.FilterPreservesSelectionOnModelChange,
filter_and_search.FilterRemoteBranches,
filter_and_search.FilterRemotes,
filter_and_search.FilterSearchHistory,
@@ -333,7 +331,6 @@ var tests = []*components.IntegrationTest{
patch_building.ApplyInReverseWithConflict,
patch_building.ApplyWithModifiedFileConflict,
patch_building.ApplyWithModifiedFileNoConflict,
patch_building.DiscardLinesFromCommit,
patch_building.EditLineInPatchBuildingPanel,
patch_building.MoveRangeToIndex,
patch_building.MoveToEarlierCommit,
@@ -354,6 +351,7 @@ var tests = []*components.IntegrationTest{
patch_building.MoveToNewCommitInLastCommitOfStackedBranch,
patch_building.MoveToNewCommitPartialHunk,
patch_building.RemoveFromCommit,
patch_building.RemoveLinesFromCommit,
patch_building.RemovePartsOfAddedFile,
patch_building.ResetWithEscape,
patch_building.SelectAllFiles,
@@ -42,7 +42,7 @@ var AddFromBranch = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused().
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree repo)"),
Contains("mybranch (worktree)"),
).
NavigateToLine(Contains("mybranch")).
Press(keys.Universal.Select).
@@ -54,7 +54,7 @@ var AddFromBranch = NewIntegrationTest(NewIntegrationTestArgs{
}).
Lines(
Contains("mybranch").IsSelected(),
Contains("newbranch (worktree linked-worktree)"),
Contains("newbranch (worktree)"),
).
// Confirm the files view is still showing in the files window
Press(keys.Universal.PrevBlock)
@@ -37,7 +37,7 @@ var AddFromBranchDetached = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused().
Lines(
Contains("(no branch)").IsSelected(),
Contains("mybranch (worktree repo)"),
Contains("mybranch (worktree)"),
)
t.Views().Status().
@@ -50,7 +50,7 @@ var AddFromCommit = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused().
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree repo)"),
Contains("mybranch (worktree)"),
)
},
})
@@ -30,7 +30,7 @@ var AssociateBranchBisect = NewIntegrationTest(NewIntegrationTestArgs{
Focus().
Lines(
Contains("mybranch").IsSelected(),
Contains("newbranch (worktree linked-worktree)"),
Contains("newbranch (worktree)"),
)
// start a bisect on the main worktree
@@ -70,7 +70,7 @@ var AssociateBranchBisect = NewIntegrationTest(NewIntegrationTestArgs{
}).
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree repo)"),
Contains("mybranch (worktree)"),
)
// switch back to main worktree
@@ -31,7 +31,7 @@ var AssociateBranchRebase = NewIntegrationTest(NewIntegrationTestArgs{
Focus().
Lines(
Contains("mybranch").IsSelected(),
Contains("newbranch (worktree linked-worktree)"),
Contains("newbranch (worktree)"),
)
// start a rebase on the main worktree
@@ -57,7 +57,7 @@ var AssociateBranchRebase = NewIntegrationTest(NewIntegrationTestArgs{
}).
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree repo)"),
Contains("mybranch (worktree)"),
)
// start a rebase on the linked worktree
@@ -83,7 +83,7 @@ var AssociateBranchRebase = NewIntegrationTest(NewIntegrationTestArgs{
Contains("(no branch").IsSelected(),
Contains("mybranch"),
// even though the linked worktree is rebasing, we still associate it with the branch
Contains("newbranch (worktree linked-worktree)"),
Contains("newbranch (worktree)"),
)
},
})
+5 -5
View File
@@ -29,7 +29,7 @@ var Crud = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Worktrees().
Focus().
Lines(
Contains("(main worktree)"),
Contains("repo (main)"),
).
Press(keys.Universal.New).
Tap(func() {
@@ -55,7 +55,7 @@ var Crud = NewIntegrationTest(NewIntegrationTestArgs{
}).
Lines(
Contains("linked-worktree").IsSelected(),
Contains("(main worktree)"),
Contains("repo (main)"),
).
// confirm we're still in the same view
IsFocused()
@@ -82,7 +82,7 @@ var Crud = NewIntegrationTest(NewIntegrationTestArgs{
Confirm()
}).
// confirm we cannot remove the main worktree
NavigateToLine(Contains("(main worktree)")).
NavigateToLine(Contains("repo (main)")).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Alert().
@@ -93,7 +93,7 @@ var Crud = NewIntegrationTest(NewIntegrationTestArgs{
// switch back to main worktree
Press(keys.Universal.Select).
Lines(
Contains("(main worktree)").IsSelected(),
Contains("repo (main)").IsSelected(),
Contains("linked-worktree"),
)
@@ -114,7 +114,7 @@ var Crud = NewIntegrationTest(NewIntegrationTestArgs{
Confirm()
}).
Lines(
Contains("(main worktree)").IsSelected(),
Contains("repo (main)").IsSelected(),
)
},
})
@@ -28,13 +28,13 @@ var CustomCommand = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Worktrees().
Focus().
Lines(
Contains("(main worktree)"),
Contains("repo (main)"),
Contains("linked-worktree"),
).
NavigateToLine(Contains("linked-worktree")).
Press("d").
Lines(
Contains("(main worktree)"),
Contains("repo (main)"),
)
},
})

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