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

Add worktrees view (#2147)

This commit is contained in:
Jesse Duffield 2023-07-30 18:42:25 +10:00 committed by GitHub
commit 08f0e28e55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
178 changed files with 58193 additions and 4353 deletions

2
.gitignore vendored
View File

@ -39,3 +39,5 @@ test/results/**
oryxBuildBinary oryxBuildBinary
__debug_bin __debug_bin
.worktrees

View File

@ -323,6 +323,7 @@ os:
editAtLine: 'myeditor --line={{line}} {{filename}}' editAtLine: 'myeditor --line={{line}} {{filename}}'
editAtLineAndWait: 'myeditor --block --line={{line}} {{filename}}' editAtLineAndWait: 'myeditor --block --line={{line}} {{filename}}'
editInTerminal: true editInTerminal: true
openDirInEditor: 'myeditor {{dir}}'
``` ```
The `editInTerminal` option is used to decide whether lazygit needs to suspend The `editInTerminal` option is used to decide whether lazygit needs to suspend

View File

@ -74,6 +74,7 @@ The permitted contexts are:
| -------------- | -------------------------------------------------------------------------------------------------------- | | -------------- | -------------------------------------------------------------------------------------------------------- |
| status | The 'Status' tab | | status | The 'Status' tab |
| files | The 'Files' tab | | files | The 'Files' tab |
| worktrees | The 'Worktrees' tab |
| localBranches | The 'Local Branches' tab | | localBranches | The 'Local Branches' tab |
| remotes | The 'Remotes' tab | | remotes | The 'Remotes' tab |
| remoteBranches | The context you get when pressing enter on a remote in the remotes tab | | remoteBranches | The context you get when pressing enter on a remote in the remotes tab |
@ -300,6 +301,7 @@ SelectedRemote
SelectedTag SelectedTag
SelectedStashEntry SelectedStashEntry
SelectedCommitFile SelectedCommitFile
SelectedWorktree
CheckedOutBranch CheckedOutBranch
``` ```

View File

@ -89,6 +89,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>t</kbd>: Revert commit <kbd>t</kbd>: Revert commit
<kbd>T</kbd>: Tag commit <kbd>T</kbd>: Tag commit
<kbd>&lt;c-l&gt;</kbd>: Open log menu <kbd>&lt;c-l&gt;</kbd>: Open log menu
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit <kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser <kbd>o</kbd>: Open commit in browser
@ -154,6 +155,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: View reset options <kbd>g</kbd>: View reset options
<kbd>R</kbd>: Rename branch <kbd>R</kbd>: Rename branch
<kbd>u</kbd>: Set/Unset upstream <kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits <kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -231,6 +233,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard <kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit <kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser <kbd>o</kbd>: Open commit in browser
@ -254,6 +257,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Delete branch <kbd>d</kbd>: Delete branch
<kbd>u</kbd>: Set as upstream of checked-out branch <kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>g</kbd>: View reset options <kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits <kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -276,6 +280,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Drop <kbd>d</kbd>: Drop
<kbd>n</kbd>: New branch <kbd>n</kbd>: New branch
<kbd>r</kbd>: Rename stash <kbd>r</kbd>: Rename stash
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View selected item's files <kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -294,6 +299,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard <kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit <kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser <kbd>o</kbd>: Open commit in browser
@ -311,6 +317,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Copy submodule name to clipboard <kbd>&lt;c-o&gt;</kbd>: Copy submodule name to clipboard
<kbd>&lt;enter&gt;</kbd>: Enter submodule <kbd>&lt;enter&gt;</kbd>: Enter submodule
<kbd>&lt;space&gt;</kbd>: Enter submodule
<kbd>d</kbd>: Remove submodule <kbd>d</kbd>: Remove submodule
<kbd>u</kbd>: Update submodule <kbd>u</kbd>: Update submodule
<kbd>n</kbd>: Add new submodule <kbd>n</kbd>: Add new submodule
@ -328,6 +335,18 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: Push tag <kbd>P</kbd>: Push tag
<kbd>n</kbd>: Create tag <kbd>n</kbd>: Create tag
<kbd>g</kbd>: View reset options <kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits <kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>

View File

@ -52,6 +52,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Drop <kbd>d</kbd>: Drop
<kbd>n</kbd>: 新しいブランチを作成 <kbd>n</kbd>: 新しいブランチを作成
<kbd>r</kbd>: Stashを変更 <kbd>r</kbd>: Stashを変更
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View selected item's files <kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -60,6 +61,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: コミットのSHAをクリップボードにコピー <kbd>&lt;c-o&gt;</kbd>: コミットのSHAをクリップボードにコピー
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: コミットをチェックアウト <kbd>&lt;space&gt;</kbd>: コミットをチェックアウト
<kbd>y</kbd>: コミットの情報をコピー <kbd>y</kbd>: コミットの情報をコピー
<kbd>o</kbd>: ブラウザでコミットを開く <kbd>o</kbd>: ブラウザでコミットを開く
@ -72,6 +74,17 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>/</kbd>: 検索を開始 <kbd>/</kbd>: 検索を開始
</pre> </pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## コミット ## コミット
<pre> <pre>
@ -95,6 +108,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>t</kbd>: コミットをrevert <kbd>t</kbd>: コミットをrevert
<kbd>T</kbd>: タグを作成 <kbd>T</kbd>: タグを作成
<kbd>&lt;c-l&gt;</kbd>: ログメニューを開く <kbd>&lt;c-l&gt;</kbd>: ログメニューを開く
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: コミットをチェックアウト <kbd>&lt;space&gt;</kbd>: コミットをチェックアウト
<kbd>y</kbd>: コミットの情報をコピー <kbd>y</kbd>: コミットの情報をコピー
<kbd>o</kbd>: ブラウザでコミットを開く <kbd>o</kbd>: ブラウザでコミットを開く
@ -133,6 +147,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: サブモジュール名をクリップボードにコピー <kbd>&lt;c-o&gt;</kbd>: サブモジュール名をクリップボードにコピー
<kbd>&lt;enter&gt;</kbd>: サブモジュールを開く <kbd>&lt;enter&gt;</kbd>: サブモジュールを開く
<kbd>&lt;space&gt;</kbd>: サブモジュールを開く
<kbd>d</kbd>: サブモジュールを削除 <kbd>d</kbd>: サブモジュールを削除
<kbd>u</kbd>: サブモジュールを更新 <kbd>u</kbd>: サブモジュールを更新
<kbd>n</kbd>: サブモジュールを新規追加 <kbd>n</kbd>: サブモジュールを新規追加
@ -160,6 +175,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: タグをpush <kbd>P</kbd>: タグをpush
<kbd>n</kbd>: タグを作成 <kbd>n</kbd>: タグを作成
<kbd>g</kbd>: View reset options <kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: コミットを閲覧 <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -211,6 +227,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: View reset options <kbd>g</kbd>: View reset options
<kbd>R</kbd>: ブランチ名を変更 <kbd>R</kbd>: ブランチ名を変更
<kbd>u</kbd>: Set/Unset upstream <kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: コミットを閲覧 <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -305,6 +322,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: ブランチを削除 <kbd>d</kbd>: ブランチを削除
<kbd>u</kbd>: Set as upstream of checked-out branch <kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>g</kbd>: View reset options <kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: コミットを閲覧 <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -313,6 +331,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: コミットのSHAをクリップボードにコピー <kbd>&lt;c-o&gt;</kbd>: コミットのSHAをクリップボードにコピー
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: コミットをチェックアウト <kbd>&lt;space&gt;</kbd>: コミットをチェックアウト
<kbd>y</kbd>: コミットの情報をコピー <kbd>y</kbd>: コミットの情報をコピー
<kbd>o</kbd>: ブラウザでコミットを開く <kbd>o</kbd>: ブラウザでコミットを開く

View File

@ -48,6 +48,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: 커밋 SHA를 클립보드에 복사 <kbd>&lt;c-o&gt;</kbd>: 커밋 SHA를 클립보드에 복사
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 커밋을 체크아웃 <kbd>&lt;space&gt;</kbd>: 커밋을 체크아웃
<kbd>y</kbd>: 커밋 attribute 복사 <kbd>y</kbd>: 커밋 attribute 복사
<kbd>o</kbd>: 브라우저에서 커밋 열기 <kbd>o</kbd>: 브라우저에서 커밋 열기
@ -68,6 +69,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Drop <kbd>d</kbd>: Drop
<kbd>n</kbd>: 새 브랜치 생성 <kbd>n</kbd>: 새 브랜치 생성
<kbd>r</kbd>: Rename stash <kbd>r</kbd>: Rename stash
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View selected item's files <kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -76,6 +78,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: 커밋 SHA를 클립보드에 복사 <kbd>&lt;c-o&gt;</kbd>: 커밋 SHA를 클립보드에 복사
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 커밋을 체크아웃 <kbd>&lt;space&gt;</kbd>: 커밋을 체크아웃
<kbd>y</kbd>: 커밋 attribute 복사 <kbd>y</kbd>: 커밋 attribute 복사
<kbd>o</kbd>: 브라우저에서 커밋 열기 <kbd>o</kbd>: 브라우저에서 커밋 열기
@ -88,6 +91,17 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>/</kbd>: 검색 시작 <kbd>/</kbd>: 검색 시작
</pre> </pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## 메뉴 ## 메뉴
<pre> <pre>
@ -177,6 +191,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: View reset options <kbd>g</kbd>: View reset options
<kbd>R</kbd>: 브랜치 이름 변경 <kbd>R</kbd>: 브랜치 이름 변경
<kbd>u</kbd>: Set/Unset upstream <kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 커밋 보기 <kbd>&lt;enter&gt;</kbd>: 커밋 보기
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -196,6 +211,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: 서브모듈 이름을 클립보드에 복사 <kbd>&lt;c-o&gt;</kbd>: 서브모듈 이름을 클립보드에 복사
<kbd>&lt;enter&gt;</kbd>: 서브모듈 열기 <kbd>&lt;enter&gt;</kbd>: 서브모듈 열기
<kbd>&lt;space&gt;</kbd>: 서브모듈 열기
<kbd>d</kbd>: 서브모듈 삭제 <kbd>d</kbd>: 서브모듈 삭제
<kbd>u</kbd>: 서브모듈 업데이트 <kbd>u</kbd>: 서브모듈 업데이트
<kbd>n</kbd>: 새로운 서브모듈 추가 <kbd>n</kbd>: 새로운 서브모듈 추가
@ -226,6 +242,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: 브랜치 삭제 <kbd>d</kbd>: 브랜치 삭제
<kbd>u</kbd>: Set as upstream of checked-out branch <kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>g</kbd>: View reset options <kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 커밋 보기 <kbd>&lt;enter&gt;</kbd>: 커밋 보기
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -253,6 +270,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>t</kbd>: 커밋 되돌리기 <kbd>t</kbd>: 커밋 되돌리기
<kbd>T</kbd>: Tag commit <kbd>T</kbd>: Tag commit
<kbd>&lt;c-l&gt;</kbd>: 로그 메뉴 열기 <kbd>&lt;c-l&gt;</kbd>: 로그 메뉴 열기
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 커밋을 체크아웃 <kbd>&lt;space&gt;</kbd>: 커밋을 체크아웃
<kbd>y</kbd>: 커밋 attribute 복사 <kbd>y</kbd>: 커밋 attribute 복사
<kbd>o</kbd>: 브라우저에서 커밋 열기 <kbd>o</kbd>: 브라우저에서 커밋 열기
@ -294,6 +312,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: 태그를 push <kbd>P</kbd>: 태그를 push
<kbd>n</kbd>: 태그를 생성 <kbd>n</kbd>: 태그를 생성
<kbd>g</kbd>: View reset options <kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 커밋 보기 <kbd>&lt;enter&gt;</kbd>: 커밋 보기
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>

View File

@ -98,6 +98,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: Bekijk reset opties <kbd>g</kbd>: Bekijk reset opties
<kbd>R</kbd>: Hernoem branch <kbd>R</kbd>: Hernoem branch
<kbd>u</kbd>: Set/Unset upstream <kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Bekijk commits <kbd>&lt;enter&gt;</kbd>: Bekijk commits
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -147,6 +148,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>t</kbd>: Commit ongedaan maken <kbd>t</kbd>: Commit ongedaan maken
<kbd>T</kbd>: Tag commit <kbd>T</kbd>: Tag commit
<kbd>&lt;c-l&gt;</kbd>: Open log menu <kbd>&lt;c-l&gt;</kbd>: Open log menu
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit <kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser <kbd>o</kbd>: Open commit in browser
@ -209,6 +211,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Kopieer commit SHA naar klembord <kbd>&lt;c-o&gt;</kbd>: Kopieer commit SHA naar klembord
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit <kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser <kbd>o</kbd>: Open commit in browser
@ -232,6 +235,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Verwijder branch <kbd>d</kbd>: Verwijder branch
<kbd>u</kbd>: Stel in als upstream van uitgecheckte branch <kbd>u</kbd>: Stel in als upstream van uitgecheckte branch
<kbd>g</kbd>: Bekijk reset opties <kbd>g</kbd>: Bekijk reset opties
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Bekijk commits <kbd>&lt;enter&gt;</kbd>: Bekijk commits
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -276,6 +280,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Laten vallen <kbd>d</kbd>: Laten vallen
<kbd>n</kbd>: Nieuwe branch <kbd>n</kbd>: Nieuwe branch
<kbd>r</kbd>: Rename stash <kbd>r</kbd>: Rename stash
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Bekijk gecommite bestanden <kbd>&lt;enter&gt;</kbd>: Bekijk gecommite bestanden
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -294,6 +299,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Kopieer commit SHA naar klembord <kbd>&lt;c-o&gt;</kbd>: Kopieer commit SHA naar klembord
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit <kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser <kbd>o</kbd>: Open commit in browser
@ -311,6 +317,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Kopieer submodule naam naar klembord <kbd>&lt;c-o&gt;</kbd>: Kopieer submodule naam naar klembord
<kbd>&lt;enter&gt;</kbd>: Enter submodule <kbd>&lt;enter&gt;</kbd>: Enter submodule
<kbd>&lt;space&gt;</kbd>: Enter submodule
<kbd>d</kbd>: Remove submodule <kbd>d</kbd>: Remove submodule
<kbd>u</kbd>: Update submodule <kbd>u</kbd>: Update submodule
<kbd>n</kbd>: Voeg nieuwe submodule toe <kbd>n</kbd>: Voeg nieuwe submodule toe
@ -328,6 +335,18 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: Push tag <kbd>P</kbd>: Push tag
<kbd>n</kbd>: Creëer tag <kbd>n</kbd>: Creëer tag
<kbd>g</kbd>: Bekijk reset opties <kbd>g</kbd>: Bekijk reset opties
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Bekijk commits <kbd>&lt;enter&gt;</kbd>: Bekijk commits
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>

View File

@ -74,6 +74,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>t</kbd>: Odwróć commit <kbd>t</kbd>: Odwróć commit
<kbd>T</kbd>: Tag commit <kbd>T</kbd>: Tag commit
<kbd>&lt;c-l&gt;</kbd>: Open log menu <kbd>&lt;c-l&gt;</kbd>: Open log menu
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit <kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser <kbd>o</kbd>: Open commit in browser
@ -112,6 +113,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: Wyświetl opcje resetu <kbd>g</kbd>: Wyświetl opcje resetu
<kbd>R</kbd>: Rename branch <kbd>R</kbd>: Rename branch
<kbd>u</kbd>: Set/Unset upstream <kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits <kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -208,6 +210,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard <kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit <kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser <kbd>o</kbd>: Open commit in browser
@ -231,6 +234,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Usuń gałąź <kbd>d</kbd>: Usuń gałąź
<kbd>u</kbd>: Set as upstream of checked-out branch <kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>g</kbd>: Wyświetl opcje resetu <kbd>g</kbd>: Wyświetl opcje resetu
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits <kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -269,6 +273,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Porzuć <kbd>d</kbd>: Porzuć
<kbd>n</kbd>: Nowa gałąź <kbd>n</kbd>: Nowa gałąź
<kbd>r</kbd>: Rename stash <kbd>r</kbd>: Rename stash
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita <kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -287,6 +292,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard <kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit <kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser <kbd>o</kbd>: Open commit in browser
@ -304,6 +310,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Copy submodule name to clipboard <kbd>&lt;c-o&gt;</kbd>: Copy submodule name to clipboard
<kbd>&lt;enter&gt;</kbd>: Enter submodule <kbd>&lt;enter&gt;</kbd>: Enter submodule
<kbd>&lt;space&gt;</kbd>: Enter submodule
<kbd>d</kbd>: Remove submodule <kbd>d</kbd>: Remove submodule
<kbd>u</kbd>: Update submodule <kbd>u</kbd>: Update submodule
<kbd>n</kbd>: Add new submodule <kbd>n</kbd>: Add new submodule
@ -321,10 +328,22 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: Push tag <kbd>P</kbd>: Push tag
<kbd>n</kbd>: Create tag <kbd>n</kbd>: Create tag
<kbd>g</kbd>: Wyświetl opcje resetu <kbd>g</kbd>: Wyświetl opcje resetu
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits <kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## Zwykłe ## Zwykłe
<pre> <pre>

View File

@ -44,6 +44,17 @@ _Связки клавиш_
<kbd>[</kbd>: Предыдущая вкладка <kbd>[</kbd>: Предыдущая вкладка
</pre> </pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## Главная панель (Индексирование) ## Главная панель (Индексирование)
<pre> <pre>
@ -109,6 +120,7 @@ _Связки клавиш_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Скопировать SHA коммита в буфер обмена <kbd>&lt;c-o&gt;</kbd>: Скопировать SHA коммита в буфер обмена
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Переключить коммит <kbd>&lt;space&gt;</kbd>: Переключить коммит
<kbd>y</kbd>: Скопировать атрибут коммита <kbd>y</kbd>: Скопировать атрибут коммита
<kbd>o</kbd>: Открыть коммит в браузере <kbd>o</kbd>: Открыть коммит в браузере
@ -144,6 +156,7 @@ _Связки клавиш_
<kbd>t</kbd>: Отменить коммит <kbd>t</kbd>: Отменить коммит
<kbd>T</kbd>: Пометить коммит тегом <kbd>T</kbd>: Пометить коммит тегом
<kbd>&lt;c-l&gt;</kbd>: Открыть меню журнала <kbd>&lt;c-l&gt;</kbd>: Открыть меню журнала
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Переключить коммит <kbd>&lt;space&gt;</kbd>: Переключить коммит
<kbd>y</kbd>: Скопировать атрибут коммита <kbd>y</kbd>: Скопировать атрибут коммита
<kbd>o</kbd>: Открыть коммит в браузере <kbd>o</kbd>: Открыть коммит в браузере
@ -175,6 +188,7 @@ _Связки клавиш_
<kbd>g</kbd>: Просмотреть параметры сброса <kbd>g</kbd>: Просмотреть параметры сброса
<kbd>R</kbd>: Переименовать ветку <kbd>R</kbd>: Переименовать ветку
<kbd>u</kbd>: Установить/убрать upstream-ветку <kbd>u</kbd>: Установить/убрать upstream-ветку
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты <kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -198,6 +212,7 @@ _Связки клавиш_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Скопировать SHA коммита в буфер обмена <kbd>&lt;c-o&gt;</kbd>: Скопировать SHA коммита в буфер обмена
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Переключить коммит <kbd>&lt;space&gt;</kbd>: Переключить коммит
<kbd>y</kbd>: Скопировать атрибут коммита <kbd>y</kbd>: Скопировать атрибут коммита
<kbd>o</kbd>: Открыть коммит в браузере <kbd>o</kbd>: Открыть коммит в браузере
@ -215,6 +230,7 @@ _Связки клавиш_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: Скопировать название подмодуля в буфер обмена <kbd>&lt;c-o&gt;</kbd>: Скопировать название подмодуля в буфер обмена
<kbd>&lt;enter&gt;</kbd>: Ввести подмодуль <kbd>&lt;enter&gt;</kbd>: Ввести подмодуль
<kbd>&lt;space&gt;</kbd>: Ввести подмодуль
<kbd>d</kbd>: Удалить подмодуль <kbd>d</kbd>: Удалить подмодуль
<kbd>u</kbd>: Обновить подмодуль <kbd>u</kbd>: Обновить подмодуль
<kbd>n</kbd>: Добавить новый подмодуль <kbd>n</kbd>: Добавить новый подмодуль
@ -264,6 +280,7 @@ _Связки клавиш_
<kbd>P</kbd>: Отправить тег <kbd>P</kbd>: Отправить тег
<kbd>n</kbd>: Создать тег <kbd>n</kbd>: Создать тег
<kbd>g</kbd>: Просмотреть параметры сброса <kbd>g</kbd>: Просмотреть параметры сброса
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты <kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -279,6 +296,7 @@ _Связки клавиш_
<kbd>d</kbd>: Удалить ветку <kbd>d</kbd>: Удалить ветку
<kbd>u</kbd>: Установить как upstream-ветку переключённую ветку <kbd>u</kbd>: Установить как upstream-ветку переключённую ветку
<kbd>g</kbd>: Просмотреть параметры сброса <kbd>g</kbd>: Просмотреть параметры сброса
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты <kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -328,6 +346,7 @@ _Связки клавиш_
<kbd>d</kbd>: Удалить припрятанные изменения из хранилища <kbd>d</kbd>: Удалить припрятанные изменения из хранилища
<kbd>n</kbd>: Новая ветка <kbd>n</kbd>: Новая ветка
<kbd>r</kbd>: Переименовать хранилище <kbd>r</kbd>: Переименовать хранилище
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Просмотреть файлы выбранного элемента <kbd>&lt;enter&gt;</kbd>: Просмотреть файлы выбранного элемента
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>

View File

@ -48,6 +48,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: 将提交的 SHA 复制到剪贴板 <kbd>&lt;c-o&gt;</kbd>: 将提交的 SHA 复制到剪贴板
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 检出提交 <kbd>&lt;space&gt;</kbd>: 检出提交
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: 在浏览器中打开提交 <kbd>o</kbd>: 在浏览器中打开提交
@ -60,6 +61,17 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## 分支页面 ## 分支页面
<pre> <pre>
@ -80,6 +92,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: 查看重置选项 <kbd>g</kbd>: 查看重置选项
<kbd>R</kbd>: 重命名分支 <kbd>R</kbd>: 重命名分支
<kbd>u</kbd>: Set/Unset upstream <kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 查看提交 <kbd>&lt;enter&gt;</kbd>: 查看提交
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -88,6 +101,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: 将提交的 SHA 复制到剪贴板 <kbd>&lt;c-o&gt;</kbd>: 将提交的 SHA 复制到剪贴板
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 检出提交 <kbd>&lt;space&gt;</kbd>: 检出提交
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: 在浏览器中打开提交 <kbd>o</kbd>: 在浏览器中打开提交
@ -105,6 +119,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: 将子模块名称复制到剪贴板 <kbd>&lt;c-o&gt;</kbd>: 将子模块名称复制到剪贴板
<kbd>&lt;enter&gt;</kbd>: 输入子模块 <kbd>&lt;enter&gt;</kbd>: 输入子模块
<kbd>&lt;space&gt;</kbd>: 输入子模块
<kbd>d</kbd>: 删除子模块 <kbd>d</kbd>: 删除子模块
<kbd>u</kbd>: 更新子模块 <kbd>u</kbd>: 更新子模块
<kbd>n</kbd>: 添加新的子模块 <kbd>n</kbd>: 添加新的子模块
@ -137,6 +152,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>t</kbd>: 还原提交 <kbd>t</kbd>: 还原提交
<kbd>T</kbd>: 标签提交 <kbd>T</kbd>: 标签提交
<kbd>&lt;c-l&gt;</kbd>: 打开日志菜单 <kbd>&lt;c-l&gt;</kbd>: 打开日志菜单
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 检出提交 <kbd>&lt;space&gt;</kbd>: 检出提交
<kbd>y</kbd>: Copy commit attribute <kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: 在浏览器中打开提交 <kbd>o</kbd>: 在浏览器中打开提交
@ -221,6 +237,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: 推送标签 <kbd>P</kbd>: 推送标签
<kbd>n</kbd>: 创建标签 <kbd>n</kbd>: 创建标签
<kbd>g</kbd>: 查看重置选项 <kbd>g</kbd>: 查看重置选项
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 查看提交 <kbd>&lt;enter&gt;</kbd>: 查看提交
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -303,6 +320,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: 删除 <kbd>d</kbd>: 删除
<kbd>n</kbd>: 新分支 <kbd>n</kbd>: 新分支
<kbd>r</kbd>: Rename stash <kbd>r</kbd>: Rename stash
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 查看提交的文件 <kbd>&lt;enter&gt;</kbd>: 查看提交的文件
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -318,6 +336,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: 删除分支 <kbd>d</kbd>: 删除分支
<kbd>u</kbd>: 设置为检出分支的上游 <kbd>u</kbd>: 设置为检出分支的上游
<kbd>g</kbd>: 查看重置选项 <kbd>g</kbd>: 查看重置选项
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 查看提交 <kbd>&lt;enter&gt;</kbd>: 查看提交
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>

View File

@ -48,6 +48,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: 複製提交 SHA 到剪貼簿 <kbd>&lt;c-o&gt;</kbd>: 複製提交 SHA 到剪貼簿
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 檢出提交 <kbd>&lt;space&gt;</kbd>: 檢出提交
<kbd>y</kbd>: 複製提交屬性 <kbd>y</kbd>: 複製提交屬性
<kbd>o</kbd>: 在瀏覽器中開啟提交 <kbd>o</kbd>: 在瀏覽器中開啟提交
@ -60,6 +61,17 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## 主視窗 (一般) ## 主視窗 (一般)
<pre> <pre>
@ -133,6 +145,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: 複製提交 SHA 到剪貼簿 <kbd>&lt;c-o&gt;</kbd>: 複製提交 SHA 到剪貼簿
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 檢出提交 <kbd>&lt;space&gt;</kbd>: 檢出提交
<kbd>y</kbd>: 複製提交屬性 <kbd>y</kbd>: 複製提交屬性
<kbd>o</kbd>: 在瀏覽器中開啟提交 <kbd>o</kbd>: 在瀏覽器中開啟提交
@ -150,6 +163,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<pre> <pre>
<kbd>&lt;c-o&gt;</kbd>: 複製子模組名稱到剪貼簿 <kbd>&lt;c-o&gt;</kbd>: 複製子模組名稱到剪貼簿
<kbd>&lt;enter&gt;</kbd>: 進入子模組 <kbd>&lt;enter&gt;</kbd>: 進入子模組
<kbd>&lt;space&gt;</kbd>: 進入子模組
<kbd>d</kbd>: 移除子模組 <kbd>d</kbd>: 移除子模組
<kbd>u</kbd>: 更新子模組 <kbd>u</kbd>: 更新子模組
<kbd>n</kbd>: 新增子模組 <kbd>n</kbd>: 新增子模組
@ -182,6 +196,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>t</kbd>: 還原提交 <kbd>t</kbd>: 還原提交
<kbd>T</kbd>: 打標籤到提交 <kbd>T</kbd>: 打標籤到提交
<kbd>&lt;c-l&gt;</kbd>: 開啟記錄選單 <kbd>&lt;c-l&gt;</kbd>: 開啟記錄選單
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 檢出提交 <kbd>&lt;space&gt;</kbd>: 檢出提交
<kbd>y</kbd>: 複製提交屬性 <kbd>y</kbd>: 複製提交屬性
<kbd>o</kbd>: 在瀏覽器中開啟提交 <kbd>o</kbd>: 在瀏覽器中開啟提交
@ -223,6 +238,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>d</kbd>: 捨棄 <kbd>d</kbd>: 捨棄
<kbd>n</kbd>: 新分支 <kbd>n</kbd>: 新分支
<kbd>r</kbd>: 重新命名收藏 <kbd>r</kbd>: 重新命名收藏
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案 <kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -247,6 +263,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>g</kbd>: 檢視重設選項 <kbd>g</kbd>: 檢視重設選項
<kbd>R</kbd>: 重新命名分支 <kbd>R</kbd>: 重新命名分支
<kbd>u</kbd>: 設定/取消設定上游 <kbd>u</kbd>: 設定/取消設定上游
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 檢視提交 <kbd>&lt;enter&gt;</kbd>: 檢視提交
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -259,6 +276,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>P</kbd>: 推送標籤 <kbd>P</kbd>: 推送標籤
<kbd>n</kbd>: 建立標籤 <kbd>n</kbd>: 建立標籤
<kbd>g</kbd>: 檢視重設選項 <kbd>g</kbd>: 檢視重設選項
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 檢視提交 <kbd>&lt;enter&gt;</kbd>: 檢視提交
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>
@ -328,6 +346,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>d</kbd>: 刪除分支 <kbd>d</kbd>: 刪除分支
<kbd>u</kbd>: 將此分支設為當前分支之上游 <kbd>u</kbd>: 將此分支設為當前分支之上游
<kbd>g</kbd>: 檢視重設選項 <kbd>g</kbd>: 檢視重設選項
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 檢視提交 <kbd>&lt;enter&gt;</kbd>: 檢視提交
<kbd>/</kbd>: Filter the current view by text <kbd>/</kbd>: Filter the current view by text
</pre> </pre>

3
go.mod
View File

@ -34,6 +34,7 @@ require (
github.com/sanity-io/litter v1.5.2 github.com/sanity-io/litter v1.5.2
github.com/sasha-s/go-deadlock v0.3.1 github.com/sasha-s/go-deadlock v0.3.1
github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus v1.4.2
github.com/spf13/afero v1.9.5
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.0
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
@ -66,7 +67,7 @@ require (
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/sergi/go-diff v1.1.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect
github.com/xanzy/ssh-agent v0.2.1 // indirect github.com/xanzy/ssh-agent v0.2.1 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/net v0.7.0 // indirect golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.10.0 // indirect golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect golang.org/x/term v0.10.0 // indirect

406
go.sum
View File

@ -1,3 +1,43 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OpenPeeDeeP/xdg v1.0.0 h1:UDLmNjCGFZZCaVMB74DqYEtXkHxnTxcr4FeJVF9uCn8= github.com/OpenPeeDeeP/xdg v1.0.0 h1:UDLmNjCGFZZCaVMB74DqYEtXkHxnTxcr4FeJVF9uCn8=
github.com/OpenPeeDeeP/xdg v1.0.0/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo= github.com/OpenPeeDeeP/xdg v1.0.0/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
@ -11,10 +51,18 @@ github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek= github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo= github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= 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/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/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do= 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/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=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -24,6 +72,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
@ -48,19 +102,78 @@ github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agR
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M= github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk= github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
@ -83,13 +196,17 @@ github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE= github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk= github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -129,16 +246,20 @@ github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM= github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
@ -151,6 +272,8 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -160,6 +283,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@ -171,37 +295,161 @@ github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 h1:s/+U+w0teGzcoH2mdIlFQ6KfVKGaYpgyGdUefZrn9TU= golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 h1:s/+U+w0teGzcoH2mdIlFQ6KfVKGaYpgyGdUefZrn9TU=
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-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-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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -217,22 +465,166 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0 h1:KzcWKJ0nMAmGoBhYVMnkWc1rXjB42lKy5aIys4TdLOA= gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0 h1:KzcWKJ0nMAmGoBhYVMnkWc1rXjB42lKy5aIys4TdLOA=
@ -248,3 +640,13 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -11,6 +11,7 @@ import (
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/afero"
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types" appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
@ -75,6 +76,7 @@ func NewCommon(config config.AppConfigurer) (*common.Common, error) {
Tr: tr, Tr: tr,
UserConfig: userConfig, UserConfig: userConfig,
Debug: config.GetDebug(), Debug: config.GetDebug(),
Fs: afero.NewOsFs(),
}, nil }, nil
} }

View File

@ -76,7 +76,10 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
} }
if cliArgs.WorkTree != "" { if cliArgs.WorkTree != "" {
env.SetGitWorkTreeEnv(cliArgs.WorkTree) err := os.Chdir(cliArgs.WorkTree)
if err != nil {
log.Fatalf("Failed to change directory to %s: %v", cliArgs.WorkTree, err)
}
} }
if cliArgs.GitDir != "" { if cliArgs.GitDir != "" {

View File

@ -116,6 +116,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
"stash": tr.StashTitle, "stash": tr.StashTitle,
"suggestions": tr.SuggestionsCheatsheetTitle, "suggestions": tr.SuggestionsCheatsheetTitle,
"extras": tr.ExtrasTitle, "extras": tr.ExtrasTitle,
"worktrees": tr.WorktreesTitle,
} }
title, ok := contextTitleMap[str] title, ok := contextTitleMap[str]

View File

@ -2,11 +2,13 @@ package commands
import ( import (
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/sasha-s/go-deadlock" "github.com/sasha-s/go-deadlock"
"github.com/spf13/afero"
gogit "github.com/jesseduffield/go-git/v5" gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
@ -37,6 +39,9 @@ type GitCommand struct {
Tag *git_commands.TagCommands Tag *git_commands.TagCommands
WorkingTree *git_commands.WorkingTreeCommands WorkingTree *git_commands.WorkingTreeCommands
Bisect *git_commands.BisectCommands Bisect *git_commands.BisectCommands
Worktree *git_commands.WorktreeCommands
Version *git_commands.GitVersion
RepoPaths *git_commands.RepoPaths
Loaders Loaders Loaders Loaders
} }
@ -50,6 +55,7 @@ type Loaders struct {
RemoteLoader *git_commands.RemoteLoader RemoteLoader *git_commands.RemoteLoader
StashLoader *git_commands.StashLoader StashLoader *git_commands.StashLoader
TagLoader *git_commands.TagLoader TagLoader *git_commands.TagLoader
Worktrees *git_commands.WorktreeLoader
} }
func NewGitCommand( func NewGitCommand(
@ -59,17 +65,49 @@ func NewGitCommand(
gitConfig git_config.IGitConfig, gitConfig git_config.IGitConfig,
syncMutex *deadlock.Mutex, syncMutex *deadlock.Mutex,
) (*GitCommand, error) { ) (*GitCommand, error) {
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil { currentPath, err := os.Getwd()
return nil, err if err != nil {
return nil, utils.WrapError(err)
} }
repo, err := setupRepository(gogit.PlainOpenWithOptions, gogit.PlainOpenOptions{DetectDotGit: false, EnableDotGitCommonDir: true}, cmn.Tr.GitconfigParseErr) // converting to forward slashes for the sake of windows (which uses backwards slashes). We want everything
if err != nil { // to have forward slashes internally
return nil, err currentPath = filepath.ToSlash(currentPath)
gitDir := env.GetGitDirEnv()
if gitDir != "" {
// we've been given the git directory explicitly so no need to navigate to it
_, err := cmn.Fs.Stat(gitDir)
if err != nil {
return nil, utils.WrapError(err)
}
} else {
// we haven't been given the git dir explicitly so we assume it's in the current working directory as `.git/` (or an ancestor directory)
rootDirectory, err := findWorktreeRoot(cmn.Fs, currentPath)
if err != nil {
return nil, utils.WrapError(err)
}
currentPath = rootDirectory
err = os.Chdir(rootDirectory)
if err != nil {
return nil, utils.WrapError(err)
}
} }
dotGitDir, err := findDotGitDir(os.Stat, os.ReadFile) repoPaths, err := git_commands.GetRepoPaths(cmn.Fs, currentPath)
if err != nil { if err != nil {
return nil, errors.Errorf("Error getting repo paths: %v", err)
}
repository, err := gogit.PlainOpenWithOptions(
repoPaths.WorktreeGitDirPath(),
&gogit.PlainOpenOptions{DetectDotGit: false, EnableDotGitCommonDir: true},
)
if err != nil {
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
return nil, errors.New(cmn.Tr.GitconfigParseErr)
}
return nil, err return nil, err
} }
@ -78,8 +116,8 @@ func NewGitCommand(
version, version,
osCommand, osCommand,
gitConfig, gitConfig,
dotGitDir, repoPaths,
repo, repository,
syncMutex, syncMutex,
), nil ), nil
} }
@ -89,7 +127,7 @@ func NewGitCommandAux(
version *git_commands.GitVersion, version *git_commands.GitVersion,
osCommand *oscommands.OSCommand, osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig, gitConfig git_config.IGitConfig,
dotGitDir string, repoPaths *git_commands.RepoPaths,
repo *gogit.Repository, repo *gogit.Repository,
syncMutex *deadlock.Mutex, syncMutex *deadlock.Mutex,
) *GitCommand { ) *GitCommand {
@ -102,9 +140,9 @@ func NewGitCommandAux(
// common ones are: cmn, osCommand, dotGitDir, configCommands // common ones are: cmn, osCommand, dotGitDir, configCommands
configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo) configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo)
fileLoader := git_commands.NewFileLoader(cmn, cmd, configCommands) gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, repoPaths, repo, configCommands, syncMutex)
gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, dotGitDir, repo, configCommands, syncMutex) fileLoader := git_commands.NewFileLoader(gitCommon, cmd, configCommands)
statusCommands := git_commands.NewStatusCommands(gitCommon) statusCommands := git_commands.NewStatusCommands(gitCommon)
flowCommands := git_commands.NewFlowCommands(gitCommon) flowCommands := git_commands.NewFlowCommands(gitCommon)
remoteCommands := git_commands.NewRemoteCommands(gitCommon) remoteCommands := git_commands.NewRemoteCommands(gitCommon)
@ -127,12 +165,14 @@ func NewGitCommandAux(
}) })
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder) patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
bisectCommands := git_commands.NewBisectCommands(gitCommon) bisectCommands := git_commands.NewBisectCommands(gitCommon)
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands) branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd) commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode, gitCommon) commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon)
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd) reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes) remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
worktreeLoader := git_commands.NewWorktreeLoader(gitCommon)
stashLoader := git_commands.NewStashLoader(cmn, cmd) stashLoader := git_commands.NewStashLoader(cmn, cmd)
tagLoader := git_commands.NewTagLoader(cmn, cmd) tagLoader := git_commands.NewTagLoader(cmn, cmd)
@ -154,6 +194,8 @@ func NewGitCommandAux(
Tag: tagCommands, Tag: tagCommands,
Bisect: bisectCommands, Bisect: bisectCommands,
WorkingTree: workingTreeCommands, WorkingTree: workingTreeCommands,
Worktree: worktreeCommands,
Version: version,
Loaders: Loaders{ Loaders: Loaders{
BranchLoader: branchLoader, BranchLoader: branchLoader,
CommitFileLoader: commitFileLoader, CommitFileLoader: commitFileLoader,
@ -161,121 +203,40 @@ func NewGitCommandAux(
FileLoader: fileLoader, FileLoader: fileLoader,
ReflogCommitLoader: reflogCommitLoader, ReflogCommitLoader: reflogCommitLoader,
RemoteLoader: remoteLoader, RemoteLoader: remoteLoader,
Worktrees: worktreeLoader,
StashLoader: stashLoader, StashLoader: stashLoader,
TagLoader: tagLoader, TagLoader: tagLoader,
}, },
RepoPaths: repoPaths,
} }
} }
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error { // this returns the root of the current worktree. So if you start lazygit from within
gitDir := env.GetGitDirEnv() // a subdirectory of the worktree, it will start in the context of the root of that worktree
if gitDir != "" { func findWorktreeRoot(fs afero.Fs, currentPath string) (string, error) {
// we've been given the git directory explicitly so no need to navigate to it
_, err := stat(gitDir)
if err != nil {
return utils.WrapError(err)
}
return nil
}
// we haven't been given the git dir explicitly so we assume it's in the current working directory as `.git/` (or an ancestor directory)
for { for {
_, err := stat(".git") // we don't care if .git is a directory or a file: either is okay.
_, err := fs.Stat(path.Join(currentPath, ".git"))
if err == nil { if err == nil {
return nil return currentPath, nil
} }
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return utils.WrapError(err) return "", utils.WrapError(err)
} }
if err = chdir(".."); err != nil { currentPath = path.Dir(currentPath)
return utils.WrapError(err)
}
currentPath, err := os.Getwd() atRoot := currentPath == path.Dir(currentPath)
if err != nil {
return err
}
atRoot := currentPath == filepath.Dir(currentPath)
if atRoot { if atRoot {
// we should never really land here: the code that creates GitCommand should // we should never really land here: the code that creates GitCommand should
// verify we're in a git directory // verify we're in a git directory
return errors.New("Must open lazygit in a git repository") return "", errors.New("Must open lazygit in a git repository")
} }
} }
} }
// resolvePath takes a path containing a symlink and returns the true path
func resolvePath(path string) (string, error) {
l, err := os.Lstat(path)
if err != nil {
return "", err
}
if l.Mode()&os.ModeSymlink == 0 {
return path, nil
}
return filepath.EvalSymlinks(path)
}
func setupRepository(openGitRepository func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error), options gogit.PlainOpenOptions, gitConfigParseErrorStr string) (*gogit.Repository, error) {
unresolvedPath := env.GetGitDirEnv()
if unresolvedPath == "" {
var err error
unresolvedPath, err = os.Getwd()
if err != nil {
return nil, err
}
}
path, err := resolvePath(unresolvedPath)
if err != nil {
return nil, err
}
repository, err := openGitRepository(path, &options)
if err != nil {
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
return nil, errors.New(gitConfigParseErrorStr)
}
return nil, err
}
return repository, err
}
func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filename string) ([]byte, error)) (string, error) {
if env.GetGitDirEnv() != "" {
return env.GetGitDirEnv(), nil
}
f, err := stat(".git")
if err != nil {
return "", err
}
if f.IsDir() {
return ".git", nil
}
fileBytes, err := readFile(".git")
if err != nil {
return "", err
}
fileContent := string(fileBytes)
if !strings.HasPrefix(fileContent, "gitdir: ") {
return "", errors.New(".git is a file which suggests we are in a submodule or a worktree but the file's contents do not contain a gitdir pointing to the actual .git directory")
}
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
}
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error { func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
return osCommand.Cmd.New(git_commands.NewGitCmd("rev-parse").Arg("--git-dir").ToArgv()).DontLog().Run() return osCommand.Cmd.New(git_commands.NewGitCmd("rev-parse").Arg("--git-dir").ToArgv()).DontLog().Run()
} }

View File

@ -19,12 +19,16 @@ func NewBisectCommands(gitCommon *GitCommon) *BisectCommands {
// This command is pretty cheap to run so we're not storing the result anywhere. // This command is pretty cheap to run so we're not storing the result anywhere.
// But if it becomes problematic we can chang that. // But if it becomes problematic we can chang that.
func (self *BisectCommands) GetInfo() *BisectInfo { func (self *BisectCommands) GetInfo() *BisectInfo {
return self.GetInfoForGitDir(self.repoPaths.WorktreeGitDirPath())
}
func (self *BisectCommands) GetInfoForGitDir(gitDir string) *BisectInfo {
var err error var err error
info := &BisectInfo{started: false, log: self.Log, newTerm: "bad", oldTerm: "good"} info := &BisectInfo{started: false, log: self.Log, newTerm: "bad", oldTerm: "good"}
// we return nil if we're not in a git bisect session. // we return nil if we're not in a git bisect session.
// we know we're in a session by the presence of a .git/BISECT_START file // we know we're in a session by the presence of a .git/BISECT_START file
bisectStartPath := filepath.Join(self.dotGitDir, "BISECT_START") bisectStartPath := filepath.Join(gitDir, "BISECT_START")
exists, err := self.os.FileExists(bisectStartPath) exists, err := self.os.FileExists(bisectStartPath)
if err != nil { if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error()) self.Log.Infof("error getting git bisect info: %s", err.Error())
@ -44,7 +48,7 @@ func (self *BisectCommands) GetInfo() *BisectInfo {
info.started = true info.started = true
info.start = strings.TrimSpace(string(startContent)) info.start = strings.TrimSpace(string(startContent))
termsContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_TERMS")) termsContent, err := os.ReadFile(filepath.Join(gitDir, "BISECT_TERMS"))
if err != nil { if err != nil {
// old git versions won't have this file so we default to bad/good // old git versions won't have this file so we default to bad/good
} else { } else {
@ -53,7 +57,7 @@ func (self *BisectCommands) GetInfo() *BisectInfo {
info.oldTerm = splitContent[1] info.oldTerm = splitContent[1]
} }
bisectRefsDir := filepath.Join(self.dotGitDir, "refs", "bisect") bisectRefsDir := filepath.Join(gitDir, "refs", "bisect")
files, err := os.ReadDir(bisectRefsDir) files, err := os.ReadDir(bisectRefsDir)
if err != nil { if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error()) self.Log.Infof("error getting git bisect info: %s", err.Error())
@ -85,7 +89,7 @@ func (self *BisectCommands) GetInfo() *BisectInfo {
info.statusMap[sha] = status info.statusMap[sha] = status
} }
currentContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_EXPECTED_REV")) currentContent, err := os.ReadFile(filepath.Join(gitDir, "BISECT_EXPECTED_REV"))
if err != nil { if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error()) self.Log.Infof("error getting git bisect info: %s", err.Error())
return info return info

View File

@ -81,6 +81,12 @@ outer:
} }
} }
// Sort branches that don't have a recency value alphabetically
// (we're really doing this for the sake of deterministic behaviour across git versions)
slices.SortFunc(branches, func(a *models.Branch, b *models.Branch) bool {
return a.Name < b.Name
})
branches = slices.Prepend(branches, branchesWithRecency...) branches = slices.Prepend(branches, branchesWithRecency...)
foundHead := false foundHead := false

View File

@ -47,7 +47,6 @@ type CommitLoader struct {
func NewCommitLoader( func NewCommitLoader(
cmn *common.Common, cmn *common.Common,
cmd oscommands.ICmdObjBuilder, cmd oscommands.ICmdObjBuilder,
dotGitDir string,
getRebaseMode func() (enums.RebaseMode, error), getRebaseMode func() (enums.RebaseMode, error),
gitCommon *GitCommon, gitCommon *GitCommon,
) *CommitLoader { ) *CommitLoader {
@ -57,7 +56,6 @@ func NewCommitLoader(
getRebaseMode: getRebaseMode, getRebaseMode: getRebaseMode,
readFile: os.ReadFile, readFile: os.ReadFile,
walkFiles: filepath.Walk, walkFiles: filepath.Walk,
dotGitDir: dotGitDir,
mainBranches: nil, mainBranches: nil,
GitCommon: gitCommon, GitCommon: gitCommon,
} }
@ -299,7 +297,7 @@ func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*mo
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) { func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
rewrittenCount := 0 rewrittenCount := 0
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-apply/rewritten")) bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply/rewritten"))
if err == nil { if err == nil {
content := string(bytesContent) content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n")) rewrittenCount = len(strings.Split(content, "\n"))
@ -307,7 +305,7 @@ func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
// we know we're rebasing, so lets get all the files whose names have numbers // we know we're rebasing, so lets get all the files whose names have numbers
commits := []*models.Commit{} commits := []*models.Commit{}
err = self.walkFiles(filepath.Join(self.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error { err = self.walkFiles(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply"), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 { if rewrittenCount > 0 {
rewrittenCount-- rewrittenCount--
return nil return nil
@ -348,7 +346,7 @@ func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
// and extracts out the sha and names of commits that we still have to go // and extracts out the sha and names of commits that we still have to go
// in the rebase: // in the rebase:
func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) { func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")) bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"))
if err != nil { if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error())) self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return // we assume an error means the file doesn't exist so we just return
@ -393,7 +391,7 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
} }
func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string { func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string {
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/done")) bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/done"))
if err != nil { if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading rebase-merge/done: %s", err.Error())) self.Log.Error(fmt.Sprintf("error occurred reading rebase-merge/done: %s", err.Error()))
return "" return ""
@ -406,7 +404,7 @@ func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string {
} }
amendFileExists := false amendFileExists := false
if _, err := os.Stat(filepath.Join(self.dotGitDir, "rebase-merge/amend")); err == nil { if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend")); err == nil {
amendFileExists = true amendFileExists = true
} }

View File

@ -12,7 +12,7 @@ type GitCommon struct {
version *GitVersion version *GitVersion
cmd oscommands.ICmdObjBuilder cmd oscommands.ICmdObjBuilder
os *oscommands.OSCommand os *oscommands.OSCommand
dotGitDir string repoPaths *RepoPaths
repo *gogit.Repository repo *gogit.Repository
config *ConfigCommands config *ConfigCommands
// mutex for doing things like push/pull/fetch // mutex for doing things like push/pull/fetch
@ -24,7 +24,7 @@ func NewGitCommon(
version *GitVersion, version *GitVersion,
cmd oscommands.ICmdObjBuilder, cmd oscommands.ICmdObjBuilder,
osCommand *oscommands.OSCommand, osCommand *oscommands.OSCommand,
dotGitDir string, repoPaths *RepoPaths,
repo *gogit.Repository, repo *gogit.Repository,
config *ConfigCommands, config *ConfigCommands,
syncMutex *deadlock.Mutex, syncMutex *deadlock.Mutex,
@ -34,7 +34,7 @@ func NewGitCommon(
version: version, version: version,
cmd: cmd, cmd: cmd,
os: osCommand, os: osCommand,
dotGitDir: dotGitDir, repoPaths: repoPaths,
repo: repo, repo: repo,
config: config, config: config,
syncMutex: syncMutex, syncMutex: syncMutex,

View File

@ -11,6 +11,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spf13/afero"
) )
type commonDeps struct { type commonDeps struct {
@ -20,9 +21,10 @@ type commonDeps struct {
gitConfig *git_config.FakeGitConfig gitConfig *git_config.FakeGitConfig
getenv func(string) string getenv func(string) string
removeFile func(string) error removeFile func(string) error
dotGitDir string
common *common.Common common *common.Common
cmd *oscommands.CmdObjBuilder cmd *oscommands.CmdObjBuilder
fs afero.Fs
repoPaths *RepoPaths
} }
func buildGitCommon(deps commonDeps) *GitCommon { func buildGitCommon(deps commonDeps) *GitCommon {
@ -33,6 +35,16 @@ func buildGitCommon(deps commonDeps) *GitCommon {
gitCommon.Common = utils.NewDummyCommonWithUserConfig(deps.userConfig) gitCommon.Common = utils.NewDummyCommonWithUserConfig(deps.userConfig)
} }
if deps.fs != nil {
gitCommon.Fs = deps.fs
}
if deps.repoPaths != nil {
gitCommon.repoPaths = deps.repoPaths
} else {
gitCommon.repoPaths = MockRepoPaths(".git")
}
runner := deps.runner runner := deps.runner
if runner == nil { if runner == nil {
runner = oscommands.NewFakeRunner(nil) runner = oscommands.NewFakeRunner(nil)
@ -81,11 +93,6 @@ func buildGitCommon(deps commonDeps) *GitCommon {
TempDir: os.TempDir(), TempDir: os.TempDir(),
}) })
gitCommon.dotGitDir = deps.dotGitDir
if gitCommon.dotGitDir == "" {
gitCommon.dotGitDir = ".git"
}
return gitCommon return gitCommon
} }
@ -96,7 +103,7 @@ func buildRepo() *gogit.Repository {
} }
func buildFileLoader(gitCommon *GitCommon) *FileLoader { func buildFileLoader(gitCommon *GitCommon) *FileLoader {
return NewFileLoader(gitCommon.Common, gitCommon.cmd, gitCommon.config) return NewFileLoader(gitCommon, gitCommon.cmd, gitCommon.config)
} }
func buildSubmoduleCommands(deps commonDeps) *SubmoduleCommands { func buildSubmoduleCommands(deps commonDeps) *SubmoduleCommands {

View File

@ -131,6 +131,17 @@ func (self *FileCommands) GetEditAtLineAndWaitCmdStr(filename string, lineNumber
return cmdStr return cmdStr
} }
func (self *FileCommands) GetOpenDirInEditorCmdStr(path string) string {
template := config.GetOpenDirInEditorTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
templateValues := map[string]string{
"dir": self.cmd.Quote(path),
}
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
return cmdStr
}
func (self *FileCommands) guessDefaultEditor() string { func (self *FileCommands) guessDefaultEditor() string {
// Try to query a few places where editors get configured // Try to query a few places where editors get configured
editor := self.config.GetCoreEditor() editor := self.config.GetCoreEditor()

View File

@ -2,11 +2,11 @@ package git_commands
import ( import (
"fmt" "fmt"
"path/filepath"
"strings" "strings"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
) )
type FileLoaderConfig interface { type FileLoaderConfig interface {
@ -14,15 +14,15 @@ type FileLoaderConfig interface {
} }
type FileLoader struct { type FileLoader struct {
*common.Common *GitCommon
cmd oscommands.ICmdObjBuilder cmd oscommands.ICmdObjBuilder
config FileLoaderConfig config FileLoaderConfig
getFileType func(string) string getFileType func(string) string
} }
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader { func NewFileLoader(gitCommon *GitCommon, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader {
return &FileLoader{ return &FileLoader{
Common: cmn, GitCommon: gitCommon,
cmd: cmd, cmd: cmd,
getFileType: oscommands.FileType, getFileType: oscommands.FileType,
config: config, config: config,
@ -58,13 +58,32 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
Name: status.Name, Name: status.Name,
PreviousName: status.PreviousName, PreviousName: status.PreviousName,
DisplayString: status.StatusString, DisplayString: status.StatusString,
Type: self.getFileType(status.Name),
} }
models.SetStatusFields(file, status.Change) models.SetStatusFields(file, status.Change)
files = append(files, file) files = append(files, file)
} }
// Go through the files to see if any of these files are actually worktrees
// so that we can render them correctly
worktreePaths := linkedWortkreePaths(self.Fs, self.repoPaths.RepoGitDirPath())
for _, file := range files {
for _, worktreePath := range worktreePaths {
absFilePath, err := filepath.Abs(file.Name)
if err != nil {
self.Log.Error(err)
continue
}
if absFilePath == worktreePath {
file.IsWorktree = true
// `git status` renders this worktree as a folder with a trailing slash but we'll represent it as a singular worktree
// If we include the slash, it will be rendered as a folder with a null file inside.
file.Name = strings.TrimSuffix(file.Name, "/")
break
}
}
}
return files return files
} }

View File

@ -5,7 +5,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -41,7 +40,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false, HasMergeConflicts: false,
HasInlineMergeConflicts: false, HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt", DisplayString: "MM file1.txt",
Type: "file",
ShortStatus: "MM", ShortStatus: "MM",
}, },
{ {
@ -54,7 +52,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false, HasMergeConflicts: false,
HasInlineMergeConflicts: false, HasInlineMergeConflicts: false,
DisplayString: "A file3.txt", DisplayString: "A file3.txt",
Type: "file",
ShortStatus: "A ", ShortStatus: "A ",
}, },
{ {
@ -67,7 +64,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false, HasMergeConflicts: false,
HasInlineMergeConflicts: false, HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt", DisplayString: "AM file2.txt",
Type: "file",
ShortStatus: "AM", ShortStatus: "AM",
}, },
{ {
@ -80,7 +76,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false, HasMergeConflicts: false,
HasInlineMergeConflicts: false, HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt", DisplayString: "?? file4.txt",
Type: "file",
ShortStatus: "??", ShortStatus: "??",
}, },
{ {
@ -93,7 +88,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: true, HasMergeConflicts: true,
HasInlineMergeConflicts: true, HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt", DisplayString: "UU file5.txt",
Type: "file",
ShortStatus: "UU", ShortStatus: "UU",
}, },
}, },
@ -113,7 +107,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false, HasMergeConflicts: false,
HasInlineMergeConflicts: false, HasInlineMergeConflicts: false,
DisplayString: "MM a\nb.txt", DisplayString: "MM a\nb.txt",
Type: "file",
ShortStatus: "MM", ShortStatus: "MM",
}, },
}, },
@ -137,7 +130,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false, HasMergeConflicts: false,
HasInlineMergeConflicts: false, HasInlineMergeConflicts: false,
DisplayString: "R before1.txt -> after1.txt", DisplayString: "R before1.txt -> after1.txt",
Type: "file",
ShortStatus: "R ", ShortStatus: "R ",
}, },
{ {
@ -151,7 +143,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false, HasMergeConflicts: false,
HasInlineMergeConflicts: false, HasInlineMergeConflicts: false,
DisplayString: "RM before2.txt -> after2.txt", DisplayString: "RM before2.txt -> after2.txt",
Type: "file",
ShortStatus: "RM", ShortStatus: "RM",
}, },
}, },
@ -174,7 +165,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false, HasMergeConflicts: false,
HasInlineMergeConflicts: false, HasInlineMergeConflicts: false,
DisplayString: "?? a -> b.txt", DisplayString: "?? a -> b.txt",
Type: "file",
ShortStatus: "??", ShortStatus: "??",
}, },
}, },
@ -187,7 +177,7 @@ func TestFileGetStatusFiles(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner) cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
loader := &FileLoader{ loader := &FileLoader{
Common: utils.NewDummyCommon(), GitCommon: buildGitCommon(commonDeps{}),
cmd: cmd, cmd: cmd,
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"}, config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"},
getFileType: func(string) string { return "file" }, getFileType: func(string) string { return "file" },

View File

@ -1,6 +1,8 @@
package git_commands package git_commands
import "strings" import (
"strings"
)
// convenience struct for building git commands. Especially useful when // convenience struct for building git commands. Especially useful when
// including conditional args // including conditional args
@ -42,9 +44,34 @@ func (self *GitCommandBuilder) Config(value string) *GitCommandBuilder {
return self return self
} }
func (self *GitCommandBuilder) RepoPath(value string) *GitCommandBuilder { // the -C arg will make git do a `cd` to the directory before doing anything else
func (self *GitCommandBuilder) Dir(path string) *GitCommandBuilder {
// repo path comes before the command // repo path comes before the command
self.args = append([]string{"-C", value}, self.args...) self.args = append([]string{"-C", path}, self.args...)
return self
}
// Note, you may prefer to use the Dir method instead of this one
func (self *GitCommandBuilder) Worktree(path string) *GitCommandBuilder {
// worktree arg comes before the command
self.args = append([]string{"--work-tree", path}, self.args...)
return self
}
// Note, you may prefer to use the Dir method instead of this one
func (self *GitCommandBuilder) GitDir(path string) *GitCommandBuilder {
// git dir arg comes before the command
self.args = append([]string{"--git-dir", path}, self.args...)
return self
}
func (self *GitCommandBuilder) GitDirIf(condition bool, path string) *GitCommandBuilder {
if condition {
return self.GitDir(path)
}
return self return self
} }

View File

@ -45,7 +45,7 @@ func TestGitCommandBuilder(t *testing.T) {
expected: []string{"git", "-c", "user.email=bar", "-c", "user.name=foo", "push"}, expected: []string{"git", "-c", "user.email=bar", "-c", "user.name=foo", "push"},
}, },
{ {
input: NewGitCmd("push").RepoPath("a/b/c").ToArgv(), input: NewGitCmd("push").Dir("a/b/c").ToArgv(),
expected: []string{"git", "-C", "a/b/c", "push"}, expected: []string{"git", "-C", "a/b/c", "push"},
}, },
} }

View File

@ -10,7 +10,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
type PatchCommands struct { type PatchCommands struct {
@ -80,7 +79,7 @@ func (self *PatchCommands) applyPatchFile(filepath string, opts ApplyPatchOpts)
} }
func (self *PatchCommands) SaveTemporaryPatch(patch string) (string, error) { func (self *PatchCommands) SaveTemporaryPatch(patch string) (string, error) {
filepath := filepath.Join(self.os.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch") filepath := filepath.Join(self.os.GetTempDir(), self.repoPaths.RepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
self.Log.Infof("saving temporary patch to %s", filepath) self.Log.Infof("saving temporary patch to %s", filepath)
if err := self.os.CreateFileWithContent(filepath, patch); err != nil { if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
return "", err return "", err

View File

@ -243,18 +243,18 @@ func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) e
// EditRebaseTodo sets the action for a given rebase commit in the git-rebase-todo file // EditRebaseTodo sets the action for a given rebase commit in the git-rebase-todo file
func (self *RebaseCommands) EditRebaseTodo(commit *models.Commit, action todo.TodoCommand) error { func (self *RebaseCommands) EditRebaseTodo(commit *models.Commit, action todo.TodoCommand) error {
return utils.EditRebaseTodo( return utils.EditRebaseTodo(
filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action, self.config.GetCoreCommentChar()) filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action, self.config.GetCoreCommentChar())
} }
// MoveTodoDown moves a rebase todo item down by one position // MoveTodoDown moves a rebase todo item down by one position
func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error { func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo") fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo")
return utils.MoveTodoDown(fileName, commit.Sha, commit.Action, self.config.GetCoreCommentChar()) return utils.MoveTodoDown(fileName, commit.Sha, commit.Action, self.config.GetCoreCommentChar())
} }
// MoveTodoDown moves a rebase todo item down by one position // MoveTodoDown moves a rebase todo item down by one position
func (self *RebaseCommands) MoveTodoUp(commit *models.Commit) error { func (self *RebaseCommands) MoveTodoUp(commit *models.Commit) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo") fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo")
return utils.MoveTodoUp(fileName, commit.Sha, commit.Action, self.config.GetCoreCommentChar()) return utils.MoveTodoUp(fileName, commit.Sha, commit.Action, self.config.GetCoreCommentChar())
} }

View File

@ -0,0 +1,248 @@
package git_commands
import (
"fmt"
ioFs "io/fs"
"os"
"path"
"path/filepath"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/samber/lo"
"github.com/spf13/afero"
)
type RepoPaths struct {
currentPath string
worktreePath string
worktreeGitDirPath string
repoPath string
repoGitDirPath string
repoName string
}
// Current working directory of the program. Currently, this will always
// be the same as WorktreePath(), but in future we may support running
// lazygit from inside a subdirectory of the worktree.
func (self *RepoPaths) CurrentPath() string {
return self.currentPath
}
// Path to the current worktree. If we're in the main worktree, this will
// be the same as RepoPath()
func (self *RepoPaths) WorktreePath() string {
return self.worktreePath
}
// Path of the worktree's git dir.
// If we're in the main worktree, this will be the .git dir under the RepoPath().
// If we're in a linked worktree, it will be the directory pointed at by the worktree's .git file
func (self *RepoPaths) WorktreeGitDirPath() string {
return self.worktreeGitDirPath
}
// Path of the repo. If we're in a the main worktree, this will be the same as WorktreePath()
// If we're in a bare repo, it will be the parent folder of the bare repo
func (self *RepoPaths) RepoPath() string {
return self.repoPath
}
// path of the git-dir for the repo.
// If this is a bare repo, it will be the location of the bare repo
// If this is a non-bare repo, it will be the location of the .git dir in
// the main worktree.
func (self *RepoPaths) RepoGitDirPath() string {
return self.repoGitDirPath
}
// Name of the repo. Basename of the folder containing the repo.
func (self *RepoPaths) RepoName() string {
return self.repoName
}
// Returns the repo paths for a typical repo
func MockRepoPaths(currentPath string) *RepoPaths {
return &RepoPaths{
currentPath: currentPath,
worktreePath: currentPath,
worktreeGitDirPath: path.Join(currentPath, ".git"),
repoPath: currentPath,
repoGitDirPath: path.Join(currentPath, ".git"),
repoName: "lazygit",
}
}
func GetRepoPaths(
fs afero.Fs,
currentPath string,
) (*RepoPaths, error) {
return getRepoPathsAux(afero.NewOsFs(), resolveSymlink, currentPath)
}
func getRepoPathsAux(
fs afero.Fs,
resolveSymlinkFn func(string) (string, error),
currentPath string,
) (*RepoPaths, error) {
worktreePath := currentPath
repoGitDirPath, repoPath, err := getCurrentRepoGitDirPath(fs, resolveSymlinkFn, currentPath)
if err != nil {
return nil, errors.Errorf("failed to get repo git dir path: %v", err)
}
worktreeGitDirPath, err := worktreeGitDirPath(fs, currentPath)
if err != nil {
return nil, errors.Errorf("failed to get worktree git dir path: %v", err)
}
repoName := path.Base(repoPath)
return &RepoPaths{
currentPath: currentPath,
worktreePath: worktreePath,
worktreeGitDirPath: worktreeGitDirPath,
repoPath: repoPath,
repoGitDirPath: repoGitDirPath,
repoName: repoName,
}, nil
}
// Returns the path of the git-dir for the worktree. For linked worktrees, the worktree has
// a .git file that points to the git-dir (which itself lives in the git-dir
// of the repo)
func worktreeGitDirPath(fs afero.Fs, worktreePath string) (string, error) {
// if .git is a file, we're in a linked worktree, otherwise we're in
// the main worktree
dotGitPath := path.Join(worktreePath, ".git")
gitFileInfo, err := fs.Stat(dotGitPath)
if err != nil {
return "", err
}
if gitFileInfo.IsDir() {
return dotGitPath, nil
}
return linkedWorktreeGitDirPath(fs, worktreePath)
}
func linkedWorktreeGitDirPath(fs afero.Fs, worktreePath string) (string, error) {
dotGitPath := path.Join(worktreePath, ".git")
gitFileContents, err := afero.ReadFile(fs, dotGitPath)
if err != nil {
return "", err
}
// The file will have `gitdir: /path/to/.git/worktrees/<worktree-name>`
gitDirLine := lo.Filter(strings.Split(string(gitFileContents), "\n"), func(line string, _ int) bool {
return strings.HasPrefix(line, "gitdir: ")
})
if len(gitDirLine) == 0 {
return "", errors.New(fmt.Sprintf("%s is a file which suggests we are in a submodule or a worktree but the file's contents do not contain a gitdir pointing to the actual .git directory", dotGitPath))
}
gitDir := strings.TrimPrefix(gitDirLine[0], "gitdir: ")
return gitDir, nil
}
func getCurrentRepoGitDirPath(
fs afero.Fs,
resolveSymlinkFn func(string) (string, error),
currentPath string,
) (string, string, error) {
var unresolvedGitPath string
if env.GetGitDirEnv() != "" {
unresolvedGitPath = env.GetGitDirEnv()
} else {
unresolvedGitPath = path.Join(currentPath, ".git")
}
gitPath, err := resolveSymlinkFn(unresolvedGitPath)
if err != nil {
return "", "", err
}
// check if .git is a file or a directory
gitFileInfo, err := fs.Stat(gitPath)
if err != nil {
return "", "", err
}
if gitFileInfo.IsDir() {
// must be in the main worktree
return gitPath, path.Dir(gitPath), nil
}
// either in a submodule, or worktree
worktreeGitPath, err := linkedWorktreeGitDirPath(fs, currentPath)
if err != nil {
return "", "", errors.Errorf("could not find git dir for %s: %v", currentPath, err)
}
// confirm whether the next directory up is the worktrees/submodules directory
parent := path.Dir(worktreeGitPath)
if path.Base(parent) != "worktrees" && path.Base(parent) != "modules" {
return "", "", errors.Errorf("could not find git dir for %s", currentPath)
}
// if it's a submodule, we treat it as its own repo
if path.Base(parent) == "modules" {
return worktreeGitPath, currentPath, nil
}
gitDirPath := path.Dir(parent)
return gitDirPath, path.Dir(gitDirPath), nil
}
// takes a path containing a symlink and returns the true path
func resolveSymlink(path string) (string, error) {
l, err := os.Lstat(path)
if err != nil {
return "", err
}
if l.Mode()&os.ModeSymlink == 0 {
return path, nil
}
return filepath.EvalSymlinks(path)
}
// Returns the paths of linked worktrees
func linkedWortkreePaths(fs afero.Fs, repoGitDirPath string) []string {
result := []string{}
// For each directory in this path we're going to cat the `gitdir` file and append its contents to our result
// That file points us to the `.git` file in the worktree.
worktreeGitDirsPath := path.Join(repoGitDirPath, "worktrees")
// ensure the directory exists
_, err := fs.Stat(worktreeGitDirsPath)
if err != nil {
return result
}
_ = afero.Walk(fs, worktreeGitDirsPath, func(currPath string, info ioFs.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
return nil
}
gitDirPath := path.Join(currPath, "gitdir")
gitDirBytes, err := afero.ReadFile(fs, gitDirPath)
if err != nil {
// ignoring error
return nil
}
trimmedGitDir := strings.TrimSpace(string(gitDirBytes))
// removing the .git part
worktreeDir := path.Dir(trimmedGitDir)
result = append(result, worktreeDir)
return nil
})
return result
}

View File

@ -0,0 +1,118 @@
package git_commands
import (
"testing"
"github.com/go-errors/errors"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)
func mockResolveSymlinkFn(p string) (string, error) { return p, nil }
type Scenario struct {
Name string
BeforeFunc func(fs afero.Fs)
Path string
Expected *RepoPaths
Err error
}
func TestGetRepoPathsAux(t *testing.T) {
scenarios := []Scenario{
{
Name: "typical case",
BeforeFunc: func(fs afero.Fs) {
// setup for main worktree
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
},
Path: "/path/to/repo",
Expected: &RepoPaths{
currentPath: "/path/to/repo",
worktreePath: "/path/to/repo",
worktreeGitDirPath: "/path/to/repo/.git",
repoPath: "/path/to/repo",
repoGitDirPath: "/path/to/repo/.git",
repoName: "repo",
},
Err: nil,
},
{
Name: "linked worktree",
BeforeFunc: func(fs afero.Fs) {
// setup for linked worktree
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/worktree1", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo/worktree1/.git", []byte("gitdir: /path/to/repo/.git/worktrees/worktree1"), 0o644)
},
Path: "/path/to/repo/worktree1",
Expected: &RepoPaths{
currentPath: "/path/to/repo/worktree1",
worktreePath: "/path/to/repo/worktree1",
worktreeGitDirPath: "/path/to/repo/.git/worktrees/worktree1",
repoPath: "/path/to/repo",
repoGitDirPath: "/path/to/repo/.git",
repoName: "repo",
},
Err: nil,
},
{
Name: "worktree .git file missing gitdir directive",
BeforeFunc: func(fs afero.Fs) {
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/worktree2", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo/worktree2/.git", []byte("blah"), 0o644)
},
Path: "/path/to/repo/worktree2",
Expected: nil,
Err: errors.New("failed to get repo git dir path: could not find git dir for /path/to/repo/worktree2: /path/to/repo/worktree2/.git is a file which suggests we are in a submodule or a worktree but the file's contents do not contain a gitdir pointing to the actual .git directory"),
},
{
Name: "worktree .git file gitdir directive points to a non-existing directory",
BeforeFunc: func(fs afero.Fs) {
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/worktree2", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo/worktree2/.git", []byte("gitdir: /nonexistant"), 0o644)
},
Path: "/path/to/repo/worktree2",
Expected: nil,
Err: errors.New("failed to get repo git dir path: could not find git dir for /path/to/repo/worktree2"),
},
{
Name: "submodule",
BeforeFunc: func(fs afero.Fs) {
_ = fs.MkdirAll("/path/to/repo/.git/modules/submodule1", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo/submodule1/.git", []byte("gitdir: /path/to/repo/.git/modules/submodule1"), 0o644)
},
Path: "/path/to/repo/submodule1",
Expected: &RepoPaths{
currentPath: "/path/to/repo/submodule1",
worktreePath: "/path/to/repo/submodule1",
worktreeGitDirPath: "/path/to/repo/.git/modules/submodule1",
repoPath: "/path/to/repo/submodule1",
repoGitDirPath: "/path/to/repo/.git/modules/submodule1",
repoName: "submodule1",
},
Err: nil,
},
}
for _, s := range scenarios {
s := s
t.Run(s.Name, func(t *testing.T) {
fs := afero.NewMemMapFs()
// prepare the filesystem for the scenario
s.BeforeFunc(fs)
// run the function with the scenario path
repoPaths, err := getRepoPathsAux(fs, mockResolveSymlinkFn, s.Path)
// check the error and the paths
if s.Err != nil {
assert.Error(t, err)
assert.EqualError(t, err, s.Err.Error())
} else {
assert.Nil(t, err)
assert.Equal(t, s.Expected, repoPaths)
}
})
}
}

View File

@ -24,14 +24,14 @@ func NewStatusCommands(
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase // RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase // and "interactive" for interactive rebase
func (self *StatusCommands) RebaseMode() (enums.RebaseMode, error) { func (self *StatusCommands) RebaseMode() (enums.RebaseMode, error) {
exists, err := self.os.FileExists(filepath.Join(self.dotGitDir, "rebase-apply")) exists, err := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply"))
if err != nil { if err != nil {
return enums.REBASE_MODE_NONE, err return enums.REBASE_MODE_NONE, err
} }
if exists { if exists {
return enums.REBASE_MODE_NORMAL, nil return enums.REBASE_MODE_NORMAL, nil
} }
exists, err = self.os.FileExists(filepath.Join(self.dotGitDir, "rebase-merge")) exists, err = self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge"))
if exists { if exists {
return enums.REBASE_MODE_INTERACTIVE, err return enums.REBASE_MODE_INTERACTIVE, err
} else { } else {
@ -69,5 +69,5 @@ func IsBareRepo(osCommand *oscommands.OSCommand) (bool, error) {
// IsInMergeState states whether we are still mid-merge // IsInMergeState states whether we are still mid-merge
func (self *StatusCommands) IsInMergeState() (bool, error) { func (self *StatusCommands) IsInMergeState() (bool, error) {
return self.os.FileExists(filepath.Join(self.dotGitDir, "MERGE_HEAD")) return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "MERGE_HEAD"))
} }

View File

@ -82,7 +82,7 @@ func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
} }
cmdArgs := NewGitCmd("stash"). cmdArgs := NewGitCmd("stash").
RepoPath(submodule.Path). Dir(submodule.Path).
Arg("--include-untracked"). Arg("--include-untracked").
ToArgv() ToArgv()
@ -139,7 +139,9 @@ func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
self.Log.Error(err) self.Log.Error(err)
} }
return os.RemoveAll(filepath.Join(self.dotGitDir, "modules", submodule.Path)) // We may in fact want to use the repo's git dir path but git docs say not to
// mix submodules and worktrees anyway.
return os.RemoveAll(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "modules", submodule.Path))
} }
func (self *SubmoduleCommands) Add(name string, path string, url string) error { func (self *SubmoduleCommands) Add(name string, path string, url string) error {

View File

@ -82,6 +82,7 @@ type PullOptions struct {
RemoteName string RemoteName string
BranchName string BranchName string
FastForwardOnly bool FastForwardOnly bool
WorktreeGitDir string
} }
func (self *SyncCommands) Pull(task gocui.Task, opts PullOptions) error { func (self *SyncCommands) Pull(task gocui.Task, opts PullOptions) error {
@ -90,6 +91,7 @@ func (self *SyncCommands) Pull(task gocui.Task, opts PullOptions) error {
ArgIf(opts.FastForwardOnly, "--ff-only"). ArgIf(opts.FastForwardOnly, "--ff-only").
ArgIf(opts.RemoteName != "", opts.RemoteName). ArgIf(opts.RemoteName != "", opts.RemoteName).
ArgIf(opts.BranchName != "", opts.BranchName). ArgIf(opts.BranchName != "", opts.BranchName).
GitDirIf(opts.WorktreeGitDir != "", opts.WorktreeGitDir).
ToArgv() ToArgv()
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user // setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
@ -97,7 +99,12 @@ func (self *SyncCommands) Pull(task gocui.Task, opts PullOptions) error {
return self.cmd.New(cmdArgs).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() return self.cmd.New(cmdArgs).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run()
} }
func (self *SyncCommands) FastForward(task gocui.Task, branchName string, remoteName string, remoteBranchName string) error { func (self *SyncCommands) FastForward(
task gocui.Task,
branchName string,
remoteName string,
remoteBranchName string,
) error {
cmdArgs := NewGitCmd("fetch"). cmdArgs := NewGitCmd("fetch").
Arg(remoteName). Arg(remoteName).
Arg(remoteBranchName + ":" + branchName). Arg(remoteBranchName + ":" + branchName).

View File

@ -0,0 +1,74 @@
package git_commands
import (
"path/filepath"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
type WorktreeCommands struct {
*GitCommon
}
func NewWorktreeCommands(gitCommon *GitCommon) *WorktreeCommands {
return &WorktreeCommands{
GitCommon: gitCommon,
}
}
type NewWorktreeOpts struct {
// required. The path of the new worktree.
Path string
// required. The base branch/ref.
Base string
// if true, ends up with a detached head
Detach bool
// optional. if empty, and if detach is false, we will checkout the base
Branch string
}
func (self *WorktreeCommands) New(opts NewWorktreeOpts) error {
if opts.Detach && opts.Branch != "" {
panic("cannot specify branch when detaching")
}
cmdArgs := NewGitCmd("worktree").Arg("add").
ArgIf(opts.Detach, "--detach").
ArgIf(opts.Branch != "", "-b", opts.Branch).
Arg(opts.Path, opts.Base)
return self.cmd.New(cmdArgs.ToArgv()).Run()
}
func (self *WorktreeCommands) Delete(worktreePath string, force bool) error {
cmdArgs := NewGitCmd("worktree").Arg("remove").ArgIf(force, "-f").Arg(worktreePath).ToArgv()
return self.cmd.New(cmdArgs).Run()
}
func (self *WorktreeCommands) Detach(worktreePath string) error {
cmdArgs := NewGitCmd("checkout").Arg("--detach").GitDir(filepath.Join(worktreePath, ".git")).ToArgv()
return self.cmd.New(cmdArgs).Run()
}
func WorktreeForBranch(branch *models.Branch, worktrees []*models.Worktree) (*models.Worktree, bool) {
for _, worktree := range worktrees {
if worktree.Branch == branch.Name {
return worktree, true
}
}
return nil, false
}
func CheckedOutByOtherWorktree(branch *models.Branch, worktrees []*models.Worktree) bool {
worktree, ok := WorktreeForBranch(branch, worktrees)
if !ok {
return false
}
return !worktree.IsCurrent
}

View File

@ -0,0 +1,256 @@
package git_commands
import (
iofs "io/fs"
"path/filepath"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/spf13/afero"
)
type WorktreeLoader struct {
*GitCommon
}
func NewWorktreeLoader(gitCommon *GitCommon) *WorktreeLoader {
return &WorktreeLoader{GitCommon: gitCommon}
}
func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
currentRepoPath := self.repoPaths.RepoPath()
worktreePath := self.repoPaths.WorktreePath()
cmdArgs := NewGitCmd("worktree").Arg("list", "--porcelain").ToArgv()
worktreesOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
splitLines := strings.Split(
utils.NormalizeLinefeeds(worktreesOutput), "\n",
)
var worktrees []*models.Worktree
var current *models.Worktree
for _, splitLine := range splitLines {
// worktrees are defined over multiple lines and are separated by blank lines
// so if we reach a blank line we're done with the current worktree
if len(splitLine) == 0 && current != nil {
worktrees = append(worktrees, current)
current = nil
continue
}
// ignore bare repo (not sure why it's even appearing in this list: it's not a worktree)
if splitLine == "bare" {
current = nil
continue
}
if strings.HasPrefix(splitLine, "worktree ") {
path := strings.SplitN(splitLine, " ", 2)[1]
isMain := path == currentRepoPath
isCurrent := path == worktreePath
isPathMissing := self.pathExists(path)
var gitDir string
gitDir, err := worktreeGitDirPath(self.Fs, path)
if err != nil {
self.Log.Warnf("Could not find git dir for worktree %s: %v", path, err)
}
current = &models.Worktree{
IsMain: isMain,
IsCurrent: isCurrent,
IsPathMissing: isPathMissing,
Path: path,
GitDir: gitDir,
}
} else if strings.HasPrefix(splitLine, "branch ") {
branch := strings.SplitN(splitLine, " ", 2)[1]
current.Branch = strings.TrimPrefix(branch, "refs/heads/")
}
}
names := getUniqueNamesFromPaths(lo.Map(worktrees, func(worktree *models.Worktree, _ int) string {
return worktree.Path
}))
for index, worktree := range worktrees {
worktree.Name = names[index]
}
// move current worktree to the top
for i, worktree := range worktrees {
if worktree.IsCurrent {
worktrees = append(worktrees[:i], worktrees[i+1:]...)
worktrees = append([]*models.Worktree{worktree}, worktrees...)
break
}
}
// Some worktrees are on a branch but are mid-rebase, and in those cases,
// `git worktree list` will not show the branch name. We can get the branch
// name from the `rebase-merge/head-name` file (if it exists) in the folder
// for the worktree in the parent repo's .git/worktrees folder.
for _, worktree := range worktrees {
// No point checking if we already have a branch name
if worktree.Branch != "" {
continue
}
// If we couldn't find the git directory, we can't find the branch name
if worktree.GitDir == "" {
continue
}
rebasedBranch, ok := self.rebasedBranch(worktree)
if ok {
worktree.Branch = rebasedBranch
continue
}
bisectedBranch, ok := self.bisectedBranch(worktree)
if ok {
worktree.Branch = bisectedBranch
continue
}
}
return worktrees, nil
}
func (self *WorktreeLoader) pathExists(path string) bool {
if _, err := self.Fs.Stat(path); err != nil {
if errors.Is(err, iofs.ErrNotExist) {
return true
}
self.Log.Errorf("failed to check if worktree path `%s` exists\n%v", path, err)
return false
}
return false
}
func (self *WorktreeLoader) rebasedBranch(worktree *models.Worktree) (string, bool) {
for _, dir := range []string{"rebase-merge", "rebase-apply"} {
if bytesContent, err := afero.ReadFile(self.Fs, filepath.Join(worktree.GitDir, dir, "head-name")); err == nil {
headName := strings.TrimSpace(string(bytesContent))
shortHeadName := strings.TrimPrefix(headName, "refs/heads/")
return shortHeadName, true
}
}
return "", false
}
func (self *WorktreeLoader) bisectedBranch(worktree *models.Worktree) (string, bool) {
bisectStartPath := filepath.Join(worktree.GitDir, "BISECT_START")
startContent, err := afero.ReadFile(self.Fs, bisectStartPath)
if err != nil {
return "", false
}
return strings.TrimSpace(string(startContent)), true
}
type pathWithIndexT struct {
path string
index int
}
type nameWithIndexT struct {
name string
index int
}
func getUniqueNamesFromPaths(paths []string) []string {
pathsWithIndex := lo.Map(paths, func(path string, index int) pathWithIndexT {
return pathWithIndexT{path, index}
})
namesWithIndex := getUniqueNamesFromPathsAux(pathsWithIndex, 0)
// now sort based on index
result := make([]string, len(namesWithIndex))
for _, nameWithIndex := range namesWithIndex {
result[nameWithIndex.index] = nameWithIndex.name
}
return result
}
func getUniqueNamesFromPathsAux(paths []pathWithIndexT, depth int) []nameWithIndexT {
// If we have no paths, return an empty array
if len(paths) == 0 {
return []nameWithIndexT{}
}
// If we have only one path, return the last segment of the path
if len(paths) == 1 {
path := paths[0]
return []nameWithIndexT{{index: path.index, name: sliceAtDepth(path.path, depth)}}
}
// group the paths by their value at the specified depth
groups := make(map[string][]pathWithIndexT)
for _, path := range paths {
value := valueAtDepth(path.path, depth)
groups[value] = append(groups[value], path)
}
result := []nameWithIndexT{}
for _, group := range groups {
if len(group) == 1 {
path := group[0]
result = append(result, nameWithIndexT{index: path.index, name: sliceAtDepth(path.path, depth)})
} else {
result = append(result, getUniqueNamesFromPathsAux(group, depth+1)...)
}
}
return result
}
// if the path is /a/b/c/d, and the depth is 0, the value is 'd'. If the depth is 1, the value is 'c', etc
func valueAtDepth(path string, depth int) string {
path = strings.TrimPrefix(path, "/")
path = strings.TrimSuffix(path, "/")
// Split the path into segments
segments := strings.Split(path, "/")
// Get the length of segments
length := len(segments)
// If the depth is greater than the length of segments, return an empty string
if depth >= length {
return ""
}
// Return the segment at the specified depth from the end of the path
return segments[length-1-depth]
}
// if the path is /a/b/c/d, and the depth is 0, the value is 'd'. If the depth is 1, the value is 'b/c', etc
func sliceAtDepth(path string, depth int) string {
path = strings.TrimPrefix(path, "/")
path = strings.TrimSuffix(path, "/")
// Split the path into segments
segments := strings.Split(path, "/")
// Get the length of segments
length := len(segments)
// If the depth is greater than or equal to the length of segments, return an empty string
if depth >= length {
return ""
}
// Join the segments from the specified depth till end of the path
return strings.Join(segments[length-1-depth:], "/")
}

View File

@ -0,0 +1,238 @@
package git_commands
import (
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)
func TestGetWorktrees(t *testing.T) {
type scenario struct {
testName string
repoPaths *RepoPaths
before func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs)
expectedWorktrees []*models.Worktree
expectedErr string
}
scenarios := []scenario{
{
testName: "Single worktree (main)",
repoPaths: &RepoPaths{
repoPath: "/path/to/repo",
worktreePath: "/path/to/repo",
},
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) {
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
`worktree /path/to/repo
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
branch refs/heads/mybranch
`,
nil)
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
},
expectedWorktrees: []*models.Worktree{
{
IsMain: true,
IsCurrent: true,
Path: "/path/to/repo",
IsPathMissing: false,
GitDir: "/path/to/repo/.git",
Branch: "mybranch",
Name: "repo",
},
},
expectedErr: "",
},
{
testName: "Multiple worktrees (main + linked)",
repoPaths: &RepoPaths{
repoPath: "/path/to/repo",
worktreePath: "/path/to/repo",
},
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) {
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
`worktree /path/to/repo
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
branch refs/heads/mybranch
worktree /path/to/repo-worktree
HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de
branch refs/heads/mybranch-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)
_ = afero.WriteFile(fs, "/path/to/repo-worktree/.git", []byte("gitdir: /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",
Name: "repo",
},
{
IsMain: false,
IsCurrent: false,
Path: "/path/to/repo-worktree",
IsPathMissing: false,
GitDir: "/path/to/repo/.git/worktrees/repo-worktree",
Branch: "mybranch-worktree",
Name: "repo-worktree",
},
},
expectedErr: "",
},
{
testName: "Worktree missing path",
repoPaths: &RepoPaths{
repoPath: "/path/to/repo",
worktreePath: "/path/to/repo",
},
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) {
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
`worktree /path/to/worktree
HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de
branch refs/heads/missingbranch
`,
nil)
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
},
expectedWorktrees: []*models.Worktree{
{
IsMain: false,
IsCurrent: false,
Path: "/path/to/worktree",
IsPathMissing: true,
GitDir: "",
Branch: "missingbranch",
Name: "worktree",
},
},
expectedErr: "",
},
{
testName: "In linked worktree",
repoPaths: &RepoPaths{
repoPath: "/path/to/repo",
worktreePath: "/path/to/repo-worktree",
},
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) {
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
`worktree /path/to/repo
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
branch refs/heads/mybranch
worktree /path/to/repo-worktree
HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de
branch refs/heads/mybranch-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)
_ = afero.WriteFile(fs, "/path/to/repo-worktree/.git", []byte("gitdir: /path/to/repo/.git/worktrees/repo-worktree"), 0o755)
},
expectedWorktrees: []*models.Worktree{
{
IsMain: false,
IsCurrent: true,
Path: "/path/to/repo-worktree",
IsPathMissing: false,
GitDir: "/path/to/repo/.git/worktrees/repo-worktree",
Branch: "mybranch-worktree",
Name: "repo-worktree",
},
{
IsMain: true,
IsCurrent: false,
Path: "/path/to/repo",
IsPathMissing: false,
GitDir: "/path/to/repo/.git",
Branch: "mybranch",
Name: "repo",
},
},
expectedErr: "",
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
runner := oscommands.NewFakeRunner(t)
fs := afero.NewMemMapFs()
s.before(runner, fs)
loader := &WorktreeLoader{
GitCommon: buildGitCommon(commonDeps{runner: runner, fs: fs, repoPaths: s.repoPaths}),
}
worktrees, err := loader.GetWorktrees()
if s.expectedErr != "" {
assert.EqualError(t, errors.New(s.expectedErr), err.Error())
} else {
assert.NoError(t, err)
assert.EqualValues(t, worktrees, s.expectedWorktrees)
}
})
}
}
func TestGetUniqueNamesFromPaths(t *testing.T) {
for _, scenario := range []struct {
input []string
expected []string
}{
{
input: []string{},
expected: []string{},
},
{
input: []string{
"/my/path/feature/one",
},
expected: []string{
"one",
},
},
{
input: []string{
"/my/path/feature/one/",
},
expected: []string{
"one",
},
},
{
input: []string{
"/a/b/c/d",
"/a/b/c/e",
"/a/b/f/d",
"/a/e/c/d",
},
expected: []string{
"b/c/d",
"e",
"f/d",
"e/c/d",
},
},
} {
actual := getUniqueNamesFromPaths(scenario.input)
assert.EqualValues(t, scenario.expected, actual)
}
}

View File

@ -1,305 +1,74 @@
package commands package commands
import ( import (
"fmt"
"os"
"testing" "testing"
"time"
"github.com/go-errors/errors" "github.com/go-errors/errors"
gogit "github.com/jesseduffield/go-git/v5" "github.com/spf13/afero"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
type fileInfoMock struct { func TestFindWorktreeRoot(t *testing.T) {
name string
size int64
fileMode os.FileMode
fileModTime time.Time
isDir bool
sys interface{}
}
// Name is a function.
func (f fileInfoMock) Name() string {
return f.name
}
// Size is a function.
func (f fileInfoMock) Size() int64 {
return f.size
}
// Mode is a function.
func (f fileInfoMock) Mode() os.FileMode {
return f.fileMode
}
// ModTime is a function.
func (f fileInfoMock) ModTime() time.Time {
return f.fileModTime
}
// IsDir is a function.
func (f fileInfoMock) IsDir() bool {
return f.isDir
}
// Sys is a function.
func (f fileInfoMock) Sys() interface{} {
return f.sys
}
// TestNavigateToRepoRootDirectory is a function.
func TestNavigateToRepoRootDirectory(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string
stat func(string) (os.FileInfo, error) currentPath string
chdir func(string) error before func(fs afero.Fs)
test func(error) expectedPath string
expectedErr string
} }
scenarios := []scenario{ scenarios := []scenario{
{ {
"Navigate to git repository", testName: "at root of worktree",
func(string) (os.FileInfo, error) { currentPath: "/path/to/repo",
return fileInfoMock{isDir: true}, nil before: func(fs afero.Fs) {
}, _ = fs.MkdirAll("/path/to/repo/.git", 0o755)
func(string) error {
return nil
},
func(err error) {
assert.NoError(t, err)
}, },
expectedPath: "/path/to/repo",
expectedErr: "",
}, },
{ {
"An error occurred when getting path information", testName: "inside worktree",
func(string) (os.FileInfo, error) { currentPath: "/path/to/repo/subdir",
return nil, fmt.Errorf("An error occurred") before: func(fs afero.Fs) {
}, _ = fs.MkdirAll("/path/to/repo/.git", 0o755)
func(string) error { _ = fs.MkdirAll("/path/to/repo/subdir", 0o755)
return nil
},
func(err error) {
assert.Error(t, err)
assert.EqualError(t, err, "An error occurred")
}, },
expectedPath: "/path/to/repo",
expectedErr: "",
}, },
{ {
"An error occurred when trying to move one path backward", testName: "not in a git repo",
func(string) (os.FileInfo, error) { currentPath: "/path/to/dir",
return nil, os.ErrNotExist before: func(fs afero.Fs) {},
}, expectedPath: "",
func(string) error { expectedErr: "Must open lazygit in a git repository",
return fmt.Errorf("An error occurred") },
}, {
func(err error) { testName: "In linked worktree",
assert.Error(t, err) currentPath: "/path/to/worktree",
assert.EqualError(t, err, "An error occurred") before: func(fs afero.Fs) {
_ = fs.MkdirAll("/path/to/worktree", 0o755)
_ = afero.WriteFile(fs, "/path/to/worktree/.git", []byte("blah"), 0o755)
}, },
expectedPath: "/path/to/worktree",
expectedErr: "",
}, },
} }
for _, s := range scenarios { for _, s := range scenarios {
s := s s := s
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
s.test(navigateToRepoRootDirectory(s.stat, s.chdir)) fs := afero.NewMemMapFs()
}) s.before(fs)
}
} root, err := findWorktreeRoot(fs, s.currentPath)
if s.expectedErr != "" {
// TestSetupRepository is a function. assert.EqualError(t, errors.New(s.expectedErr), err.Error())
func TestSetupRepository(t *testing.T) { } else {
type scenario struct { assert.NoError(t, err)
testName string assert.Equal(t, s.expectedPath, root)
openGitRepository func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) }
errorStr string
options gogit.PlainOpenOptions
test func(*gogit.Repository, error)
}
scenarios := []scenario{
{
"A gitconfig parsing error occurred",
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
return nil, fmt.Errorf(`unquoted '\' must be followed by new line`)
},
"error translated",
gogit.PlainOpenOptions{},
func(r *gogit.Repository, err error) {
assert.Error(t, err)
assert.EqualError(t, err, "error translated")
},
},
{
"A gogit error occurred",
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
return nil, fmt.Errorf("Error from inside gogit")
},
"",
gogit.PlainOpenOptions{},
func(r *gogit.Repository, err error) {
assert.Error(t, err)
assert.EqualError(t, err, "Error from inside gogit")
},
},
{
"Setup done properly",
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
assert.NoError(t, os.RemoveAll("/tmp/lazygit-test"))
r, err := gogit.PlainInit("/tmp/lazygit-test", false)
assert.NoError(t, err)
return r, nil
},
"",
gogit.PlainOpenOptions{},
func(r *gogit.Repository, err error) {
assert.NoError(t, err)
assert.NotNil(t, r)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.test(setupRepository(s.openGitRepository, s.options, s.errorStr))
})
}
}
// TestNewGitCommand is a function.
func TestNewGitCommand(t *testing.T) {
actual, err := os.Getwd()
assert.NoError(t, err)
defer func() {
assert.NoError(t, os.Chdir(actual))
}()
type scenario struct {
testName string
setup func()
test func(*GitCommand, error)
}
scenarios := []scenario{
{
"An error occurred, folder doesn't contains a git repository",
func() {
assert.NoError(t, os.Chdir("/tmp"))
},
func(gitCmd *GitCommand, err error) {
assert.Error(t, err)
assert.Regexp(t, `Must open lazygit in a git repository`, err.Error())
},
},
{
"New GitCommand object created",
func() {
assert.NoError(t, os.RemoveAll("/tmp/lazygit-test"))
_, err := gogit.PlainInit("/tmp/lazygit-test", false)
assert.NoError(t, err)
assert.NoError(t, os.Chdir("/tmp/lazygit-test"))
},
func(gitCmd *GitCommand, err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.setup()
s.test(
NewGitCommand(utils.NewDummyCommon(),
&git_commands.GitVersion{},
oscommands.NewDummyOSCommand(),
git_config.NewFakeGitConfig(nil),
&deadlock.Mutex{},
))
})
}
}
func TestFindDotGitDir(t *testing.T) {
type scenario struct {
testName string
stat func(string) (os.FileInfo, error)
readFile func(filename string) ([]byte, error)
test func(string, error)
}
scenarios := []scenario{
{
".git is a directory",
func(dotGit string) (os.FileInfo, error) {
assert.Equal(t, ".git", dotGit)
return os.Stat("testdata/a_dir")
},
func(dotGit string) ([]byte, error) {
assert.Fail(t, "readFile should not be called if .git is a directory")
return nil, nil
},
func(gitDir string, err error) {
assert.NoError(t, err)
assert.Equal(t, ".git", gitDir)
},
},
{
".git is a file",
func(dotGit string) (os.FileInfo, error) {
assert.Equal(t, ".git", dotGit)
return os.Stat("testdata/a_file")
},
func(dotGit string) ([]byte, error) {
assert.Equal(t, ".git", dotGit)
return []byte("gitdir: blah\n"), nil
},
func(gitDir string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah", gitDir)
},
},
{
"os.Stat returns an error",
func(dotGit string) (os.FileInfo, error) {
assert.Equal(t, ".git", dotGit)
return nil, errors.New("error")
},
func(dotGit string) ([]byte, error) {
assert.Fail(t, "readFile should not be called os.Stat returns an error")
return nil, nil
},
func(gitDir string, err error) {
assert.Error(t, err)
},
},
{
"readFile returns an error",
func(dotGit string) (os.FileInfo, error) {
assert.Equal(t, ".git", dotGit)
return os.Stat("testdata/a_file")
},
func(dotGit string) ([]byte, error) {
return nil, errors.New("error")
},
func(gitDir string, err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.test(findDotGitDir(s.stat, s.readFile))
}) })
} }
} }

View File

@ -18,8 +18,10 @@ type File struct {
HasMergeConflicts bool HasMergeConflicts bool
HasInlineMergeConflicts bool HasInlineMergeConflicts bool
DisplayString string DisplayString string
Type string // one of 'file', 'directory', and 'other'
ShortStatus string // e.g. 'AD', ' A', 'M ', '??' ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
// If true, this must be a worktree folder
IsWorktree bool
} }
// sometimes we need to deal with either a node (which contains a file) or an actual file // sometimes we need to deal with either a node (which contains a file) or an actual file

View File

@ -0,0 +1,37 @@
package models
// A git worktree
type Worktree struct {
// if false, this is a linked worktree
IsMain bool
// if true, this is the worktree that is currently checked out
IsCurrent bool
// path to the directory of the worktree i.e. the directory that contains all the user's files
Path string
// if true, the path is not found
IsPathMissing bool
// path of the git directory for this worktree. The equivalent of the .git directory
// in the main worktree. For linked worktrees this would be <repo_path>/.git/worktrees/<name>
GitDir string
// If the worktree has a branch checked out, this field will be set to the branch name.
// A branch is considered 'checked out' if:
// * the worktree is directly on the branch
// * the worktree is mid-rebase on the branch
// * the worktree is mid-bisect on the branch
Branch 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
}
func (w *Worktree) RefName() string {
return w.Name
}
func (w *Worktree) ID() string {
return w.Path
}
func (w *Worktree) Description() string {
return w.RefName()
}

View File

@ -24,6 +24,9 @@ type ICmdObj interface {
AddEnvVars(...string) ICmdObj AddEnvVars(...string) ICmdObj
GetEnvVars() []string GetEnvVars() []string
// sets the working directory
SetWd(string) ICmdObj
// runs the command and returns an error if any // runs the command and returns an error if any
Run() error Run() error
// runs the command and returns the output as a string, and an error if any // runs the command and returns the output as a string, and an error if any
@ -142,6 +145,12 @@ func (self *CmdObj) GetEnvVars() []string {
return self.cmd.Env return self.cmd.Env
} }
func (self *CmdObj) SetWd(wd string) ICmdObj {
self.cmd.Dir = wd
return self
}
func (self *CmdObj) DontLog() ICmdObj { func (self *CmdObj) DontLog() ICmdObj {
self.dontLog = true self.dontLog = true
return self return self

View File

@ -4,6 +4,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/afero"
) )
// Commonly used things wrapped into one struct for convenience when passing it around // Commonly used things wrapped into one struct for convenience when passing it around
@ -12,4 +13,7 @@ type Common struct {
Tr *i18n.TranslationSet Tr *i18n.TranslationSet
UserConfig *config.UserConfig UserConfig *config.UserConfig
Debug bool Debug bool
// for interacting with the filesystem. We use afero rather than the default
// `os` package for the sake of mocking the filesystem in tests
Fs afero.Fs
} }

View File

@ -28,10 +28,20 @@ func GetEditAtLineAndWaitTemplate(osConfig *OSConfig, guessDefaultEditor func()
return template return template
} }
func GetOpenDirInEditorTemplate(osConfig *OSConfig, guessDefaultEditor func() string) string {
preset := getPreset(osConfig, guessDefaultEditor)
template := osConfig.OpenDirInEditor
if template == "" {
template = preset.openDirInEditorTemplate
}
return template
}
type editPreset struct { type editPreset struct {
editTemplate string editTemplate string
editAtLineTemplate string editAtLineTemplate string
editAtLineAndWaitTemplate string editAtLineAndWaitTemplate string
openDirInEditorTemplate string
editInTerminal bool editInTerminal bool
} }
@ -48,30 +58,35 @@ func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset
editTemplate: "hx -- {{filename}}", editTemplate: "hx -- {{filename}}",
editAtLineTemplate: "hx -- {{filename}}:{{line}}", editAtLineTemplate: "hx -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "hx -- {{filename}}:{{line}}", editAtLineAndWaitTemplate: "hx -- {{filename}}:{{line}}",
openDirInEditorTemplate: "hx -- {{dir}}",
editInTerminal: true, editInTerminal: true,
}, },
"vscode": { "vscode": {
editTemplate: "code --reuse-window -- {{filename}}", editTemplate: "code --reuse-window -- {{filename}}",
editAtLineTemplate: "code --reuse-window --goto -- {{filename}}:{{line}}", editAtLineTemplate: "code --reuse-window --goto -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "code --reuse-window --goto --wait -- {{filename}}:{{line}}", editAtLineAndWaitTemplate: "code --reuse-window --goto --wait -- {{filename}}:{{line}}",
openDirInEditorTemplate: "code -- {{dir}}",
editInTerminal: false, editInTerminal: false,
}, },
"sublime": { "sublime": {
editTemplate: "subl -- {{filename}}", editTemplate: "subl -- {{filename}}",
editAtLineTemplate: "subl -- {{filename}}:{{line}}", editAtLineTemplate: "subl -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "subl --wait -- {{filename}}:{{line}}", editAtLineAndWaitTemplate: "subl --wait -- {{filename}}:{{line}}",
openDirInEditorTemplate: "subl -- {{dir}}",
editInTerminal: false, editInTerminal: false,
}, },
"bbedit": { "bbedit": {
editTemplate: "bbedit -- {{filename}}", editTemplate: "bbedit -- {{filename}}",
editAtLineTemplate: "bbedit +{{line}} -- {{filename}}", editAtLineTemplate: "bbedit +{{line}} -- {{filename}}",
editAtLineAndWaitTemplate: "bbedit +{{line}} --wait -- {{filename}}", editAtLineAndWaitTemplate: "bbedit +{{line}} --wait -- {{filename}}",
openDirInEditorTemplate: "bbedit -- {{dir}}",
editInTerminal: false, editInTerminal: false,
}, },
"xcode": { "xcode": {
editTemplate: "xed -- {{filename}}", editTemplate: "xed -- {{filename}}",
editAtLineTemplate: "xed --line {{line}} -- {{filename}}", editAtLineTemplate: "xed --line {{line}} -- {{filename}}",
editAtLineAndWaitTemplate: "xed --line {{line}} --wait -- {{filename}}", editAtLineAndWaitTemplate: "xed --line {{line}} --wait -- {{filename}}",
openDirInEditorTemplate: "xed -- {{dir}}",
editInTerminal: false, editInTerminal: false,
}, },
} }
@ -107,6 +122,7 @@ func standardTerminalEditorPreset(editor string) *editPreset {
editTemplate: editor + " -- {{filename}}", editTemplate: editor + " -- {{filename}}",
editAtLineTemplate: editor + " +{{line}} -- {{filename}}", editAtLineTemplate: editor + " +{{line}} -- {{filename}}",
editAtLineAndWaitTemplate: editor + " +{{line}} -- {{filename}}", editAtLineAndWaitTemplate: editor + " +{{line}} -- {{filename}}",
openDirInEditorTemplate: editor + " -- {{dir}}",
editInTerminal: true, editInTerminal: true,
} }
} }

View File

@ -132,6 +132,7 @@ type KeybindingConfig struct {
Status KeybindingStatusConfig `yaml:"status"` Status KeybindingStatusConfig `yaml:"status"`
Files KeybindingFilesConfig `yaml:"files"` Files KeybindingFilesConfig `yaml:"files"`
Branches KeybindingBranchesConfig `yaml:"branches"` Branches KeybindingBranchesConfig `yaml:"branches"`
Worktrees KeybindingWorktreesConfig `yaml:"worktrees"`
Commits KeybindingCommitsConfig `yaml:"commits"` Commits KeybindingCommitsConfig `yaml:"commits"`
Stash KeybindingStashConfig `yaml:"stash"` Stash KeybindingStashConfig `yaml:"stash"`
CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"` CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"`
@ -246,6 +247,10 @@ type KeybindingBranchesConfig struct {
FetchRemote string `yaml:"fetchRemote"` FetchRemote string `yaml:"fetchRemote"`
} }
type KeybindingWorktreesConfig struct {
ViewWorktreeOptions string `yaml:"viewWorktreeOptions"`
}
type KeybindingCommitsConfig struct { type KeybindingCommitsConfig struct {
SquashDown string `yaml:"squashDown"` SquashDown string `yaml:"squashDown"`
RenameCommit string `yaml:"renameCommit"` RenameCommit string `yaml:"renameCommit"`
@ -313,6 +318,9 @@ type OSConfig struct {
// Pointer to bool so that we can distinguish unset (nil) from false. // Pointer to bool so that we can distinguish unset (nil) from false.
EditInTerminal *bool `yaml:"editInTerminal,omitempty"` EditInTerminal *bool `yaml:"editInTerminal,omitempty"`
// For opening a directory in an editor
OpenDirInEditor string `yaml:"openDirInEditor,omitempty"`
// A built-in preset that sets all of the above settings. Supported presets // A built-in preset that sets all of the above settings. Supported presets
// are defined in the getPreset function in editor_presets.go. // are defined in the getPreset function in editor_presets.go.
EditPreset string `yaml:"editPreset,omitempty"` EditPreset string `yaml:"editPreset,omitempty"`
@ -587,6 +595,9 @@ func GetDefaultConfig() *UserConfig {
SetUpstream: "u", SetUpstream: "u",
FetchRemote: "f", FetchRemote: "f",
}, },
Worktrees: KeybindingWorktreesConfig{
ViewWorktreeOptions: "w",
},
Commits: KeybindingCommitsConfig{ Commits: KeybindingCommitsConfig{
SquashDown: "s", SquashDown: "s",
RenameCommit: "r", RenameCommit: "r",

11
pkg/env/env.go vendored
View File

@ -10,19 +10,10 @@ func GetGitDirEnv() string {
return os.Getenv("GIT_DIR") return os.Getenv("GIT_DIR")
} }
func GetGitWorkTreeEnv() string {
return os.Getenv("GIT_WORK_TREE")
}
func SetGitDirEnv(value string) { func SetGitDirEnv(value string) {
os.Setenv("GIT_DIR", value) os.Setenv("GIT_DIR", value)
} }
func SetGitWorkTreeEnv(value string) { func UnsetGitDirEnv() {
os.Setenv("GIT_WORK_TREE", value)
}
func UnsetGitDirEnvs() {
_ = os.Unsetenv("GIT_DIR") _ = os.Unsetenv("GIT_DIR")
_ = os.Unsetenv("GIT_WORK_TREE")
} }

View File

@ -376,3 +376,16 @@ func (self *ContextMgr) AllPatchExplorer() []types.IPatchExplorerContext {
return listContexts return listContexts
} }
func (self *ContextMgr) ContextForKey(key types.ContextKey) types.Context {
self.RLock()
defer self.RUnlock()
for _, context := range self.allContexts.Flatten() {
if context.GetKey() == key {
return context
}
}
return nil
}

View File

@ -31,6 +31,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
c.Modes().Diffing.Ref, c.Modes().Diffing.Ref,
c.Tr, c.Tr,
c.UserConfig, c.UserConfig,
c.Model().Worktrees,
) )
} }

View File

@ -5,12 +5,16 @@ import (
) )
const ( const (
// used as a nil value when passing a context key as an arg
NO_CONTEXT types.ContextKey = "none"
GLOBAL_CONTEXT_KEY types.ContextKey = "global" GLOBAL_CONTEXT_KEY types.ContextKey = "global"
STATUS_CONTEXT_KEY types.ContextKey = "status" STATUS_CONTEXT_KEY types.ContextKey = "status"
SNAKE_CONTEXT_KEY types.ContextKey = "snake" SNAKE_CONTEXT_KEY types.ContextKey = "snake"
FILES_CONTEXT_KEY types.ContextKey = "files" FILES_CONTEXT_KEY types.ContextKey = "files"
LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches" LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches"
REMOTES_CONTEXT_KEY types.ContextKey = "remotes" REMOTES_CONTEXT_KEY types.ContextKey = "remotes"
WORKTREES_CONTEXT_KEY types.ContextKey = "worktrees"
REMOTE_BRANCHES_CONTEXT_KEY types.ContextKey = "remoteBranches" REMOTE_BRANCHES_CONTEXT_KEY types.ContextKey = "remoteBranches"
TAGS_CONTEXT_KEY types.ContextKey = "tags" TAGS_CONTEXT_KEY types.ContextKey = "tags"
LOCAL_COMMITS_CONTEXT_KEY types.ContextKey = "commits" LOCAL_COMMITS_CONTEXT_KEY types.ContextKey = "commits"
@ -49,6 +53,7 @@ var AllContextKeys = []types.ContextKey{
FILES_CONTEXT_KEY, FILES_CONTEXT_KEY,
LOCAL_BRANCHES_CONTEXT_KEY, LOCAL_BRANCHES_CONTEXT_KEY,
REMOTES_CONTEXT_KEY, REMOTES_CONTEXT_KEY,
WORKTREES_CONTEXT_KEY,
REMOTE_BRANCHES_CONTEXT_KEY, REMOTE_BRANCHES_CONTEXT_KEY,
TAGS_CONTEXT_KEY, TAGS_CONTEXT_KEY,
LOCAL_COMMITS_CONTEXT_KEY, LOCAL_COMMITS_CONTEXT_KEY,
@ -84,6 +89,7 @@ type ContextTree struct {
LocalCommits *LocalCommitsContext LocalCommits *LocalCommitsContext
CommitFiles *CommitFilesContext CommitFiles *CommitFilesContext
Remotes *RemotesContext Remotes *RemotesContext
Worktrees *WorktreesContext
Submodules *SubmodulesContext Submodules *SubmodulesContext
RemoteBranches *RemoteBranchesContext RemoteBranches *RemoteBranchesContext
ReflogCommits *ReflogCommitsContext ReflogCommits *ReflogCommitsContext
@ -118,6 +124,7 @@ func (self *ContextTree) Flatten() []types.Context {
self.Status, self.Status,
self.Snake, self.Snake,
self.Submodules, self.Submodules,
self.Worktrees,
self.Files, self.Files,
self.SubCommits, self.SubCommits,
self.Remotes, self.Remotes,

View File

@ -29,6 +29,7 @@ func NewContextTree(c *ContextCommon) *ContextTree {
Submodules: NewSubmodulesContext(c), Submodules: NewSubmodulesContext(c),
Menu: NewMenuContext(c), Menu: NewMenuContext(c),
Remotes: NewRemotesContext(c), Remotes: NewRemotesContext(c),
Worktrees: NewWorktreesContext(c),
RemoteBranches: NewRemoteBranchesContext(c), RemoteBranches: NewRemoteBranchesContext(c),
LocalCommits: NewLocalCommitsContext(c), LocalCommits: NewLocalCommitsContext(c),
CommitFiles: commitFilesContext, CommitFiles: commitFilesContext,

View File

@ -0,0 +1,55 @@
package context
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type WorktreesContext struct {
*FilteredListViewModel[*models.Worktree]
*ListContextTrait
}
var _ types.IListContext = (*WorktreesContext)(nil)
func NewWorktreesContext(c *ContextCommon) *WorktreesContext {
viewModel := NewFilteredListViewModel(
func() []*models.Worktree { return c.Model().Worktrees },
func(Worktree *models.Worktree) []string {
return []string{Worktree.Name}
},
)
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetWorktreeDisplayStrings(
c.Tr,
viewModel.GetFilteredList(),
)
}
return &WorktreesContext{
FilteredListViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Worktrees,
WindowName: "files",
Key: WORKTREES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT,
Focusable: true,
})),
list: viewModel,
getDisplayStrings: getDisplayStrings,
c: c,
},
}
}
func (self *WorktreesContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}

View File

@ -18,10 +18,13 @@ func (gui *Gui) Helpers() *helpers.Helpers {
func (gui *Gui) resetHelpersAndControllers() { func (gui *Gui) resetHelpersAndControllers() {
helperCommon := gui.c helperCommon := gui.c
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo)
refsHelper := helpers.NewRefsHelper(helperCommon) refsHelper := helpers.NewRefsHelper(helperCommon)
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon)
worktreeHelper := helpers.NewWorktreeHelper(helperCommon, reposHelper, refsHelper, suggestionsHelper)
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper) rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper)
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon)
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage }) setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription }) setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
@ -41,11 +44,20 @@ func (gui *Gui) resetHelpersAndControllers() {
gpgHelper := helpers.NewGpgHelper(helperCommon) gpgHelper := helpers.NewGpgHelper(helperCommon)
viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts) viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts)
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon) patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon)
stagingHelper := helpers.NewStagingHelper(helperCommon) stagingHelper := helpers.NewStagingHelper(helperCommon)
mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon) mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon)
refreshHelper := helpers.NewRefreshHelper(helperCommon, refsHelper, rebaseHelper, patchBuildingHelper, stagingHelper, mergeConflictsHelper, gui.fileWatcher)
refreshHelper := helpers.NewRefreshHelper(
helperCommon,
refsHelper,
rebaseHelper,
patchBuildingHelper,
stagingHelper,
mergeConflictsHelper,
worktreeHelper,
gui.fileWatcher,
)
diffHelper := helpers.NewDiffHelper(helperCommon) diffHelper := helpers.NewDiffHelper(helperCommon)
cherryPickHelper := helpers.NewCherryPickHelper( cherryPickHelper := helpers.NewCherryPickHelper(
helperCommon, helperCommon,
@ -84,7 +96,7 @@ func (gui *Gui) resetHelpersAndControllers() {
Commits: commitsHelper, Commits: commitsHelper,
Snake: helpers.NewSnakeHelper(helperCommon), Snake: helpers.NewSnakeHelper(helperCommon),
Diff: diffHelper, Diff: diffHelper,
Repos: helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo), Repos: reposHelper,
RecordDirectory: recordDirectoryHelper, RecordDirectory: recordDirectoryHelper,
Update: helpers.NewUpdateHelper(helperCommon, gui.Updater), Update: helpers.NewUpdateHelper(helperCommon, gui.Updater),
Window: windowHelper, Window: windowHelper,
@ -99,7 +111,8 @@ func (gui *Gui) resetHelpersAndControllers() {
modeHelper, modeHelper,
appStatusHelper, appStatusHelper,
), ),
Search: helpers.NewSearchHelper(helperCommon), Search: helpers.NewSearchHelper(helperCommon),
Worktree: worktreeHelper,
} }
gui.CustomCommandsClient = custom_commands.NewClient( gui.CustomCommandsClient = custom_commands.NewClient(
@ -138,6 +151,7 @@ func (gui *Gui) resetHelpersAndControllers() {
common, common,
func(branches []*models.RemoteBranch) { gui.State.Model.RemoteBranches = branches }, func(branches []*models.RemoteBranch) { gui.State.Model.RemoteBranches = branches },
) )
worktreesController := controllers.NewWorktreesController(common)
undoController := controllers.NewUndoController(common) undoController := controllers.NewUndoController(common)
globalController := controllers.NewGlobalController(common) globalController := controllers.NewGlobalController(common)
contextLinesController := controllers.NewContextLinesController(common) contextLinesController := controllers.NewContextLinesController(common)
@ -177,6 +191,7 @@ func (gui *Gui) resetHelpersAndControllers() {
for _, context := range []types.Context{ for _, context := range []types.Context{
gui.State.Contexts.Status, gui.State.Contexts.Status,
gui.State.Contexts.Remotes, gui.State.Contexts.Remotes,
gui.State.Contexts.Worktrees,
gui.State.Contexts.Tags, gui.State.Contexts.Tags,
gui.State.Contexts.Branches, gui.State.Contexts.Branches,
gui.State.Contexts.RemoteBranches, gui.State.Contexts.RemoteBranches,
@ -227,6 +242,18 @@ func (gui *Gui) resetHelpersAndControllers() {
controllers.AttachControllers(context, controllers.NewBasicCommitsController(common, context)) controllers.AttachControllers(context, controllers.NewBasicCommitsController(common, context))
} }
for _, context := range []controllers.CanViewWorktreeOptions{
gui.State.Contexts.LocalCommits,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.SubCommits,
gui.State.Contexts.Stash,
gui.State.Contexts.Branches,
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Tags,
} {
controllers.AttachControllers(context, controllers.NewWorktreeOptionsController(common, context))
}
controllers.AttachControllers(gui.State.Contexts.ReflogCommits, controllers.AttachControllers(gui.State.Contexts.ReflogCommits,
reflogCommitsController, reflogCommitsController,
) )
@ -298,6 +325,10 @@ func (gui *Gui) resetHelpersAndControllers() {
remotesController, remotesController,
) )
controllers.AttachControllers(gui.State.Contexts.Worktrees,
worktreesController,
)
controllers.AttachControllers(gui.State.Contexts.Stash, controllers.AttachControllers(gui.State.Contexts.Stash,
stashController, stashController,
) )

View File

@ -202,10 +202,33 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error {
return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch) return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch)
} }
worktreeForRef, ok := self.worktreeForBranch(selectedBranch)
if ok && !worktreeForRef.IsCurrent {
return self.promptToCheckoutWorktree(worktreeForRef)
}
self.c.LogAction(self.c.Tr.Actions.CheckoutBranch) self.c.LogAction(self.c.Tr.Actions.CheckoutBranch)
return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{}) return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{})
} }
func (self *BranchesController) worktreeForBranch(branch *models.Branch) (*models.Worktree, bool) {
return git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees)
}
func (self *BranchesController) promptToCheckoutWorktree(worktree *models.Worktree) error {
prompt := utils.ResolvePlaceholderString(self.c.Tr.AlreadyCheckedOutByWorktree, map[string]string{
"worktreeName": worktree.Name,
})
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.SwitchToWorktree,
Prompt: prompt,
HandleConfirm: func() error {
return self.c.Helpers().Worktree.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY)
},
})
}
func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error { func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error {
return self.createPullRequest(selectedBranch.Name, "") return self.createPullRequest(selectedBranch.Name, "")
} }
@ -298,9 +321,56 @@ func (self *BranchesController) delete(branch *models.Branch) error {
if checkedOutBranch.Name == branch.Name { if checkedOutBranch.Name == branch.Name {
return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch) return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch)
} }
if self.checkedOutByOtherWorktree(branch) {
return self.promptWorktreeBranchDelete(branch)
}
return self.deleteWithForce(branch, false) return self.deleteWithForce(branch, false)
} }
func (self *BranchesController) checkedOutByOtherWorktree(branch *models.Branch) bool {
return git_commands.CheckedOutByOtherWorktree(branch, self.c.Model().Worktrees)
}
func (self *BranchesController) promptWorktreeBranchDelete(selectedBranch *models.Branch) error {
worktree, ok := self.worktreeForBranch(selectedBranch)
if !ok {
self.c.Log.Error("promptWorktreeBranchDelete out of sync with list of worktrees")
return nil
}
// TODO: i18n
title := utils.ResolvePlaceholderString(self.c.Tr.BranchCheckedOutByWorktree, map[string]string{
"worktreeName": worktree.Name,
"branchName": selectedBranch.Name,
})
return self.c.Menu(types.CreateMenuOptions{
Title: title,
Items: []*types.MenuItem{
{
Label: self.c.Tr.SwitchToWorktree,
OnPress: func() error {
return self.c.Helpers().Worktree.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY)
},
},
{
Label: self.c.Tr.DetachWorktree,
Tooltip: self.c.Tr.DetachWorktreeTooltip,
OnPress: func() error {
return self.c.Helpers().Worktree.Detach(worktree)
},
},
{
Label: self.c.Tr.RemoveWorktree,
OnPress: func() error {
return self.c.Helpers().Worktree.Remove(worktree, false)
},
},
},
})
}
func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, force bool) error { func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, force bool) error {
title := self.c.Tr.DeleteBranch title := self.c.Tr.DeleteBranch
var templateStr string var templateStr string
@ -365,15 +435,23 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
) )
return self.c.WithLoaderPanel(message, func(task gocui.Task) error { return self.c.WithLoaderPanel(message, func(task gocui.Task) error {
if branch == self.c.Helpers().Refs.GetCheckedOutRef() { worktree, ok := self.worktreeForBranch(branch)
if ok {
self.c.LogAction(action) self.c.LogAction(action)
worktreeGitDir := ""
// if it is the current worktree path, no need to specify the path
if !worktree.IsCurrent {
worktreeGitDir = worktree.GitDir
}
err := self.c.Git().Sync.Pull( err := self.c.Git().Sync.Pull(
task, task,
git_commands.PullOptions{ git_commands.PullOptions{
RemoteName: branch.UpstreamRemote, RemoteName: branch.UpstreamRemote,
BranchName: branch.UpstreamBranch, BranchName: branch.UpstreamBranch,
FastForwardOnly: true, FastForwardOnly: true,
WorktreeGitDir: worktreeGitDir,
}, },
) )
if err != nil { if err != nil {
@ -383,7 +461,10 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
} else { } else {
self.c.LogAction(action) self.c.LogAction(action)
err := self.c.Git().Sync.FastForward(task, branch.Name, branch.UpstreamRemote, branch.UpstreamBranch)
err := self.c.Git().Sync.FastForward(
task, branch.Name, branch.UpstreamRemote, branch.UpstreamBranch,
)
if err != nil { if err != nil {
_ = self.c.Error(err) _ = self.c.Error(err)
} }
@ -414,7 +495,10 @@ func (self *BranchesController) rename(branch *models.Branch) error {
} }
// need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch // need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch
_ = self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.BRANCHES}}) _ = self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC,
Scope: []types.RefreshableView{types.BRANCHES, types.WORKTREES},
})
// now that we've got our stuff again we need to find that branch and reselect it. // now that we've got our stuff again we need to find that branch and reselect it.
for i, newBranch := range self.c.Model().Branches { for i, newBranch := range self.c.Model().Branches {

View File

@ -51,7 +51,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
if err := self.c.Git().WorkingTree.DiscardAllDirChanges(node); err != nil { if err := self.c.Git().WorkingTree.DiscardAllDirChanges(node); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
}, },
Key: 'x', Key: 'x',
Tooltip: utils.ResolvePlaceholderString( Tooltip: utils.ResolvePlaceholderString(
@ -72,7 +72,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
return self.c.Error(err) return self.c.Error(err)
} }
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
}, },
Key: 'u', Key: 'u',
Tooltip: utils.ResolvePlaceholderString( Tooltip: utils.ResolvePlaceholderString(
@ -107,7 +107,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
if err := self.c.Git().WorkingTree.DiscardAllFileChanges(file); err != nil { if err := self.c.Git().WorkingTree.DiscardAllFileChanges(file); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
}, },
Key: 'x', Key: 'x',
Tooltip: utils.ResolvePlaceholderString( Tooltip: utils.ResolvePlaceholderString(
@ -128,7 +128,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
return self.c.Error(err) return self.c.Error(err)
} }
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
}, },
Key: 'u', Key: 'u',
Tooltip: utils.ResolvePlaceholderString( Tooltip: utils.ResolvePlaceholderString(

View File

@ -37,6 +37,13 @@ func (self *FilesHelper) EditFileAtLineAndWait(filename string, lineNumber int)
return self.callEditor(cmdStr, true) return self.callEditor(cmdStr, true)
} }
func (self *FilesHelper) OpenDirInEditor(path string) error {
cmdStr := self.c.Git().File.GetOpenDirInEditorCmdStr(path)
// Not editing in terminal because surely that's not a thing.
return self.callEditor(cmdStr, false)
}
func (self *FilesHelper) callEditor(cmdStr string, editInTerminal bool) error { func (self *FilesHelper) callEditor(cmdStr string, editInTerminal bool) error {
if editInTerminal { if editInTerminal {
return self.c.RunSubprocessAndRefresh( return self.c.RunSubprocessAndRefresh(

View File

@ -47,6 +47,7 @@ type Helpers struct {
AppStatus *AppStatusHelper AppStatus *AppStatusHelper
WindowArrangement *WindowArrangementHelper WindowArrangement *WindowArrangementHelper
Search *SearchHelper Search *SearchHelper
Worktree *WorktreeHelper
} }
func NewStubHelpers() *Helpers { func NewStubHelpers() *Helpers {
@ -80,5 +81,6 @@ func NewStubHelpers() *Helpers {
AppStatus: &AppStatusHelper{}, AppStatus: &AppStatusHelper{},
WindowArrangement: &WindowArrangementHelper{}, WindowArrangement: &WindowArrangementHelper{},
Search: &SearchHelper{}, Search: &SearchHelper{},
Worktree: &WorktreeHelper{},
} }
} }

View File

@ -16,7 +16,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
"github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
@ -28,6 +27,7 @@ type RefreshHelper struct {
patchBuildingHelper *PatchBuildingHelper patchBuildingHelper *PatchBuildingHelper
stagingHelper *StagingHelper stagingHelper *StagingHelper
mergeConflictsHelper *MergeConflictsHelper mergeConflictsHelper *MergeConflictsHelper
worktreeHelper *WorktreeHelper
fileWatcher types.IFileWatcher fileWatcher types.IFileWatcher
} }
@ -38,6 +38,7 @@ func NewRefreshHelper(
patchBuildingHelper *PatchBuildingHelper, patchBuildingHelper *PatchBuildingHelper,
stagingHelper *StagingHelper, stagingHelper *StagingHelper,
mergeConflictsHelper *MergeConflictsHelper, mergeConflictsHelper *MergeConflictsHelper,
worktreeHelper *WorktreeHelper,
fileWatcher types.IFileWatcher, fileWatcher types.IFileWatcher,
) *RefreshHelper { ) *RefreshHelper {
return &RefreshHelper{ return &RefreshHelper{
@ -47,6 +48,7 @@ func NewRefreshHelper(
patchBuildingHelper: patchBuildingHelper, patchBuildingHelper: patchBuildingHelper,
stagingHelper: stagingHelper, stagingHelper: stagingHelper,
mergeConflictsHelper: mergeConflictsHelper, mergeConflictsHelper: mergeConflictsHelper,
worktreeHelper: worktreeHelper,
fileWatcher: fileWatcher, fileWatcher: fileWatcher,
} }
} }
@ -83,6 +85,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
types.REFLOG, types.REFLOG,
types.TAGS, types.TAGS,
types.REMOTES, types.REMOTES,
types.WORKTREES,
types.STATUS, types.STATUS,
types.BISECT_INFO, types.BISECT_INFO,
types.STAGING, types.STAGING,
@ -150,6 +153,10 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
refresh("remotes", func() { _ = self.refreshRemotes() }) refresh("remotes", func() { _ = self.refreshRemotes() })
} }
if scopeSet.Includes(types.WORKTREES) {
refresh("worktrees", func() { _ = self.refreshWorktrees() })
}
if scopeSet.Includes(types.STAGING) { if scopeSet.Includes(types.STAGING) {
refresh("staging", func() { refresh("staging", func() {
fileWg.Wait() fileWg.Wait()
@ -197,6 +204,7 @@ func getScopeNames(scopes []types.RefreshableView) []string {
types.REFLOG: "reflog", types.REFLOG: "reflog",
types.TAGS: "tags", types.TAGS: "tags",
types.REMOTES: "remotes", types.REMOTES: "remotes",
types.WORKTREES: "worktrees",
types.STATUS: "status", types.STATUS: "status",
types.BISECT_INFO: "bisect", types.BISECT_INFO: "bisect",
types.STAGING: "staging", types.STAGING: "staging",
@ -589,6 +597,25 @@ func (self *RefreshHelper) refreshRemotes() error {
return nil return nil
} }
func (self *RefreshHelper) refreshWorktrees() error {
worktrees, err := self.c.Git().Loaders.Worktrees.GetWorktrees()
if err != nil {
self.c.Log.Error(err)
self.c.Model().Worktrees = []*models.Worktree{}
return nil
}
self.c.Model().Worktrees = worktrees
// need to refresh branches because the branches view shows worktrees against
// branches
if err := self.c.PostRefreshUpdate(self.c.Contexts().Branches); err != nil {
return err
}
return self.c.PostRefreshUpdate(self.c.Contexts().Worktrees)
}
func (self *RefreshHelper) refreshStashEntries() error { func (self *RefreshHelper) refreshStashEntries() error {
self.c.Model().StashEntries = self.c.Git().Loaders.StashLoader. self.c.Model().StashEntries = self.c.Git().Loaders.StashLoader.
GetStashEntries(self.c.Modes().Filtering.GetPath()) GetStashEntries(self.c.Modes().Filtering.GetPath())
@ -606,20 +633,13 @@ func (self *RefreshHelper) refreshStatus() {
// need to wait for branches to refresh // need to wait for branches to refresh
return return
} }
status := ""
if currentBranch.IsRealBranch() {
status += presentation.ColoredBranchStatus(currentBranch, self.c.Tr) + " "
}
workingTreeState := self.c.Git().Status.WorkingTreeState() workingTreeState := self.c.Git().Status.WorkingTreeState()
if workingTreeState != enums.REBASE_MODE_NONE { linkedWorktreeName := self.worktreeHelper.GetLinkedWorktreeName()
status += style.FgYellow.Sprintf("(%s) ", presentation.FormatWorkingTreeStateLower(self.c.Tr, workingTreeState))
}
name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name) repoName := self.c.Git().RepoPaths.RepoName()
repoName := utils.GetCurrentRepoName()
status += fmt.Sprintf("%s → %s ", repoName, name) status := presentation.FormatStatus(repoName, currentBranch, linkedWorktreeName, workingTreeState, self.c.Tr)
self.c.SetViewContent(self.c.Views().Status, status) self.c.SetViewContent(self.c.Views().Status, status)
} }

View File

@ -8,17 +8,19 @@ import (
"sync" "sync"
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/gocui"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types" appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/env" "github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
type onNewRepoFn func(startArgs appTypes.StartArgs, reuseState bool) error type onNewRepoFn func(startArgs appTypes.StartArgs, contextKey types.ContextKey) error
// helps switch back and forth between repos // helps switch back and forth between repos
type ReposHelper struct { type ReposHelper struct {
@ -46,7 +48,7 @@ func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error
} }
self.c.State().GetRepoPathStack().Push(wd) self.c.State().GetRepoPathStack().Push(wd)
return self.DispatchSwitchToRepo(submodule.Path, true) return self.DispatchSwitchToRepo(submodule.Path, context.NO_CONTEXT)
} }
func (self *ReposHelper) getCurrentBranch(path string) string { func (self *ReposHelper) getCurrentBranch(path string) string {
@ -129,7 +131,7 @@ func (self *ReposHelper) CreateRecentReposMenu() error {
// if we were in a submodule, we want to forget about that stack of repos // if we were in a submodule, we want to forget about that stack of repos
// so that hitting escape in the new repo does nothing // so that hitting escape in the new repo does nothing
self.c.State().GetRepoPathStack().Clear() self.c.State().GetRepoPathStack().Clear()
return self.DispatchSwitchToRepo(path, false) return self.DispatchSwitchToRepo(path, context.NO_CONTEXT)
}, },
} }
}) })
@ -137,39 +139,48 @@ func (self *ReposHelper) CreateRecentReposMenu() error {
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.RecentRepos, Items: menuItems}) return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.RecentRepos, Items: menuItems})
} }
func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool) error { func (self *ReposHelper) DispatchSwitchToRepo(path string, contextKey types.ContextKey) error {
env.UnsetGitDirEnvs() return self.DispatchSwitchTo(path, self.c.Tr.ErrRepositoryMovedOrDeleted, contextKey)
originalPath, err := os.Getwd() }
if err != nil {
return nil
}
if err := os.Chdir(path); err != nil { func (self *ReposHelper) DispatchSwitchTo(path string, errMsg string, contextKey types.ContextKey) error {
if os.IsNotExist(err) { return self.c.WithWaitingStatus(self.c.Tr.Switching, func(gocui.Task) error {
return self.c.ErrorMsg(self.c.Tr.ErrRepositoryMovedOrDeleted) env.UnsetGitDirEnv()
originalPath, err := os.Getwd()
if err != nil {
return nil
} }
return err
}
if err := commands.VerifyInGitRepo(self.c.OS()); err != nil { msg := utils.ResolvePlaceholderString(self.c.Tr.ChangingDirectoryTo, map[string]string{"path": path})
if err := os.Chdir(originalPath); err != nil { self.c.LogCommand(msg, false)
if err := os.Chdir(path); err != nil {
if os.IsNotExist(err) {
return self.c.ErrorMsg(errMsg)
}
return err return err
} }
return err if err := commands.VerifyInGitRepo(self.c.OS()); err != nil {
} if err := os.Chdir(originalPath); err != nil {
return err
}
if err := self.recordDirectoryHelper.RecordCurrentDirectory(); err != nil { return err
return err }
}
// these two mutexes are used by our background goroutines (triggered via `self.goEvery`. We don't want to if err := self.recordDirectoryHelper.RecordCurrentDirectory(); err != nil {
// switch to a repo while one of these goroutines is in the process of updating something return err
self.c.Mutexes().SyncMutex.Lock() }
defer self.c.Mutexes().SyncMutex.Unlock()
self.c.Mutexes().RefreshingFilesMutex.Lock() // these two mutexes are used by our background goroutines (triggered via `self.goEvery`. We don't want to
defer self.c.Mutexes().RefreshingFilesMutex.Unlock() // switch to a repo while one of these goroutines is in the process of updating something
self.c.Mutexes().SyncMutex.Lock()
defer self.c.Mutexes().SyncMutex.Unlock()
return self.onNewRepo(appTypes.StartArgs{}, reuse) self.c.Mutexes().RefreshingFilesMutex.Lock()
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
return self.onNewRepo(appTypes.StartArgs{}, contextKey)
})
} }

View File

@ -8,7 +8,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
type IWorkingTreeHelper interface { type IWorkingTreeHelper interface {
@ -203,7 +202,7 @@ func (self *WorkingTreeHelper) prepareFilesForCommit() error {
} }
func (self *WorkingTreeHelper) commitPrefixConfigForRepo() *config.CommitPrefixConfig { func (self *WorkingTreeHelper) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
cfg, ok := self.c.UserConfig.Git.CommitPrefixes[utils.GetCurrentRepoName()] cfg, ok := self.c.UserConfig.Git.CommitPrefixes[self.c.Git().RepoPaths.RepoName()]
if !ok { if !ok {
return nil return nil
} }

View File

@ -0,0 +1,242 @@
package helpers
import (
"strings"
"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/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type IWorktreeHelper interface {
GetMainWorktreeName() string
GetCurrentWorktreeName() string
}
type WorktreeHelper struct {
c *HelperCommon
reposHelper *ReposHelper
refsHelper *RefsHelper
suggestionsHelper *SuggestionsHelper
}
func NewWorktreeHelper(c *HelperCommon, reposHelper *ReposHelper, refsHelper *RefsHelper, suggestionsHelper *SuggestionsHelper) *WorktreeHelper {
return &WorktreeHelper{
c: c,
reposHelper: reposHelper,
refsHelper: refsHelper,
suggestionsHelper: suggestionsHelper,
}
}
func (self *WorktreeHelper) GetMainWorktreeName() string {
for _, worktree := range self.c.Model().Worktrees {
if worktree.IsMain {
return worktree.Name
}
}
return ""
}
// If we're on the main worktree, we return an empty string
func (self *WorktreeHelper) GetLinkedWorktreeName() string {
worktrees := self.c.Model().Worktrees
if len(worktrees) == 0 {
return ""
}
// worktrees always have the current worktree on top
currentWorktree := worktrees[0]
if currentWorktree.IsMain {
return ""
}
return currentWorktree.Name
}
func (self *WorktreeHelper) NewWorktree() error {
branch := self.refsHelper.GetCheckedOutRef()
currentBranchName := branch.RefName()
f := func(detached bool) error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.NewWorktreeBase,
InitialContent: currentBranchName,
FindSuggestionsFunc: self.suggestionsHelper.GetRefsSuggestionsFunc(),
HandleConfirm: func(base string) error {
// we assume that the base can be checked out
canCheckoutBase := true
return self.NewWorktreeCheckout(base, canCheckoutBase, detached, context.WORKTREES_CONTEXT_KEY)
},
})
}
placeholders := map[string]string{"ref": "ref"}
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.WorktreeTitle,
Items: []*types.MenuItem{
{
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFrom, placeholders)},
OnPress: func() error {
return f(false)
},
},
{
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFromDetached, placeholders)},
OnPress: func() error {
return f(true)
},
},
},
})
}
func (self *WorktreeHelper) NewWorktreeCheckout(base string, canCheckoutBase bool, detached bool, contextKey types.ContextKey) error {
opts := git_commands.NewWorktreeOpts{
Base: base,
Detach: detached,
}
f := func() error {
return self.c.WithWaitingStatus(self.c.Tr.AddingWorktree, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.AddWorktree)
if err := self.c.Git().Worktree.New(opts); err != nil {
return err
}
return self.reposHelper.DispatchSwitchTo(opts.Path, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
})
}
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.NewWorktreePath,
HandleConfirm: func(path string) error {
opts.Path = path
if detached {
return f()
}
if canCheckoutBase {
title := utils.ResolvePlaceholderString(self.c.Tr.NewBranchNameLeaveBlank, map[string]string{"default": base})
// prompt for the new branch name where a blank means we just check out the branch
return self.c.Prompt(types.PromptOpts{
Title: title,
HandleConfirm: func(branchName string) error {
opts.Branch = branchName
return f()
},
})
} else {
// prompt for the new branch name where a blank means we just check out the branch
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.NewBranchName,
HandleConfirm: func(branchName string) error {
if branchName == "" {
return self.c.ErrorMsg(self.c.Tr.BranchNameCannotBeBlank)
}
opts.Branch = branchName
return f()
},
})
}
},
})
}
func (self *WorktreeHelper) Switch(worktree *models.Worktree, contextKey types.ContextKey) error {
if worktree.IsCurrent {
return self.c.ErrorMsg(self.c.Tr.AlreadyInWorktree)
}
self.c.LogAction(self.c.Tr.SwitchToWorktree)
return self.reposHelper.DispatchSwitchTo(worktree.Path, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
}
func (self *WorktreeHelper) Remove(worktree *models.Worktree, force bool) error {
title := self.c.Tr.RemoveWorktreeTitle
var templateStr string
if force {
templateStr = self.c.Tr.ForceRemoveWorktreePrompt
} else {
templateStr = self.c.Tr.RemoveWorktreePrompt
}
message := utils.ResolvePlaceholderString(
templateStr,
map[string]string{
"worktreeName": worktree.Name,
},
)
return self.c.Confirm(types.ConfirmOpts{
Title: title,
Prompt: message,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.RemovingWorktree, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.RemoveWorktree)
if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil {
errMessage := err.Error()
if !strings.Contains(errMessage, "--force") {
return self.c.Error(err)
}
if !force {
return self.Remove(worktree, true)
}
return self.c.ErrorMsg(errMessage)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
})
},
})
}
func (self *WorktreeHelper) Detach(worktree *models.Worktree) error {
return self.c.WithWaitingStatus(self.c.Tr.DetachingWorktree, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.RemovingWorktree)
err := self.c.Git().Worktree.Detach(worktree.Path)
if err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
})
}
func (self *WorktreeHelper) ViewWorktreeOptions(context types.IListContext, ref string) error {
currentBranch := self.refsHelper.GetCheckedOutRef()
canCheckoutBase := context == self.c.Contexts().Branches && ref != currentBranch.RefName()
return self.ViewBranchWorktreeOptions(ref, canCheckoutBase)
}
func (self *WorktreeHelper) ViewBranchWorktreeOptions(branchName string, canCheckoutBase bool) error {
placeholders := map[string]string{"ref": branchName}
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.WorktreeTitle,
Items: []*types.MenuItem{
{
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFrom, placeholders)},
OnPress: func() error {
return self.NewWorktreeCheckout(branchName, canCheckoutBase, false, context.LOCAL_BRANCHES_CONTEXT_KEY)
},
},
{
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFromDetached, placeholders)},
OnPress: func() error {
return self.NewWorktreeCheckout(branchName, canCheckoutBase, true, context.LOCAL_BRANCHES_CONTEXT_KEY)
},
},
},
})
}

View File

@ -2,6 +2,7 @@ package controllers
import ( import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -77,7 +78,7 @@ func (self *QuitActions) Escape() error {
repoPathStack := self.c.State().GetRepoPathStack() repoPathStack := self.c.State().GetRepoPathStack()
if !repoPathStack.IsEmpty() { if !repoPathStack.IsEmpty() {
return self.c.Helpers().Repos.DispatchSwitchToRepo(repoPathStack.Pop(), true) return self.c.Helpers().Repos.DispatchSwitchToRepo(repoPathStack.Pop(), context.NO_CONTEXT)
} }
if self.c.UserConfig.QuitOnTopLevelReturn { if self.c.UserConfig.QuitOnTopLevelReturn {

View File

@ -11,7 +11,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
type StatusController struct { type StatusController struct {
@ -108,7 +107,7 @@ func (self *StatusController) onClick() error {
cx, _ := self.c.Views().Status.Cursor() cx, _ := self.c.Views().Status.Cursor()
upstreamStatus := presentation.BranchStatus(currentBranch, self.c.Tr) upstreamStatus := presentation.BranchStatus(currentBranch, self.c.Tr)
repoName := utils.GetCurrentRepoName() repoName := self.c.Git().RepoPaths.RepoName()
workingTreeState := self.c.Git().Status.WorkingTreeState() workingTreeState := self.c.Git().Status.WorkingTreeState()
switch workingTreeState { switch workingTreeState {
case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING: case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING:

View File

@ -35,6 +35,11 @@ func (self *SubmodulesController) GetKeybindings(opts types.KeybindingsOpts) []*
Handler: self.checkSelected(self.enter), Handler: self.checkSelected(self.enter),
Description: self.c.Tr.EnterSubmodule, Description: self.c.Tr.EnterSubmodule,
}, },
{
Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelected(self.enter),
Description: self.c.Tr.EnterSubmodule,
},
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.remove), Handler: self.checkSelected(self.remove),

View File

@ -0,0 +1,59 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// This controller is for all contexts that have items you can create a worktree from
var _ types.IController = &WorktreeOptionsController{}
type CanViewWorktreeOptions interface {
types.IListContext
}
type WorktreeOptionsController struct {
baseController
c *ControllerCommon
context CanViewWorktreeOptions
}
func NewWorktreeOptionsController(controllerCommon *ControllerCommon, context CanViewWorktreeOptions) *WorktreeOptionsController {
return &WorktreeOptionsController{
baseController: baseController{},
c: controllerCommon,
context: context,
}
}
func (self *WorktreeOptionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Worktrees.ViewWorktreeOptions),
Handler: self.checkSelected(self.viewWorktreeOptions),
Description: self.c.Tr.ViewWorktreeOptions,
OpensMenu: true,
},
}
return bindings
}
func (self *WorktreeOptionsController) checkSelected(callback func(string) error) func() error {
return func() error {
ref := self.context.GetSelectedItemId()
if ref == "" {
return nil
}
return callback(ref)
}
}
func (self *WorktreeOptionsController) Context() types.Context {
return self.context
}
func (self *WorktreeOptionsController) viewWorktreeOptions(ref string) error {
return self.c.Helpers().Worktree.ViewWorktreeOptions(self.context, ref)
}

View File

@ -0,0 +1,144 @@
package controllers
import (
"fmt"
"strings"
"text/tabwriter"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type WorktreesController struct {
baseController
c *ControllerCommon
}
var _ types.IController = &WorktreesController{}
func NewWorktreesController(
common *ControllerCommon,
) *WorktreesController {
return &WorktreesController{
baseController: baseController{},
c: common,
}
}
func (self *WorktreesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.New),
Handler: self.add,
Description: self.c.Tr.CreateWorktree,
},
{
Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelected(self.enter),
Description: self.c.Tr.SwitchToWorktree,
},
{
Key: opts.GetKey(opts.Config.Universal.Confirm),
Handler: self.checkSelected(self.enter),
Description: self.c.Tr.SwitchToWorktree,
},
{
Key: opts.GetKey(opts.Config.Universal.OpenFile),
Handler: self.checkSelected(self.open),
Description: self.c.Tr.OpenInEditor,
},
{
Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.remove),
Description: self.c.Tr.RemoveWorktree,
},
}
return bindings
}
func (self *WorktreesController) GetOnRenderToMain() func() error {
return func() error {
var task types.UpdateTask
worktree := self.context().GetSelected()
if worktree == nil {
task = types.NewRenderStringTask(self.c.Tr.NoWorktreesThisRepo)
} else {
main := ""
if worktree.IsMain {
main = style.FgDefault.Sprintf(" %s", self.c.Tr.MainWorktree)
}
missing := ""
if worktree.IsPathMissing {
missing = style.FgRed.Sprintf(" %s", self.c.Tr.MissingWorktree)
}
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)
_, _ = 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()
task = types.NewRenderStringTask(builder.String())
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.WorktreeTitle,
Task: task,
},
})
}
}
func (self *WorktreesController) add() error {
return self.c.Helpers().Worktree.NewWorktree()
}
func (self *WorktreesController) remove(worktree *models.Worktree) error {
if worktree.IsMain {
return self.c.ErrorMsg(self.c.Tr.CantDeleteMainWorktree)
}
if worktree.IsCurrent {
return self.c.ErrorMsg(self.c.Tr.CantDeleteCurrentWorktree)
}
return self.c.Helpers().Worktree.Remove(worktree, false)
}
func (self *WorktreesController) GetOnClick() func() error {
return self.checkSelected(self.enter)
}
func (self *WorktreesController) enter(worktree *models.Worktree) error {
return self.c.Helpers().Worktree.Switch(worktree, context.WORKTREES_CONTEXT_KEY)
}
func (self *WorktreesController) open(worktree *models.Worktree) error {
return self.c.Helpers().Files.OpenDirInEditor(worktree.Path)
}
func (self *WorktreesController) checkSelected(callback func(worktree *models.Worktree) error) func() error {
return func() error {
worktree := self.context().GetSelected()
if worktree == nil {
return nil
}
return callback(worktree)
}
}
func (self *WorktreesController) Context() types.Context {
return self.context()
}
func (self *WorktreesController) context() *context.WorktreesContext {
return self.c.Contexts().Worktrees
}

View File

@ -64,7 +64,8 @@ type Gui struct {
CustomCommandsClient *custom_commands.Client CustomCommandsClient *custom_commands.Client
// this is a mapping of repos to gui states, so that we can restore the original // this is a mapping of repos to gui states, so that we can restore the original
// gui state when returning from a subrepo // gui state when returning from a subrepo.
// In repos with multiple worktrees, we store a separate repo state per worktree.
RepoStateMap map[Repo]*GuiRepoState RepoStateMap map[Repo]*GuiRepoState
Config config.AppConfigurer Config config.AppConfigurer
Updater *updates.Updater Updater *updates.Updater
@ -276,7 +277,7 @@ func (self *GuiRepoState) GetSplitMainPanel() bool {
return self.SplitMainPanel return self.SplitMainPanel
} }
func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error { func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.ContextKey) error {
var err error var err error
gui.git, err = commands.NewGitCommand( gui.git, err = commands.NewGitCommand(
gui.Common, gui.Common,
@ -289,7 +290,7 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
return err return err
} }
contextToPush := gui.resetState(startArgs, reuseState) contextToPush := gui.resetState(startArgs)
gui.resetHelpersAndControllers() gui.resetHelpersAndControllers()
@ -297,6 +298,17 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
return err return err
} }
// if a context key has been given, push that instead, and set its index to 0
if contextKey != context.NO_CONTEXT {
contextToPush = gui.c.ContextForKey(contextKey)
// when we pass a list context, the expectation is that our cursor goes to the top,
// because e.g. with worktrees, we'll show the current worktree at the top of the list.
listContext, ok := contextToPush.(types.IListContext)
if ok {
listContext.GetList().SetSelectedLineIdx(0)
}
}
if err := gui.c.PushContext(contextToPush); err != nil { if err := gui.c.PushContext(contextToPush); err != nil {
return err return err
} }
@ -313,26 +325,23 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
// it gets a bit confusing to land back in the status panel when visiting a repo // it gets a bit confusing to land back in the status panel when visiting a repo
// you've already switched from. There's no doubt some easy way to make the UX // you've already switched from. There's no doubt some easy way to make the UX
// optimal for all cases but I'm too lazy to think about what that is right now // optimal for all cases but I'm too lazy to think about what that is right now
func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types.Context { func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
currentDir, err := os.Getwd() worktreePath := gui.git.RepoPaths.WorktreePath()
if reuseState { if state := gui.RepoStateMap[Repo(worktreePath)]; state != nil {
if err == nil { gui.State = state
if state := gui.RepoStateMap[Repo(currentDir)]; state != nil { gui.State.ViewsSetup = false
gui.State = state
gui.State.ViewsSetup = false
// setting this to nil so we don't get stuck based on a popup that was contextTree := gui.State.Contexts
// previously opened gui.State.WindowViewNameMap = initialWindowViewNameMap(contextTree)
gui.Mutexes.PopupMutex.Lock()
gui.State.CurrentPopupOpts = nil
gui.Mutexes.PopupMutex.Unlock()
return gui.c.CurrentContext() // setting this to nil so we don't get stuck based on a popup that was
} // previously opened
} else { gui.Mutexes.PopupMutex.Lock()
gui.c.Log.Error(err) gui.State.CurrentPopupOpts = nil
} gui.Mutexes.PopupMutex.Unlock()
return gui.c.CurrentContext()
} }
contextTree := gui.contextTree() contextTree := gui.contextTree()
@ -340,6 +349,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types.
initialScreenMode := initialScreenMode(startArgs, gui.Config) initialScreenMode := initialScreenMode(startArgs, gui.Config)
gui.State = &GuiRepoState{ gui.State = &GuiRepoState{
ViewsSetup: false,
Model: &types.Model{ Model: &types.Model{
CommitFiles: nil, CommitFiles: nil,
Files: make([]*models.File, 0), Files: make([]*models.File, 0),
@ -364,7 +374,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types.
SearchState: types.NewSearchState(), SearchState: types.NewSearchState(),
} }
gui.RepoStateMap[Repo(currentDir)] = gui.State gui.RepoStateMap[Repo(worktreePath)] = gui.State
return initialContext(contextTree, startArgs) return initialContext(contextTree, startArgs)
} }
@ -555,7 +565,7 @@ func (gui *Gui) initGocui(headless bool, test integrationTypes.IntegrationTest)
} }
func (gui *Gui) viewTabMap() map[string][]context.TabView { func (gui *Gui) viewTabMap() map[string][]context.TabView {
return map[string][]context.TabView{ result := map[string][]context.TabView{
"branches": { "branches": {
{ {
Tab: gui.c.Tr.LocalBranchesTitle, Tab: gui.c.Tr.LocalBranchesTitle,
@ -585,12 +595,18 @@ func (gui *Gui) viewTabMap() map[string][]context.TabView {
Tab: gui.c.Tr.FilesTitle, Tab: gui.c.Tr.FilesTitle,
ViewName: "files", ViewName: "files",
}, },
context.TabView{
Tab: gui.c.Tr.WorktreesTitle,
ViewName: "worktrees",
},
{ {
Tab: gui.c.Tr.SubmodulesTitle, Tab: gui.c.Tr.SubmodulesTitle,
ViewName: "submodules", ViewName: "submodules",
}, },
}, },
} }
return result
} }
// Run: setup the gui with keybindings and start the mainloop // Run: setup the gui with keybindings and start the mainloop
@ -639,7 +655,7 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
} }
// onNewRepo must be called after g.SetManager because SetManager deletes keybindings // onNewRepo must be called after g.SetManager because SetManager deletes keybindings
if err := gui.onNewRepo(startArgs, false); err != nil { if err := gui.onNewRepo(startArgs, context.NO_CONTEXT); err != nil {
return err return err
} }

View File

@ -76,6 +76,10 @@ func (self *guiCommon) Context() types.IContextMgr {
return self.gui.State.ContextMgr return self.gui.State.ContextMgr
} }
func (self *guiCommon) ContextForKey(key types.ContextKey) types.Context {
return self.gui.State.ContextMgr.ContextForKey(key)
}
func (self *guiCommon) ActivateContext(context types.Context) error { func (self *guiCommon) ActivateContext(context types.Context) error {
return self.gui.State.ContextMgr.ActivateContext(context, types.OnFocusOpts{}) return self.gui.State.ContextMgr.ActivateContext(context, types.OnFocusOpts{})
} }

View File

@ -175,6 +175,10 @@ func (gui *Gui) prepareView(viewName string) (*gocui.View, error) {
} }
func (gui *Gui) onInitialViewsCreationForRepo() error { func (gui *Gui) onInitialViewsCreationForRepo() error {
if err := gui.onRepoViewReset(); err != nil {
return err
}
// hide any popup views. This only applies when we've just switched repos // hide any popup views. This only applies when we've just switched repos
for _, viewName := range gui.popupViewNames() { for _, viewName := range gui.popupViewNames() {
view, err := gui.g.View(viewName) view, err := gui.g.View(viewName)
@ -201,7 +205,7 @@ func (gui *Gui) popupViewNames() []string {
}) })
} }
func (gui *Gui) onInitialViewsCreation() error { func (gui *Gui) onRepoViewReset() error {
// now we order the views (in order of bottom first) // now we order the views (in order of bottom first)
for _, view := range gui.orderedViews() { for _, view := range gui.orderedViews() {
if _, err := gui.g.SetViewOnTop(view.Name()); err != nil { if _, err := gui.g.SetViewOnTop(view.Name()); err != nil {
@ -228,6 +232,10 @@ func (gui *Gui) onInitialViewsCreation() error {
} }
gui.g.Mutexes.ViewsMutex.Unlock() gui.g.Mutexes.ViewsMutex.Unlock()
return nil
}
func (gui *Gui) onInitialViewsCreation() error {
if !gui.c.UserConfig.DisableStartupPopups { if !gui.c.UserConfig.DisableStartupPopups {
storedPopupVersion := gui.c.GetAppState().StartupPopupVersion storedPopupVersion := gui.c.GetAppState().StartupPopupVersion
if storedPopupVersion < StartupPopupVersion { if storedPopupVersion < StartupPopupVersion {

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
@ -12,6 +13,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
) )
var branchPrefixColorCache = make(map[string]style.TextStyle) var branchPrefixColorCache = make(map[string]style.TextStyle)
@ -22,10 +24,11 @@ func GetBranchListDisplayStrings(
diffName string, diffName string,
tr *i18n.TranslationSet, tr *i18n.TranslationSet,
userConfig *config.UserConfig, userConfig *config.UserConfig,
worktrees []*models.Worktree,
) [][]string { ) [][]string {
return slices.Map(branches, func(branch *models.Branch) []string { return slices.Map(branches, func(branch *models.Branch) []string {
diffed := branch.Name == diffName diffed := branch.Name == diffName
return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig) return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig, worktrees)
}) })
} }
@ -36,6 +39,7 @@ func getBranchDisplayStrings(
diffed bool, diffed bool,
tr *i18n.TranslationSet, tr *i18n.TranslationSet,
userConfig *config.UserConfig, userConfig *config.UserConfig,
worktrees []*models.Worktree,
) []string { ) []string {
displayName := b.Name displayName := b.Name
if b.DisplayName != "" { if b.DisplayName != "" {
@ -49,6 +53,10 @@ func getBranchDisplayStrings(
coloredName := nameTextStyle.Sprint(displayName) coloredName := nameTextStyle.Sprint(displayName)
branchStatus := utils.WithPadding(ColoredBranchStatus(b, tr), 2, utils.AlignLeft) branchStatus := utils.WithPadding(ColoredBranchStatus(b, tr), 2, utils.AlignLeft)
if git_commands.CheckedOutByOtherWorktree(b, worktrees) {
worktreeIcon := lo.Ternary(icons.IsIconEnabled(), icons.LINKED_WORKTREE_ICON, fmt.Sprintf("(%s)", tr.LcWorktree))
coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon))
}
coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus) coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus)
recencyColor := style.FgCyan recencyColor := style.FgCyan
@ -58,6 +66,7 @@ func getBranchDisplayStrings(
res := make([]string, 0, 6) res := make([]string, 0, 6)
res = append(res, recencyColor.Sprint(b.Recency)) res = append(res, recencyColor.Sprint(b.Recency))
if icons.IsIconEnabled() { if icons.IsIconEnabled() {
res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b))) res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b)))
} }

View File

@ -155,10 +155,11 @@ func getFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, di
} }
isSubmodule := file != nil && file.IsSubmodule(submoduleConfigs) isSubmodule := file != nil && file.IsSubmodule(submoduleConfigs)
isLinkedWorktree := file != nil && file.IsWorktree
isDirectory := file == nil isDirectory := file == nil
if icons.IsIconEnabled() { if icons.IsIconEnabled() {
output += restColor.Sprintf("%s ", icons.IconForFile(name, isSubmodule, isDirectory)) output += restColor.Sprintf("%s ", icons.IconForFile(name, isSubmodule, isLinkedWorktree, isDirectory))
} }
output += restColor.Sprint(utils.EscapeSpecialChars(name)) output += restColor.Sprint(utils.EscapeSpecialChars(name))
@ -193,10 +194,11 @@ func getCommitFileLine(name string, diffName string, commitFile *models.CommitFi
} }
isSubmodule := false isSubmodule := false
isLinkedWorktree := false
isDirectory := commitFile == nil isDirectory := commitFile == nil
if icons.IsIconEnabled() { if icons.IsIconEnabled() {
output += colour.Sprintf("%s ", icons.IconForFile(name, isSubmodule, isDirectory)) output += colour.Sprintf("%s ", icons.IconForFile(name, isSubmodule, isLinkedWorktree, isDirectory))
} }
output += colour.Sprint(name) output += colour.Sprint(name)

View File

@ -323,7 +323,7 @@ func patchFileIconsForNerdFontsV2() {
extIconMap[".vue"] = "\ufd42" // ﵂ extIconMap[".vue"] = "\ufd42" // ﵂
} }
func IconForFile(name string, isSubmodule bool, isDirectory bool) string { func IconForFile(name string, isSubmodule bool, isLinkedWorktree bool, isDirectory bool) string {
base := filepath.Base(name) base := filepath.Base(name)
if icon, ok := nameIconMap[base]; ok { if icon, ok := nameIconMap[base]; ok {
return icon return icon
@ -336,6 +336,8 @@ func IconForFile(name string, isSubmodule bool, isDirectory bool) string {
if isSubmodule { if isSubmodule {
return DEFAULT_SUBMODULE_ICON return DEFAULT_SUBMODULE_ICON
} else if isLinkedWorktree {
return LINKED_WORKTREE_ICON
} else if isDirectory { } else if isDirectory {
return DEFAULT_DIRECTORY_ICON return DEFAULT_DIRECTORY_ICON
} }

View File

@ -7,13 +7,15 @@ import (
) )
var ( var (
BRANCH_ICON = "\U000f062c" // 󰘬 BRANCH_ICON = "\U000f062c" // 󰘬
DETACHED_HEAD_ICON = "\ue729" //  DETACHED_HEAD_ICON = "\ue729" // 
TAG_ICON = "\uf02b" //  TAG_ICON = "\uf02b" // 
COMMIT_ICON = "\U000f0718" // 󰜘 COMMIT_ICON = "\U000f0718" // 󰜘
MERGE_COMMIT_ICON = "\U000f062d" // 󰘭 MERGE_COMMIT_ICON = "\U000f062d" // 󰘭
DEFAULT_REMOTE_ICON = "\uf02a2" // 󰊢 DEFAULT_REMOTE_ICON = "\uf02a2" // 󰊢
STASH_ICON = "\uf01c" //  STASH_ICON = "\uf01c" // 
LINKED_WORKTREE_ICON = "\uf838" // 
MISSING_LINKED_WORKTREE_ICON = "\uf839" // 
) )
var remoteIcons = map[string]string{ var remoteIcons = map[string]string{
@ -68,3 +70,10 @@ func IconForRemote(remote *models.Remote) string {
func IconForStash(stash *models.StashEntry) string { func IconForStash(stash *models.StashEntry) string {
return STASH_ICON return STASH_ICON
} }
func IconForWorktree(missing bool) string {
if missing {
return MISSING_LINKED_WORKTREE_ICON
}
return LINKED_WORKTREE_ICON
}

View File

@ -0,0 +1,36 @@
package presentation
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/i18n"
)
func FormatStatus(repoName string, currentBranch *models.Branch, linkedWorktreeName string, workingTreeState enums.RebaseMode, tr *i18n.TranslationSet) string {
status := ""
if currentBranch.IsRealBranch() {
status += ColoredBranchStatus(currentBranch, tr) + " "
}
if workingTreeState != enums.REBASE_MODE_NONE {
status += style.FgYellow.Sprintf("(%s) ", FormatWorkingTreeStateLower(tr, workingTreeState))
}
name := GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
// If the user is in a linked worktree (i.e. not the main worktree) we'll display that
if linkedWorktreeName != "" {
icon := ""
if icons.IsIconEnabled() {
icon = icons.LINKED_WORKTREE_ICON + " "
}
repoName = fmt.Sprintf("%s(%s%s)", repoName, icon, style.FgCyan.Sprint(linkedWorktreeName))
}
status += fmt.Sprintf("%s → %s ", repoName, name)
return status
}

View File

@ -0,0 +1,51 @@
package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/samber/lo"
)
func GetWorktreeDisplayStrings(tr *i18n.TranslationSet, worktrees []*models.Worktree) [][]string {
return lo.Map(worktrees, func(worktree *models.Worktree, _ int) []string {
return GetWorktreeDisplayString(
tr,
worktree)
})
}
func GetWorktreeDisplayString(tr *i18n.TranslationSet, worktree *models.Worktree) []string {
textStyle := theme.DefaultTextColor
current := ""
currentColor := style.FgCyan
if worktree.IsCurrent {
current = " *"
currentColor = style.FgGreen
}
icon := icons.IconForWorktree(false)
if worktree.IsPathMissing {
textStyle = style.FgRed
icon = icons.IconForWorktree(true)
}
res := []string{}
res = append(res, currentColor.Sprint(current))
if icons.IsIconEnabled() {
res = append(res, textStyle.Sprint(icon))
}
name := worktree.Name
if worktree.IsMain {
name += " " + tr.MainWorktree
}
if worktree.IsPathMissing && !icons.IsIconEnabled() {
name += " " + tr.MissingWorktree
}
res = append(res, textStyle.Sprint(name))
return res
}

View File

@ -28,6 +28,8 @@ func (gui *Gui) updateRecentRepoList() error {
} }
known, recentRepos := newRecentReposList(recentRepos, currentRepo) known, recentRepos := newRecentReposList(recentRepos, currentRepo)
gui.IsNewRepo = known gui.IsNewRepo = known
// TODO: migrate this file to use forward slashes on all OSes for consistency
// (windows uses backslashes at the moment)
gui.c.GetAppState().RecentRepos = recentRepos gui.c.GetAppState().RecentRepos = recentRepos
return gui.c.SaveAppState() return gui.c.SaveAppState()
} }

View File

@ -33,6 +33,7 @@ type SessionState struct {
SelectedStashEntry *models.StashEntry SelectedStashEntry *models.StashEntry
SelectedCommitFile *models.CommitFile SelectedCommitFile *models.CommitFile
SelectedCommitFilePath string SelectedCommitFilePath string
SelectedWorktree *models.Worktree
CheckedOutBranch *models.Branch CheckedOutBranch *models.Branch
} }
@ -50,6 +51,7 @@ func (self *SessionStateLoader) call() *SessionState {
SelectedCommitFile: self.c.Contexts().CommitFiles.GetSelectedFile(), SelectedCommitFile: self.c.Contexts().CommitFiles.GetSelectedFile(),
SelectedCommitFilePath: self.c.Contexts().CommitFiles.GetSelectedPath(), SelectedCommitFilePath: self.c.Contexts().CommitFiles.GetSelectedPath(),
SelectedSubCommit: self.c.Contexts().SubCommits.GetSelected(), SelectedSubCommit: self.c.Contexts().SubCommits.GetSelected(),
SelectedWorktree: self.c.Contexts().Worktrees.GetSelected(),
CheckedOutBranch: self.refsHelper.GetCheckedOutRef(), CheckedOutBranch: self.refsHelper.GetCheckedOutRef(),
} }
} }

View File

@ -66,6 +66,7 @@ type IGuiCommon interface {
IsCurrentContext(Context) bool IsCurrentContext(Context) bool
// TODO: replace the above context-based methods with just using Context() e.g. replace PushContext() with Context().Push() // TODO: replace the above context-based methods with just using Context() e.g. replace PushContext() with Context().Push()
Context() IContextMgr Context() IContextMgr
ContextForKey(key ContextKey) Context
ActivateContext(context Context) error ActivateContext(context Context) error
@ -201,6 +202,7 @@ type Model struct {
StashEntries []*models.StashEntry StashEntries []*models.StashEntry
SubCommits []*models.Commit SubCommits []*models.Commit
Remotes []*models.Remote Remotes []*models.Remote
Worktrees []*models.Worktree
// FilteredReflogCommits are the ones that appear in the reflog panel. // FilteredReflogCommits are the ones that appear in the reflog panel.
// when in filtering mode we only include the ones that match the given path // when in filtering mode we only include the ones that match the given path

View File

@ -13,6 +13,7 @@ const (
REFLOG REFLOG
TAGS TAGS
REMOTES REMOTES
WORKTREES
STATUS STATUS
SUBMODULES SUBMODULES
STAGING STAGING

View File

@ -8,6 +8,7 @@ type Views struct {
Files *gocui.View Files *gocui.View
Branches *gocui.View Branches *gocui.View
Remotes *gocui.View Remotes *gocui.View
Worktrees *gocui.View
Tags *gocui.View Tags *gocui.View
RemoteBranches *gocui.View RemoteBranches *gocui.View
ReflogCommits *gocui.View ReflogCommits *gocui.View

View File

@ -29,6 +29,7 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
{viewPtr: &gui.Views.Files, name: "files"}, {viewPtr: &gui.Views.Files, name: "files"},
{viewPtr: &gui.Views.Tags, name: "tags"}, {viewPtr: &gui.Views.Tags, name: "tags"},
{viewPtr: &gui.Views.Remotes, name: "remotes"}, {viewPtr: &gui.Views.Remotes, name: "remotes"},
{viewPtr: &gui.Views.Worktrees, name: "worktrees"},
{viewPtr: &gui.Views.Branches, name: "localBranches"}, {viewPtr: &gui.Views.Branches, name: "localBranches"},
{viewPtr: &gui.Views.RemoteBranches, name: "remoteBranches"}, {viewPtr: &gui.Views.RemoteBranches, name: "remoteBranches"},
{viewPtr: &gui.Views.ReflogCommits, name: "reflogCommits"}, {viewPtr: &gui.Views.ReflogCommits, name: "reflogCommits"},
@ -113,6 +114,8 @@ func (gui *Gui) createAllViews() error {
gui.Views.Remotes.Title = gui.c.Tr.RemotesTitle gui.Views.Remotes.Title = gui.c.Tr.RemotesTitle
gui.Views.Worktrees.Title = gui.c.Tr.WorktreesTitle
gui.Views.Tags.Title = gui.c.Tr.TagsTitle gui.Views.Tags.Title = gui.c.Tr.TagsTitle
gui.Views.Files.Title = gui.c.Tr.FilesTitle gui.Views.Files.Title = gui.c.Tr.FilesTitle

View File

@ -156,6 +156,7 @@ type TranslationSet struct {
GitconfigParseErr string GitconfigParseErr string
EditFile string EditFile string
OpenFile string OpenFile string
OpenInEditor string
IgnoreFile string IgnoreFile string
ExcludeFile string ExcludeFile string
RefreshFiles string RefreshFiles string
@ -481,6 +482,7 @@ type TranslationSet struct {
ErrCannotEditDirectory string ErrCannotEditDirectory string
ErrStageDirWithInlineMergeConflicts string ErrStageDirWithInlineMergeConflicts string
ErrRepositoryMovedOrDeleted string ErrRepositoryMovedOrDeleted string
ErrWorktreeMovedOrRemoved string
CommandLog string CommandLog string
ToggleShowCommandLog string ToggleShowCommandLog string
FocusCommandLog string FocusCommandLog string
@ -541,6 +543,41 @@ type TranslationSet struct {
FilterPrefix string FilterPrefix string
ExitSearchMode string ExitSearchMode string
ExitTextFilterMode string ExitTextFilterMode string
SwitchToWorktree string
AlreadyCheckedOutByWorktree string
BranchCheckedOutByWorktree string
DetachWorktreeTooltip string
Switching string
RemoveWorktree string
RemoveWorktreeTitle string
DetachWorktree string
DetachingWorktree string
WorktreesTitle string
WorktreeTitle string
RemoveWorktreePrompt string
ForceRemoveWorktreePrompt string
RemovingWorktree string
AddingWorktree string
CantDeleteCurrentWorktree string
AlreadyInWorktree string
CantDeleteMainWorktree string
NoWorktreesThisRepo string
MissingWorktree string
MainWorktree string
CreateWorktree string
NewWorktreePath string
NewWorktreeBase string
BranchNameCannotBeBlank string
NewBranchName string
NewBranchNameLeaveBlank string
ViewWorktreeOptions string
CreateWorktreeFrom string
CreateWorktreeFromDetached string
LcWorktree string
ChangingDirectoryTo string
Name string
Branch string
Path string
Actions Actions Actions Actions
Bisect Bisect Bisect Bisect
} }
@ -671,6 +708,8 @@ type Actions struct {
ResetBisect string ResetBisect string
BisectSkip string BisectSkip string
BisectMark string BisectMark string
RemoveWorktree string
AddWorktree string
} }
const englishIntroPopupMessage = ` const englishIntroPopupMessage = `
@ -854,6 +893,7 @@ func EnglishTranslationSet() TranslationSet {
GitconfigParseErr: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`, GitconfigParseErr: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`,
EditFile: `Edit file`, EditFile: `Edit file`,
OpenFile: `Open file`, OpenFile: `Open file`,
OpenInEditor: "Open in editor",
IgnoreFile: `Add to .gitignore`, IgnoreFile: `Add to .gitignore`,
ExcludeFile: `Add to .git/info/exclude`, ExcludeFile: `Add to .git/info/exclude`,
RefreshFiles: `Refresh files`, RefreshFiles: `Refresh files`,
@ -1181,6 +1221,7 @@ func EnglishTranslationSet() TranslationSet {
ErrStageDirWithInlineMergeConflicts: "Cannot stage/unstage directory containing files with inline merge conflicts. Please fix up the merge conflicts first", ErrStageDirWithInlineMergeConflicts: "Cannot stage/unstage directory containing files with inline merge conflicts. Please fix up the merge conflicts first",
ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯", ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯",
CommandLog: "Command log", CommandLog: "Command log",
ErrWorktreeMovedOrRemoved: "Cannot find worktree. It might have been moved or removed ¯\\_(ツ)_/¯",
ToggleShowCommandLog: "Toggle show/hide command log", ToggleShowCommandLog: "Toggle show/hide command log",
FocusCommandLog: "Focus command log", FocusCommandLog: "Focus command log",
CommandLogHeader: "You can hide/focus this panel by pressing '%s'\n", CommandLogHeader: "You can hide/focus this panel by pressing '%s'\n",
@ -1239,6 +1280,41 @@ func EnglishTranslationSet() TranslationSet {
SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode", SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode",
SearchPrefix: "Search: ", SearchPrefix: "Search: ",
FilterPrefix: "Filter: ", FilterPrefix: "Filter: ",
WorktreesTitle: "Worktrees",
WorktreeTitle: "Worktree",
SwitchToWorktree: "Switch to worktree",
AlreadyCheckedOutByWorktree: "This branch is checked out by worktree {{.worktreeName}}. Do you want to switch to that worktree?",
BranchCheckedOutByWorktree: "Branch {{.branchName}} is checked out by worktree {{.worktreeName}}",
DetachWorktreeTooltip: "This will run `git checkout --detach` on the worktree so that it stops hogging the branch, but the worktree's working tree will be left alone",
Switching: "Switching",
RemoveWorktree: "Remove worktree",
RemoveWorktreeTitle: "Remove worktree",
RemoveWorktreePrompt: "Are you sure you want to remove worktree '{{.worktreeName}}'?",
ForceRemoveWorktreePrompt: "'{{.worktreeName}}' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?",
RemovingWorktree: "Deleting worktree",
DetachWorktree: "Detach worktree",
DetachingWorktree: "Detaching worktree",
AddingWorktree: "Adding worktree",
CantDeleteCurrentWorktree: "You cannot remove the current worktree!",
AlreadyInWorktree: "You are already in the selected worktree",
CantDeleteMainWorktree: "You cannot remove the main worktree!",
NoWorktreesThisRepo: "No worktrees",
MissingWorktree: "(missing)",
MainWorktree: "(main)",
CreateWorktree: "Create worktree",
NewWorktreePath: "New worktree path",
NewWorktreeBase: "New worktree base ref",
BranchNameCannotBeBlank: "Branch name cannot be blank",
NewBranchName: "New branch name",
NewBranchNameLeaveBlank: "New branch name (leave blank to checkout {{.default}})",
ViewWorktreeOptions: "View worktree options",
CreateWorktreeFrom: "Create worktree from {{.ref}}",
CreateWorktreeFromDetached: "Create worktree from {{.ref}} (detached)",
LcWorktree: "worktree",
ChangingDirectoryTo: "Changing directory to {{.path}}",
Name: "Name",
Branch: "Branch",
Path: "Path",
Actions: Actions{ Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) // TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit", CheckoutCommit: "Checkout commit",
@ -1346,6 +1422,8 @@ func EnglishTranslationSet() TranslationSet {
ResetBisect: "Reset bisect", ResetBisect: "Reset bisect",
BisectSkip: "Bisect skip", BisectSkip: "Bisect skip",
BisectMark: "Bisect mark", BisectMark: "Bisect mark",
RemoveWorktree: "Remove worktree",
AddWorktree: "Add worktree",
}, },
Bisect: Bisect{ Bisect: Bisect{
Mark: "Mark current commit (%s) as %s", Mark: "Mark current commit (%s) as %s",

View File

@ -82,3 +82,12 @@ If you need to share test logic across test directories you can put helper funct
### Don't do too much in one test ### Don't do too much in one test
If you're testing different pieces of functionality, it's better to test them in isolation using multiple short tests, compared to one larger longer test. Sometimes it's appropriate to have a longer test which tests how various different pieces interact, but err on the side of keeping things short. If you're testing different pieces of functionality, it's better to test them in isolation using multiple short tests, compared to one larger longer test. Sometimes it's appropriate to have a longer test which tests how various different pieces interact, but err on the side of keeping things short.
## Testing against old git versions
Our CI tests against multiple git versions. If your test fails on an old version, then to troubleshoot you'll need to install the failing git version. One option is to use [rtx](https://github.com/jdxcode/rtx) (see installation steps in the readme) with the git plugin like so:
```sh
rtx plugin add git
rtx install git 2.20.0
rtx local git 2.20.0
```

View File

@ -85,7 +85,7 @@ func (self *Shell) CreateFile(path string, content string) *Shell {
func (self *Shell) DeleteFile(path string) *Shell { func (self *Shell) DeleteFile(path string) *Shell {
fullPath := filepath.Join(self.dir, path) fullPath := filepath.Join(self.dir, path)
err := os.Remove(fullPath) err := os.RemoveAll(fullPath)
if err != nil { if err != nil {
self.fail(fmt.Sprintf("error deleting file: %s\n%s", fullPath, err)) self.fail(fmt.Sprintf("error deleting file: %s\n%s", fullPath, err))
} }
@ -254,6 +254,30 @@ func (self *Shell) Init() *Shell {
return self return self
} }
func (self *Shell) AddWorktree(base string, path string, newBranchName string) *Shell {
return self.RunCommand([]string{
"git", "worktree", "add", "-b",
newBranchName, path, base,
})
}
// add worktree and have it checkout the base branch
func (self *Shell) AddWorktreeCheckout(base string, path string) *Shell {
return self.RunCommand([]string{
"git", "worktree", "add", path, base,
})
}
func (self *Shell) AddFileInWorktree(worktreePath string) *Shell {
self.CreateFile(filepath.Join(worktreePath, "content"), "content")
self.RunCommand([]string{
"git", "-C", worktreePath, "add", "content",
})
return self
}
func (self *Shell) MakeExecutable(path string) *Shell { func (self *Shell) MakeExecutable(path string) *Shell {
// 0755 sets the executable permission for owner, and read/execute permissions for group and others // 0755 sets the executable permission for owner, and read/execute permissions for group and others
err := os.Chmod(filepath.Join(self.dir, path), 0o755) err := os.Chmod(filepath.Join(self.dir, path), 0o755)
@ -304,3 +328,11 @@ func (self *Shell) CopyFile(source string, destination string) *Shell {
return self return self
} }
// NOTE: this only takes effect before running the test;
// the test will still run in the original directory
func (self *Shell) Chdir(path string) *Shell {
self.dir = filepath.Join(self.dir, path)
return self
}

View File

@ -8,7 +8,6 @@ import (
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/env"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
@ -170,11 +169,12 @@ func (self *IntegrationTest) SetupRepo(shell *Shell) {
} }
func (self *IntegrationTest) Run(gui integrationTypes.GuiDriver) { func (self *IntegrationTest) Run(gui integrationTypes.GuiDriver) {
// we pass the --pass arg to lazygit when running an integration test, and that pwd, err := os.Getwd()
// ends up stored in the following env var if err != nil {
repoPath := env.GetGitWorkTreeEnv() panic(err)
}
shell := NewShell(repoPath, func(errorMsg string) { gui.Fail(errorMsg) }) shell := NewShell(pwd, func(errorMsg string) { gui.Fail(errorMsg) })
keys := gui.Keys() keys := gui.Keys()
testDriver := NewTestDriver(gui, shell, keys, KeyPressDelay()) testDriver := NewTestDriver(gui, shell, keys, KeyPressDelay())

View File

@ -330,7 +330,7 @@ func (self *ViewDriver) Focus() *ViewDriver {
} }
windows := []window{ windows := []window{
{name: "status", viewNames: []string{"status"}}, {name: "status", viewNames: []string{"status"}},
{name: "files", viewNames: []string{"files", "submodules"}}, {name: "files", viewNames: []string{"files", "worktrees", "submodules"}},
{name: "branches", viewNames: []string{"localBranches", "remotes", "tags"}}, {name: "branches", viewNames: []string{"localBranches", "remotes", "tags"}},
{name: "commits", viewNames: []string{"commits", "reflogCommits"}}, {name: "commits", viewNames: []string{"commits", "reflogCommits"}},
{name: "stash", viewNames: []string{"stash"}}, {name: "stash", viewNames: []string{"stash"}},

View File

@ -129,6 +129,10 @@ func (self *Views) Files() *ViewDriver {
return self.regularView("files") return self.regularView("files")
} }
func (self *Views) Worktrees() *ViewDriver {
return self.regularView("worktrees")
}
func (self *Views) Status() *ViewDriver { func (self *Views) Status() *ViewDriver {
return self.regularView("status") return self.regularView("status")
} }

View File

@ -26,6 +26,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/integration/tests/tag" "github.com/jesseduffield/lazygit/pkg/integration/tests/tag"
"github.com/jesseduffield/lazygit/pkg/integration/tests/ui" "github.com/jesseduffield/lazygit/pkg/integration/tests/ui"
"github.com/jesseduffield/lazygit/pkg/integration/tests/undo" "github.com/jesseduffield/lazygit/pkg/integration/tests/undo"
"github.com/jesseduffield/lazygit/pkg/integration/tests/worktree"
) )
var tests = []*components.IntegrationTest{ var tests = []*components.IntegrationTest{
@ -219,4 +220,18 @@ var tests = []*components.IntegrationTest{
ui.SwitchTabFromMenu, ui.SwitchTabFromMenu,
undo.UndoCheckoutAndDrop, undo.UndoCheckoutAndDrop,
undo.UndoDrop, undo.UndoDrop,
worktree.AddFromBranch,
worktree.AddFromBranchDetached,
worktree.AddFromCommit,
worktree.AssociateBranchBisect,
worktree.AssociateBranchRebase,
worktree.BareRepo,
worktree.Crud,
worktree.CustomCommand,
worktree.DetachWorktreeFromBranch,
worktree.FastForwardWorktreeBranch,
worktree.ForceRemoveWorktree,
worktree.RemoveWorktreeFromBranch,
worktree.ResetWindowTabs,
worktree.WorktreeInRepo,
} }

View File

@ -20,6 +20,6 @@ var SwitchTabFromMenu = NewIntegrationTest(NewIntegrationTestArgs{
Select(Contains("Next tab")). Select(Contains("Next tab")).
Confirm() Confirm()
t.Views().Submodules().IsFocused() t.Views().Worktrees().IsFocused()
}, },
}) })

View File

@ -0,0 +1,65 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var AddFromBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Add a worktree via the branches view, then switch back to the main worktree via the branches view",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("mybranch"),
).
Press(keys.Worktrees.ViewWorktreeOptions).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Worktree")).
Select(Contains(`Create worktree from mybranch`).DoesNotContain("detached")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New worktree path")).
Type("../linked-worktree").
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New branch name")).
Type("newbranch").
Confirm()
}).
// confirm we're still focused on the branches view
IsFocused().
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree)"),
).
NavigateToLine(Contains("mybranch")).
Press(keys.Universal.Select).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Switch to worktree")).
Content(Equals("This branch is checked out by worktree repo. Do you want to switch to that worktree?")).
Confirm()
}).
Lines(
Contains("mybranch").IsSelected(),
Contains("newbranch (worktree)"),
).
// Confirm the files view is still showing in the files window
Press(keys.Universal.PrevBlock)
t.Views().Files().
IsFocused()
},
})

View File

@ -0,0 +1,46 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var AddFromBranchDetached = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Add a detached worktree via the branches view",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("mybranch"),
).
Press(keys.Worktrees.ViewWorktreeOptions).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Worktree")).
Select(Contains(`Create worktree from mybranch (detached)`)).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New worktree path")).
Type("../linked-worktree").
Confirm()
}).
// confirm we're still focused on the branches view
IsFocused().
Lines(
Contains("(no branch)").IsSelected(),
Contains("mybranch (worktree)"),
)
t.Views().Status().
Content(Contains("repo(linked-worktree)"))
},
})

View File

@ -0,0 +1,56 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var AddFromCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Add a worktree via the commits view",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.EmptyCommit("commit two")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("commit two").IsSelected(),
Contains("initial commit"),
).
NavigateToLine(Contains("initial commit")).
Press(keys.Worktrees.ViewWorktreeOptions).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Worktree")).
Select(MatchesRegexp(`Create worktree from .*`).DoesNotContain("detached")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New worktree path")).
Type("../linked-worktree").
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New branch name")).
Type("newbranch").
Confirm()
}).
Lines(
Contains("initial commit"),
)
// Confirm we're now in the branches view
t.Views().Branches().
IsFocused().
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree)"),
)
},
})

View File

@ -0,0 +1,88 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
// This is important because `git worktree list` will show a worktree being in a detached head state (which is true)
// when it's in the middle of a bisect, but it won't tell you about the branch it's on.
// Even so, if you attempt to check out that branch from another worktree git won't let you, so we need to
// keep track of the association ourselves.
// not bothering to test the linked worktree here because it's the same logic as the rebase test
var AssociateBranchBisect = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that when you start a bisect in a linked worktree, Lazygit still associates the worktree with the branch",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.EmptyCommit("commit 2")
shell.EmptyCommit("commit 3")
shell.AddWorktree("mybranch", "../linked-worktree", "newbranch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("mybranch").IsSelected(),
Contains("newbranch (worktree)"),
)
// start a bisect on the main worktree
t.Views().Commits().
Focus().
SelectedLine(Contains("commit 3")).
Press(keys.Commits.ViewBisectOptions).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Bisect")).
Select(MatchesRegexp(`Mark .* as bad`)).
Confirm()
t.Views().Information().Content(Contains("Bisecting"))
}).
NavigateToLine(Contains("initial commit")).
Press(keys.Commits.ViewBisectOptions).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Bisect")).
Select(MatchesRegexp(`Mark .* as good`)).
Confirm()
})
t.Views().Branches().
Focus().
// switch to linked worktree
NavigateToLine(Contains("newbranch")).
Press(keys.Universal.Select).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Switch to worktree")).
Content(Equals("This branch is checked out by worktree linked-worktree. Do you want to switch to that worktree?")).
Confirm()
t.Views().Information().Content(DoesNotContain("Bisecting"))
}).
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree)"),
)
// switch back to main worktree
t.Views().Branches().
Focus().
NavigateToLine(Contains("mybranch")).
Press(keys.Universal.Select).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Switch to worktree")).
Content(Equals("This branch is checked out by worktree repo. Do you want to switch to that worktree?")).
Confirm()
})
},
})

View File

@ -0,0 +1,89 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
// This is important because `git worktree list` will show a worktree being in a detached head state (which is true)
// when it's in the middle of a rebase, but it won't tell you about the branch it's on.
// Even so, if you attempt to check out that branch from another worktree git won't let you, so we need to
// keep track of the association ourselves.
// We need different logic for associated the branch depending on whether it's a main worktree or
// linked worktree, so this test handles both.
var AssociateBranchRebase = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that when you start a rebase in a linked or main worktree, Lazygit still associates the worktree with the branch",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.EmptyCommit("commit 2")
shell.EmptyCommit("commit 3")
shell.AddWorktree("mybranch", "../linked-worktree", "newbranch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("mybranch").IsSelected(),
Contains("newbranch (worktree)"),
)
// start a rebase on the main worktree
t.Views().Commits().
Focus().
NavigateToLine(Contains("commit 2")).
Press(keys.Universal.Edit)
t.Views().Information().Content(Contains("Rebasing"))
t.Views().Branches().
Focus().
// switch to linked worktree
NavigateToLine(Contains("newbranch")).
Press(keys.Universal.Select).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Switch to worktree")).
Content(Equals("This branch is checked out by worktree linked-worktree. Do you want to switch to that worktree?")).
Confirm()
t.Views().Information().Content(DoesNotContain("Rebasing"))
}).
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree)"),
)
// start a rebase on the linked worktree
t.Views().Commits().
Focus().
NavigateToLine(Contains("commit 2")).
Press(keys.Universal.Edit)
t.Views().Information().Content(Contains("Rebasing"))
// switch back to main worktree
t.Views().Branches().
Focus().
NavigateToLine(Contains("mybranch")).
Press(keys.Universal.Select).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Switch to worktree")).
Content(Equals("This branch is checked out by worktree repo. Do you want to switch to that worktree?")).
Confirm()
}).
Lines(
Contains("(no branch").IsSelected(),
Contains("mybranch"),
// even though the linked worktree is rebasing, we still associate it with the branch
Contains("newbranch (worktree)"),
)
},
})

View File

@ -0,0 +1,102 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var BareRepo = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Open lazygit in the worktree of a bare repo and do a rebase/bisect",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
// we're going to have a directory structure like this:
// project
// - .bare
// - repo (a worktree)
// - worktree2 (another worktree)
//
// The first repo is called 'repo' because that's the
// directory that all lazygit tests start in
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("blah", "blah")
shell.Commit("initial commit")
shell.EmptyCommit("commit two")
shell.EmptyCommit("commit three")
shell.RunCommand([]string{"git", "clone", "--bare", ".", "../.bare"})
shell.DeleteFile(".git")
shell.Chdir("..")
// This is the dir we were just in (and the dir that lazygit starts in when the test runs)
// We're going to replace it with a worktree
shell.DeleteFile("repo")
shell.RunCommand([]string{"git", "--git-dir", ".bare", "worktree", "add", "-b", "repo", "repo", "mybranch"})
shell.RunCommand([]string{"git", "--git-dir", ".bare", "worktree", "add", "-b", "worktree2", "worktree2", "mybranch"})
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Lines(
Contains("repo"),
Contains("mybranch"),
Contains("worktree2 (worktree)"),
)
// test that a rebase works fine
// (rebase uses the git dir of the worktree so we're confirming that it points
// to the right git dir)
t.Views().Commits().
Focus().
Lines(
Contains("commit three").IsSelected(),
Contains("commit two"),
Contains("initial commit"),
).
Press(keys.Commits.MoveDownCommit).
Lines(
Contains("commit two"),
Contains("commit three").IsSelected(),
Contains("initial commit"),
).
// test that bisect works fine (same logic as above)
NavigateToLine(Contains("commit two")).
Press(keys.Commits.ViewBisectOptions).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Bisect")).
Select(MatchesRegexp(`Mark .* as bad`)).
Confirm()
t.Views().Information().Content(Contains("Bisecting"))
}).
NavigateToLine(Contains("initial commit")).
Press(keys.Commits.ViewBisectOptions).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Bisect")).
Select(MatchesRegexp(`Mark .* as good`)).
Confirm()
t.Views().Information().Content(Contains("Bisecting"))
})
// switch to other worktree
t.Views().Worktrees().
Focus().
Lines(
Contains("repo").IsSelected(),
Contains("worktree2"),
).
NavigateToLine(Contains("worktree2")).
Press(keys.Universal.Select).
Lines(
Contains("worktree2").IsSelected(),
Contains("repo"),
)
},
})

View File

@ -0,0 +1,120 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var Crud = NewIntegrationTest(NewIntegrationTestArgs{
Description: "From the worktrees view, add a work tree, switch to it, switch back, and remove it",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Lines(
Contains("mybranch"),
)
t.Views().Status().
Lines(
Contains("repo → mybranch"),
)
t.Views().Worktrees().
Focus().
Lines(
Contains("repo (main)"),
).
Press(keys.Universal.New).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Worktree")).
Select(Contains(`Create worktree from ref`).DoesNotContain(("detached"))).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New worktree base ref")).
InitialText(Equals("mybranch")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New worktree path")).
Type("../linked-worktree").
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New branch name (leave blank to checkout mybranch)")).
Type("newbranch").
Confirm()
}).
Lines(
Contains("linked-worktree").IsSelected(),
Contains("repo (main)"),
).
// confirm we're still in the same view
IsFocused()
// status panel includes the worktree if it's a linked worktree
t.Views().Status().
Lines(
Contains("repo(linked-worktree) → newbranch"),
)
t.Views().Branches().
Lines(
Contains("newbranch"),
Contains("mybranch"),
)
t.Views().Worktrees().
// confirm we can't remove the current worktree
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Equals("You cannot remove the current worktree!")).
Confirm()
}).
// confirm we cannot remove the main worktree
NavigateToLine(Contains("repo (main)")).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Equals("You cannot remove the main worktree!")).
Confirm()
}).
// switch back to main worktree
Press(keys.Universal.Select).
Lines(
Contains("repo (main)").IsSelected(),
Contains("linked-worktree"),
)
t.Views().Branches().
Lines(
Contains("mybranch"),
Contains("newbranch"),
)
t.Views().Worktrees().
// remove linked worktree
NavigateToLine(Contains("linked-worktree")).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Remove worktree")).
Content(Contains("Are you sure you want to remove worktree 'linked-worktree'?")).
Confirm()
}).
Lines(
Contains("repo (main)").IsSelected(),
)
},
})

View File

@ -0,0 +1,40 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CustomCommand = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that custom commands work with worktrees by deleting a worktree via a custom command",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.UserConfig.CustomCommands = []config.CustomCommand{
{
Key: "d",
Context: "worktrees",
Command: "git worktree remove {{ .SelectedWorktree.Path | quote }}",
},
}
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.AddWorktree("mybranch", "../linked-worktree", "newbranch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Worktrees().
Focus().
Lines(
Contains("repo (main)"),
Contains("linked-worktree"),
).
NavigateToLine(Contains("linked-worktree")).
Press("d").
Lines(
Contains("repo (main)"),
)
},
})

View File

@ -0,0 +1,48 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DetachWorktreeFromBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Detach a worktree from the branches view",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.EmptyCommit("commit 2")
shell.EmptyCommit("commit 3")
shell.AddWorktree("mybranch", "../linked-worktree", "newbranch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("mybranch").IsSelected(),
Contains("newbranch (worktree)"),
).
NavigateToLine(Contains("newbranch")).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Branch newbranch is checked out by worktree linked-worktree")).
Select(Equals("Detach worktree")).
Confirm()
}).
Lines(
Contains("mybranch"),
Contains("newbranch").DoesNotContain("(worktree)").IsSelected(),
)
t.Views().Worktrees().
Focus().
Lines(
Contains("repo (main)").IsSelected(),
Contains("linked-worktree"),
)
},
})

View File

@ -0,0 +1,52 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FastForwardWorktreeBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Fast-forward a linked worktree branch from another worktree",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
// both main and linked worktree will have changed to fast-foward
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.EmptyCommit("two")
shell.EmptyCommit("three")
shell.NewBranch("newbranch")
shell.CloneIntoRemote("origin")
shell.SetBranchUpstream("mybranch", "origin/mybranch")
shell.SetBranchUpstream("newbranch", "origin/newbranch")
// remove the 'three' commit so that we have something to pull from the remote
shell.HardReset("HEAD^")
shell.Checkout("mybranch")
shell.HardReset("HEAD^")
shell.AddWorktreeCheckout("newbranch", "../linked-worktree")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("mybranch").Contains("↓1").IsSelected(),
Contains("newbranch (worktree)").Contains("↓1"),
).
Press(keys.Branches.FastForward).
Lines(
Contains("mybranch").Contains("✓").IsSelected(),
Contains("newbranch (worktree)").Contains("↓1"),
).
NavigateToLine(Contains("newbranch (worktree)")).
Press(keys.Branches.FastForward).
Lines(
Contains("mybranch").Contains("✓"),
Contains("newbranch (worktree)").Contains("✓").IsSelected(),
)
},
})

View File

@ -0,0 +1,46 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ForceRemoveWorktree = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Force remove a dirty worktree",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.EmptyCommit("commit 2")
shell.EmptyCommit("commit 3")
shell.AddWorktree("mybranch", "../linked-worktree", "newbranch")
shell.AddFileInWorktree("../linked-worktree")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Worktrees().
Focus().
Lines(
Contains("repo (main)").IsSelected(),
Contains("linked-worktree"),
).
NavigateToLine(Contains("linked-worktree")).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Remove worktree")).
Content(Equals("Are you sure you want to remove worktree 'linked-worktree'?")).
Confirm()
t.ExpectPopup().Confirmation().
Title(Equals("Remove worktree")).
Content(Equals("'linked-worktree' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?")).
Confirm()
}).
Lines(
Contains("repo (main)").IsSelected(),
)
},
})

View File

@ -0,0 +1,58 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var RemoveWorktreeFromBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Remove a worktree from the branches view",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.EmptyCommit("commit 2")
shell.EmptyCommit("commit 3")
shell.AddWorktree("mybranch", "../linked-worktree", "newbranch")
shell.AddFileInWorktree("../linked-worktree")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("mybranch").IsSelected(),
Contains("newbranch (worktree)"),
).
NavigateToLine(Contains("newbranch")).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Branch newbranch is checked out by worktree linked-worktree")).
Select(Equals("Remove worktree")).
Confirm()
t.ExpectPopup().Confirmation().
Title(Equals("Remove worktree")).
Content(Equals("Are you sure you want to remove worktree 'linked-worktree'?")).
Confirm()
t.ExpectPopup().Confirmation().
Title(Equals("Remove worktree")).
Content(Equals("'linked-worktree' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?")).
Confirm()
}).
Lines(
Contains("mybranch"),
Contains("newbranch").DoesNotContain("(worktree)").IsSelected(),
)
t.Views().Worktrees().
Focus().
Lines(
Contains("repo (main)").IsSelected(),
)
},
})

View File

@ -0,0 +1,52 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
// This is verifying logic that is subject to change (we're just doing the easiest approach)
// There are two other UX flows we could have:
// 1) associate window tab states with the repo, so that when you switch back to a repo you get the same window tab states
// 2) retain the same window tab states when switching repos
// Option 1 is straightforward, but option 2 is harder because you'd need to deactivate any views containing dependent
// content e.g. the sub-commits view.
var ResetWindowTabs = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that window tabs are reset whenever switching repos",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.EmptyCommit("commit 2")
shell.EmptyCommit("commit 3")
shell.AddWorktree("mybranch", "../linked-worktree", "newbranch")
shell.AddFileInWorktree("../linked-worktree")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
// focus the remotes tab i.e. the second tab in the branches window
t.Views().Remotes().
Focus()
t.Views().Worktrees().
Focus().
Lines(
Contains("repo (main)").IsSelected(),
Contains("linked-worktree"),
).
NavigateToLine(Contains("linked-worktree")).
Press(keys.Universal.Select).
Lines(
Contains("linked-worktree").IsSelected(),
Contains("repo (main)"),
).
// navigate back to the branches window
Press(keys.Universal.NextBlock)
t.Views().Branches().
IsFocused()
},
})

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