diff --git a/docs-master/Config.md b/docs-master/Config.md index 5e57df34a..152044be6 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -527,6 +527,15 @@ git: # to 40 to disable truncation. truncateCopiedCommitHashesTo: 12 +# Config relating to git worktrees +worktree: + # Default parent directory for new worktrees. It is offered as a candidate + # location alongside the parent directories of any worktrees you already have. + # A relative path is resolved against the repository's root directory, so + # "../worktrees" sits beside the repo and ".worktrees" sits inside it. + # A leading "~" is expanded to your home directory, so "~/worktrees" works. + defaultPath: "" + # Periodic update checks update: # One of: 'prompt' (default) | 'background' | 'never' @@ -681,6 +690,7 @@ keybinding: confirmInEditor: [, ] remove: d new: "n" + newWorktree: w edit: e openFile: o scrollUpMain: [, K, ] @@ -761,8 +771,6 @@ keybinding: fetchRemote: f addForkRemote: F sortOrder: s - worktrees: - viewWorktreeOptions: w commits: squashDown: s renameCommit: r diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index b105b8108..4aa202740 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -112,13 +112,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Open commit in browser | | | `` n `` | Create new branch off of commit | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | View files | | -| `` w `` | View worktree options | | | `` / `` | Search the current view by text | | ## Confirmation panel @@ -178,6 +178,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Checkout | Checkout selected item. | | `` n `` | New branch | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` o `` | Create pull request | | | `` O `` | View create pull request options | | | `` G `` | Open pull request in browser | | @@ -197,7 +198,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | View commits | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Main panel (merging) @@ -282,6 +282,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Open commit in browser | | | `` n `` | Create new branch off of commit | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Reset copied (cherry-picked) commits selection | | @@ -289,7 +290,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | View commits | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Remote branches @@ -299,6 +299,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Copy branch name to clipboard | | | `` `` | Checkout | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. | | `` n `` | New branch | | +| `` w `` | New worktree | | | `` M `` | Merge | View options for merging the selected item into the current branch (regular merge, squash merge) | | `` r `` | Rebase | Rebase the checked-out branch onto the selected branch. | | `` d `` | Delete | Delete the remote branch from the remote. | @@ -308,7 +309,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | View commits | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Remotes @@ -339,10 +339,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. | | `` d `` | Drop | Remove the stash entry from the stash list. | | `` n `` | New branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. | +| `` w `` | New worktree | | | `` r `` | Rename stash | | | `` 0 `` | Focus main view | | | `` `` | View files | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Status @@ -366,6 +366,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Open commit in browser | | | `` n `` | Create new branch off of commit | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Reset copied (cherry-picked) commits selection | | @@ -373,7 +374,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | View files | | -| `` w `` | View worktree options | | | `` / `` | Search the current view by text | | ## Submodules @@ -397,13 +397,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Copy tag to clipboard | | | `` `` | Checkout | Checkout the selected tag as a detached HEAD. | | `` n `` | New tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. | +| `` w `` | New worktree | | | `` d `` | Delete | View delete options for local/remote tag. | | `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. | | `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | View commits | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Worktrees diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index a31d93d1a..5b9c798a1 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -92,13 +92,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | ブラウザでコミットを開く | | | `` n `` | コミットから新しいブランチを作成 | | | `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | 新しいワークツリー | | | `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:
- ソフトリセット:変更を保持し、ステージされた状態にします
- ミックスリセット:変更を保持し、ステージされていない状態にします
- ハードリセット:すべての変更を破棄します | | `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `` を押して選択をキャンセルできます。 | | `` `` | 外部差分ツールを開く(git difftool) | | | `` * `` | 現在のブランチのコミットを選択 | | | `` 0 `` | メインビューにフォーカス | | | `` `` | ファイルを表示 | | -| `` w `` | ワークツリーオプションを表示 | | | `` / `` | 現在のビューをテキストで検索 | | ## コミットファイル @@ -138,6 +138,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | ブラウザでコミットを開く | | | `` n `` | コミットから新しいブランチを作成 | | | `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | 新しいワークツリー | | | `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:
- ソフトリセット:変更を保持し、ステージされた状態にします
- ミックスリセット:変更を保持し、ステージされていない状態にします
- ハードリセット:すべての変更を破棄します | | `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `` を押して選択をキャンセルできます。 | | `` `` | コピーされた(チェリーピックされた)コミットの選択をリセット | | @@ -145,7 +146,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | 現在のブランチのコミットを選択 | | | `` 0 `` | メインビューにフォーカス | | | `` `` | ファイルを表示 | | -| `` w `` | ワークツリーオプションを表示 | | | `` / `` | 現在のビューをテキストで検索 | | ## サブモジュール @@ -170,10 +170,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` g `` | ポップ | スタッシュエントリをワーキングディレクトリに適用し、スタッシュエントリを削除します。 | | `` d `` | 削除 | スタッシュリストからスタッシュエントリを削除します。 | | `` n `` | 新しいブランチ | 選択したスタッシュエントリから新しいブランチを作成します。これは、スタッシュエントリが作成されたコミットをgitがチェックアウトし、そのコミットから新しいブランチを作成した後、スタッシュエントリを追加のコミットとして新しいブランチに適用することで機能します。 | +| `` w `` | 新しいワークツリー | | | `` r `` | スタッシュの名前を変更 | | | `` 0 `` | メインビューにフォーカス | | | `` `` | ファイルを表示 | | -| `` w `` | ワークツリーオプションを表示 | | | `` / `` | 現在のビューをテキストでフィルタリング | | ## ステータス @@ -202,13 +202,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | タグをクリップボードにコピー | | | `` `` | チェックアウト(ブランチの切り替え) | 選択したタグをデタッチドHEADとしてチェックアウトします。 | | `` n `` | 新しいタグを作成 | 現在のコミットから新しいタグを作成します。タグ名とオプションの説明を入力するよう促されます。 | +| `` w `` | 新しいワークツリー | | | `` d `` | 削除 | ローカル/リモートタグの削除オプションを表示します。 | | `` P `` | タグをプッシュ | 選択したタグをリモートにプッシュします。リモートを選択するよう促されます。 | | `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:
- ソフトリセット:変更を保持し、ステージされた状態にします
- ミックスリセット:変更を保持し、ステージされていない状態にします
- ハードリセット:すべての変更を破棄します | | `` `` | 外部差分ツールを開く(git difftool) | | | `` 0 `` | メインビューにフォーカス | | | `` `` | コミットを表示 | | -| `` w `` | ワークツリーオプションを表示 | | | `` / `` | 現在のビューをテキストでフィルタリング | | ## ファイル @@ -326,6 +326,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | ブラウザでコミットを開く | | | `` n `` | コミットから新しいブランチを作成 | | | `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | 新しいワークツリー | | | `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:
- ソフトリセット:変更を保持し、ステージされた状態にします
- ミックスリセット:変更を保持し、ステージされていない状態にします
- ハードリセット:すべての変更を破棄します | | `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `` を押して選択をキャンセルできます。 | | `` `` | コピーされた(チェリーピックされた)コミットの選択をリセット | | @@ -333,7 +334,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | 現在のブランチのコミットを選択 | | | `` 0 `` | メインビューにフォーカス | | | `` `` | コミットを表示 | | -| `` w `` | ワークツリーオプションを表示 | | | `` / `` | 現在のビューをテキストでフィルタリング | | ## リモート @@ -355,6 +355,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | ブランチ名をクリップボードにコピー | | | `` `` | チェックアウト(ブランチの切り替え) | 選択したリモートブランチに基づいて新しいローカルブランチをチェックアウトするか、リモートブランチをデタッチドヘッドとしてチェックアウトします。 | | `` n `` | 新しいブランチ | | +| `` w `` | 新しいワークツリー | | | `` M `` | マージ | 選択した項目を現在のブランチにマージするためのオプションを表示します(通常のマージ、スカッシュマージ) | | `` r `` | リベース | チェックアウトしたブランチを選択したブランチ上にリベースします。 | | `` d `` | 削除 | リモートからリモートブランチを削除します。 | @@ -364,7 +365,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 外部差分ツールを開く(git difftool) | | | `` 0 `` | メインビューにフォーカス | | | `` `` | コミットを表示 | | -| `` w `` | ワークツリーオプションを表示 | | | `` / `` | 現在のビューをテキストでフィルタリング | | ## ローカルブランチ @@ -376,6 +376,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | チェックアウト(ブランチの切り替え) | 選択した項目をチェックアウトします。 | | `` n `` | 新しいブランチ | | | `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | 新しいワークツリー | | | `` o `` | プルリクエストを作成 | | | `` O `` | プルリクエスト作成オプションを表示 | | | `` G `` | Open pull request in browser | | @@ -395,7 +396,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 外部差分ツールを開く(git difftool) | | | `` 0 `` | メインビューにフォーカス | | | `` `` | コミットを表示 | | -| `` w `` | ワークツリーオプションを表示 | | | `` / `` | 現在のビューをテキストでフィルタリング | | ## ワークツリー diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index ebf943868..d1eb3afb1 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -69,6 +69,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | 브라우저에서 커밋 열기 | | | `` n `` | 커밋에서 새 브랜치를 만듭니다. | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Reset cherry-picked (copied) commits selection | | @@ -76,7 +77,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | 커밋 보기 | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Secondary @@ -95,10 +95,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. | | `` d `` | Drop | Remove the stash entry from the stash list. | | `` n `` | 새 브랜치 생성 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. | +| `` w `` | New worktree | | | `` r `` | Rename stash | | | `` 0 `` | Focus main view | | | `` `` | View selected item's files | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Sub-commits @@ -111,6 +111,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | 브라우저에서 커밋 열기 | | | `` n `` | 커밋에서 새 브랜치를 만듭니다. | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Reset cherry-picked (copied) commits selection | | @@ -118,7 +119,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | View selected item's files | | -| `` w `` | View worktree options | | | `` / `` | 검색 시작 | | ## Worktrees @@ -212,6 +212,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 체크아웃 | Checkout selected item. | | `` n `` | 새 브랜치 생성 | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` o `` | 풀 리퀘스트 생성 | | | `` O `` | 풀 리퀘스트 생성 옵션 | | | `` G `` | Open pull request in browser | | @@ -231,7 +232,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | 커밋 보기 | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## 상태 @@ -278,6 +278,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 브랜치명을 클립보드에 복사 | | | `` `` | 체크아웃 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. | | `` n `` | 새 브랜치 생성 | | +| `` w `` | New worktree | | | `` M `` | 현재 브랜치에 병합 | View options for merging the selected item into the current branch (regular merge, squash merge) | | `` r `` | 체크아웃된 브랜치를 이 브랜치에 리베이스 | Rebase the checked-out branch onto the selected branch. | | `` d `` | 삭제 | Delete the remote branch from the remote. | @@ -287,7 +288,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | 커밋 보기 | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## 커밋 @@ -323,13 +323,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | 브라우저에서 커밋 열기 | | | `` n `` | 커밋에서 새 브랜치를 만듭니다. | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | View selected item's files | | -| `` w `` | View worktree options | | | `` / `` | 검색 시작 | | ## 커밋 파일 @@ -366,13 +366,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Copy tag to clipboard | | | `` `` | 체크아웃 | Checkout the selected tag as a detached HEAD. | | `` n `` | 태그를 생성 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. | +| `` w `` | New worktree | | | `` d `` | 삭제 | View delete options for local/remote tag. | | `` P `` | 태그를 push | Push the selected tag to a remote. You'll be prompted to select a remote. | | `` g `` | 초기화 | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | 커밋 보기 | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## 파일 diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index a7d7b09ac..7f1216b5f 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -102,6 +102,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Uitchecken | Checkout selected item. | | `` n `` | Nieuwe branch | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` o `` | Maak een pull-request | | | `` O `` | Bekijk opties voor pull-aanvraag | | | `` G `` | Open pull request in browser | | @@ -121,7 +122,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | Bekijk commits | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Commit bericht @@ -184,13 +184,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Open commit in browser | | | `` n `` | Creëer nieuwe branch van commit | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | Bekijk gecommite bestanden | | -| `` w `` | View worktree options | | | `` / `` | Start met zoeken | | ## Input prompt @@ -260,6 +260,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Open commit in browser | | | `` n `` | Creëer nieuwe branch van commit | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Reset cherry-picked (gekopieerde) commits selectie | | @@ -267,7 +268,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | Bekijk commits | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Remote branches @@ -277,6 +277,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Kopieer branch name naar klembord | | | `` `` | Uitchecken | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. | | `` n `` | Nieuwe branch | | +| `` w `` | New worktree | | | `` M `` | Merge in met huidige checked out branch | View options for merging the selected item into the current branch (regular merge, squash merge) | | `` r `` | Rebase branch | Rebase the checked-out branch onto the selected branch. | | `` d `` | Delete | Delete the remote branch from the remote. | @@ -286,7 +287,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | Bekijk commits | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Remotes @@ -339,10 +339,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. | | `` d `` | Laten vallen | Remove the stash entry from the stash list. | | `` n `` | Nieuwe branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. | +| `` w `` | New worktree | | | `` r `` | Rename stash | | | `` 0 `` | Focus main view | | | `` `` | Bekijk gecommite bestanden | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Status @@ -366,6 +366,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Open commit in browser | | | `` n `` | Creëer nieuwe branch van commit | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Reset cherry-picked (gekopieerde) commits selectie | | @@ -373,7 +374,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | Bekijk gecommite bestanden | | -| `` w `` | View worktree options | | | `` / `` | Start met zoeken | | ## Submodules @@ -397,13 +397,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Copy tag to clipboard | | | `` `` | Uitchecken | Checkout the selected tag as a detached HEAD. | | `` n `` | Creëer tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. | +| `` w `` | New worktree | | | `` d `` | Delete | View delete options for local/remote tag. | | `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. | | `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | Bekijk commits | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Worktrees diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index 1f6710416..719a7e6c4 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -85,13 +85,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Otwórz commit w przeglądarce | | | `` n `` | Utwórz nową gałąź z commita | | | `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | Nowe drzewo pracy | | | `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. | | `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć ``, aby anulować zaznaczenie. | | `` `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | | | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | Wyświetl pliki | | -| `` w `` | Zobacz opcje drzewa pracy | | | `` / `` | Szukaj w bieżącym widoku po tekście | | ## Dodatkowy @@ -122,6 +122,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Otwórz commit w przeglądarce | | | `` n `` | Utwórz nową gałąź z commita | | | `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | Nowe drzewo pracy | | | `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. | | `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć ``, aby anulować zaznaczenie. | | `` `` | Resetuj wybrane (cherry-picked) commity | | @@ -129,7 +130,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | Pokaż commity | | -| `` w `` | Zobacz opcje drzewa pracy | | | `` / `` | Filtruj bieżący widok po tekście | | ## Główny panel (budowanie łatki) @@ -164,6 +164,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Przełącz | Przełącz wybrany element. | | `` n `` | Nowa gałąź | | | `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | Nowe drzewo pracy | | | `` o `` | Utwórz żądanie ściągnięcia | | | `` O `` | Zobacz opcje tworzenia pull requesta | | | `` G `` | Otwórz żądanie ściągnięcia w przeglądarce | | @@ -183,7 +184,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | Pokaż commity | | -| `` w `` | Zobacz opcje drzewa pracy | | | `` / `` | Filtruj bieżący widok po tekście | | ## Menu @@ -318,10 +318,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` g `` | Wyciągnij | Zastosuj wpis schowka do katalogu roboczego i usuń wpis schowka. | | `` d `` | Usuń | Usuń wpis schowka z listy schowka. | | `` n `` | Nowa gałąź | Utwórz nową gałąź z wybranego wpisu schowka. Działa poprzez przełączenie git na commit, na którym wpis schowka został utworzony, tworzenie nowej gałęzi z tego commita, a następnie zastosowanie wpisu schowka do nowej gałęzi jako dodatkowego commita. | +| `` w `` | Nowe drzewo pracy | | | `` r `` | Zmień nazwę schowka | | | `` 0 `` | Focus main view | | | `` `` | Wyświetl pliki | | -| `` w `` | Zobacz opcje drzewa pracy | | | `` / `` | Filtruj bieżący widok po tekście | | ## Status @@ -345,6 +345,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Otwórz commit w przeglądarce | | | `` n `` | Utwórz nową gałąź z commita | | | `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | Nowe drzewo pracy | | | `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. | | `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć ``, aby anulować zaznaczenie. | | `` `` | Resetuj wybrane (cherry-picked) commity | | @@ -352,7 +353,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | Wyświetl pliki | | -| `` w `` | Zobacz opcje drzewa pracy | | | `` / `` | Szukaj w bieżącym widoku po tekście | | ## Submoduły @@ -376,13 +376,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Skopiuj tag do schowka | | | `` `` | Przełącz | Przełącz wybrany tag jako odłączoną głowę (detached HEAD). | | `` n `` | Nowy tag | Utwórz nowy tag z bieżącego commita. Zostaniesz poproszony o wprowadzenie nazwy tagu i opcjonalnego opisu. | +| `` w `` | Nowe drzewo pracy | | | `` d `` | Usuń | Wyświetl opcje usuwania lokalnego/odległego tagu. | | `` P `` | Wyślij tag | Wyślij wybrany tag do zdalnego. Zostaniesz poproszony o wybranie zdalnego. | | `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. | | `` `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | Pokaż commity | | -| `` w `` | Zobacz opcje drzewa pracy | | | `` / `` | Filtruj bieżący widok po tekście | | ## Zdalne @@ -404,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Kopiuj nazwę gałęzi do schowka | | | `` `` | Przełącz | Przełącz na nową lokalną gałąź na podstawie wybranej gałęzi zdalnej. Nowa gałąź będzie śledzić gałąź zdalną. | | `` n `` | Nowa gałąź | | +| `` w `` | Nowe drzewo pracy | | | `` M `` | Scal | Scal wybraną gałąź z aktualnie sprawdzoną gałęzią. | | `` r `` | Przebazuj | Przebazuj przełączoną gałąź na wybraną gałąź. | | `` d `` | Usuń | Usuń gałąź zdalną ze zdalnego. | @@ -413,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | Pokaż commity | | -| `` w `` | Zobacz opcje drzewa pracy | | | `` / `` | Filtruj bieżący widok po tekście | | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index a2d163735..5fcba2688 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -94,6 +94,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Verificar | Checar item selecionado | | `` n `` | Nova branch | | | `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | Nova árvore de trabalho | | | `` o `` | Criar solicitação de pull | | | `` O `` | View create pull request options | | | `` G `` | Open pull request in browser | | @@ -113,7 +114,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Abrir ferramenta de diff externa (git difftool) | | | `` 0 `` | Focar visualização principal | | | `` `` | Ver commits | | -| `` w `` | Ver opções da árvore de trabalho | | | `` / `` | Filtrar a visualização atual por texto | | ## Branches remotos @@ -123,6 +123,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Copiar nome da branch para área de transferência | | | `` `` | Verificar | Checar a nova branch baseada na brach remota selecionada, ou a branch remota como HEAD, desanexado | | `` n `` | Nova branch | | +| `` w `` | Nova árvore de trabalho | | | `` M `` | Mesclar | Ver opções para mesclar o item selecionado no branch atual (mesclar regularmente, mesclar squash) | | `` r `` | Refazer | Refazer a branch checada na branch selecionada | | `` d `` | Apagar | Excluir o branch remoto do controle remoto. | @@ -132,7 +133,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Abrir ferramenta de diff externa (git difftool) | | | `` 0 `` | Focar visualização principal | | | `` `` | Ver commits | | -| `` w `` | Ver opções da árvore de trabalho | | | `` / `` | Filtrar a visualização atual por texto | | ## Commit arquivos @@ -188,13 +188,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Abrir commit no navegador | | | `` n `` | Create new branch off of commit | | | `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | Nova árvore de trabalho | | | `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. | | `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `` para cancelar a seleção. | | `` `` | Abrir ferramenta de diff externa (git difftool) | | | `` * `` | Select commits of current branch | | | `` 0 `` | Focar visualização principal | | | `` `` | Ver arquivos | | -| `` w `` | Ver opções da árvore de trabalho | | | `` / `` | Pesquisar na visualização atual por texto | | ## Etiquetas @@ -204,13 +204,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Copiar etiqueta para área de transferência | | | `` `` | Verificar | Checar a tag selecionada como um HEAD, desanexado | | `` n `` | Nova etiqueta | Crie uma nova etiqueta a partir do commit atual. Você será solicitado a digitar um nome e uma descrição opcional. | +| `` w `` | Nova árvore de trabalho | | | `` d `` | Apagar | Ver opções de exclusão para tag local/remoto. | | `` P `` | Empurrar etiqueta | Push the selected tag to a remote. You'll be prompted to select a remote. | | `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. | | `` `` | Abrir ferramenta de diff externa (git difftool) | | | `` 0 `` | Focar visualização principal | | | `` `` | Ver commits | | -| `` w `` | Ver opções da árvore de trabalho | | | `` / `` | Filtrar a visualização atual por texto | | ## Input prompt @@ -310,6 +310,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Abrir commit no navegador | | | `` n `` | Create new branch off of commit | | | `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | Nova árvore de trabalho | | | `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. | | `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `` para cancelar a seleção. | | `` `` | Reset copied (cherry-picked) commits selection | | @@ -317,7 +318,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focar visualização principal | | | `` `` | Ver commits | | -| `` w `` | Ver opções da árvore de trabalho | | | `` / `` | Filtrar a visualização atual por texto | | ## Remotes @@ -348,10 +348,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` g `` | Pop | Aplique a entrada de stash no seu diretório de trabalho e remova a entrada de stash. | | `` d `` | Descartar | Remova a entrada do stash da lista de armazenamento. | | `` n `` | Nova branch | Criar um novo ramo a partir da entrada de lixo selecionada. Isso funciona verificando o commit do qual a entrada de lixo foi criada, criar um novo branch a partir desse commit e, em seguida, aplicar a entrada de lixo ao novo branch como um commit adicional. | +| `` w `` | Nova árvore de trabalho | | | `` r `` | Renomear o stash | | | `` 0 `` | Focar visualização principal | | | `` `` | Ver arquivos | | -| `` w `` | Ver opções da árvore de trabalho | | | `` / `` | Filtrar a visualização atual por texto | | ## Status @@ -375,6 +375,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Abrir commit no navegador | | | `` n `` | Create new branch off of commit | | | `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | Nova árvore de trabalho | | | `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. | | `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `` para cancelar a seleção. | | `` `` | Reset copied (cherry-picked) commits selection | | @@ -382,7 +383,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focar visualização principal | | | `` `` | Ver arquivos | | -| `` w `` | Ver opções da árvore de trabalho | | | `` / `` | Pesquisar na visualização atual por texto | | ## Submódulos diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index 68be4a601..683135448 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -151,6 +151,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Открыть коммит в браузере | | | `` n `` | Создать новую ветку с этого коммита | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | | @@ -158,7 +159,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | Просмотреть коммиты | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Коммиты @@ -194,13 +194,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Открыть коммит в браузере | | | `` n `` | Создать новую ветку с этого коммита | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | Просмотреть файлы выбранного элемента | | -| `` w `` | View worktree options | | | `` / `` | Найти | | ## Локальные Ветки @@ -212,6 +212,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Переключить | Checkout selected item. | | `` n `` | Новая ветка | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` o `` | Создать запрос на принятие изменений | | | `` O `` | Создать параметры запроса принятие изменений | | | `` G `` | Open pull request in browser | | @@ -231,7 +232,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | Просмотреть коммиты | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Меню @@ -260,6 +260,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | Открыть коммит в браузере | | | `` n `` | Создать новую ветку с этого коммита | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | | @@ -267,7 +268,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | Просмотреть файлы выбранного элемента | | -| `` w `` | View worktree options | | | `` / `` | Найти | | ## Подмодули @@ -329,13 +329,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Copy tag to clipboard | | | `` `` | Переключить | Checkout the selected tag as a detached HEAD. | | `` n `` | Создать тег | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. | +| `` w `` | New worktree | | | `` d `` | Delete | View delete options for local/remote tag. | | `` P `` | Отправить тег | Push the selected tag to a remote. You'll be prompted to select a remote. | | `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | Просмотреть коммиты | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Удалённые ветки @@ -345,6 +345,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Скопировать название ветки в буфер обмена | | | `` `` | Переключить | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. | | `` n `` | Новая ветка | | +| `` w `` | New worktree | | | `` M `` | Слияние с текущей переключённой веткой | View options for merging the selected item into the current branch (regular merge, squash merge) | | `` r `` | Перебазировать переключённую ветку на эту ветку | Rebase the checked-out branch onto the selected branch. | | `` d `` | Delete | Delete the remote branch from the remote. | @@ -354,7 +355,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Open external diff tool (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | Просмотреть коммиты | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | ## Удалённые репозитории @@ -410,8 +410,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` g `` | Применить припрятанные изменения и тут же удалить их из хранилища | Apply the stash entry to your working directory and remove the stash entry. | | `` d `` | Удалить припрятанные изменения из хранилища | Remove the stash entry from the stash list. | | `` n `` | Новая ветка | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. | +| `` w `` | New worktree | | | `` r `` | Переименовать хранилище | | | `` 0 `` | Focus main view | | | `` `` | Просмотреть файлы выбранного элемента | | -| `` w `` | View worktree options | | | `` / `` | Filter the current view by text | | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index 6828c35e3..b4f1134ec 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -62,6 +62,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | 在浏览器中打开提交 | | | `` n `` | 从提交创建新分支 | | | `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。

请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 | +| `` w `` | 新建工作树 | | | `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 | | `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `` 来取消选择。 | | `` `` | 重置已拣选(复制)的提交 | | @@ -69,7 +70,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | 选择当前分支的提交 | | | `` 0 `` | 聚焦主视图 | | | `` `` | 查看提交的文件 | | -| `` w `` | 查看工作区选项 | | | `` / `` | 开始搜索 | | ## 子模块 @@ -106,6 +106,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | 在浏览器中打开提交 | | | `` n `` | 从提交创建新分支 | | | `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。

请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 | +| `` w `` | 新建工作树 | | | `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 | | `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `` 来取消选择。 | | `` `` | 重置已拣选(复制)的提交 | | @@ -113,7 +114,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | 选择当前分支的提交 | | | `` 0 `` | 聚焦主视图 | | | `` `` | 查看提交 | | -| `` w `` | 查看工作区选项 | | | `` / `` | 通过文本过滤当前视图 | | ## 提交 @@ -149,13 +149,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | 在浏览器中打开提交 | | | `` n `` | 从提交创建新分支 | | | `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。

请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 | +| `` w `` | 新建工作树 | | | `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 | | `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `` 来取消选择。 | | `` `` | 使用外部差异比较工具(git difftool) | | | `` * `` | 选择当前分支的提交 | | | `` 0 `` | 聚焦主视图 | | | `` `` | 查看提交的文件 | | -| `` w `` | 查看工作区选项 | | | `` / `` | 开始搜索 | | ## 提交信息 @@ -227,6 +227,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 检出 | 检出选中的项目 | | `` n `` | 新分支 | | | `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。

请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 | +| `` w `` | 新建工作树 | | | `` o `` | 创建拉取请求 | | | `` O `` | 创建拉取请求选项 | | | `` G `` | 在浏览器中打开拉取请求 | | @@ -246,7 +247,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 使用外部差异比较工具(git difftool) | | | `` 0 `` | 聚焦主视图 | | | `` `` | 查看提交 | | -| `` w `` | 查看工作区选项 | | | `` / `` | 通过文本过滤当前视图 | | ## 构建补丁中 @@ -272,13 +272,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 复制标签到剪贴板 | | | `` `` | 检出 | 检出选择的标签作为分离的HEAD | | `` n `` | 创建标签 | 基于当前提交创建一个新标签。您将在弹窗中输入标签名称和描述(可选)。 | +| `` w `` | 新建工作树 | | | `` d `` | 删除 | 查看本地/远程标签的删除选项 | | `` P `` | 推送标签 | 推送选择的标签到远端。您将在弹窗中选择一个远端。 | | `` g `` | 重置 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 | | `` `` | 使用外部差异比较工具(git difftool) | | | `` 0 `` | 聚焦主视图 | | | `` `` | 查看提交 | | -| `` w `` | 查看工作区选项 | | | `` / `` | 通过文本过滤当前视图 | | ## 次要 @@ -372,10 +372,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` g `` | 应用并删除 | 将存储项应用到工作目录并删除存储项。 | | `` d `` | 删除 | 从贮藏列表中删除该贮藏项 | | `` n `` | 新分支 | 从选定的贮藏项创建一个新分支。这是通过 git 检查创建贮藏项的提交,从该提交创建一个新分支,然后将贮藏项作为附加提交应用到新分支来实现的。 | +| `` w `` | 新建工作树 | | | `` r `` | 重命名贮藏 | | | `` 0 `` | 聚焦主视图 | | | `` `` | 查看提交的文件 | | -| `` w `` | 查看工作区选项 | | | `` / `` | 通过文本过滤当前视图 | | ## 输入提示 @@ -404,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 复制分支名称到剪贴板 | | | `` `` | 检出 | 基于当前选中的远程分支检出一个新的本地分支,或者将远程分支作分离的HEAD。 | | `` n `` | 新分支 | | +| `` w `` | 新建工作树 | | | `` M `` | 合并到当前检出的分支 | 查看将选中项合并到当前分支的选项(正常合并,压缩合并) | | `` r `` | 变基 | 将检出的分支变基到所选的分支上。 | | `` d `` | 删除 | 从远程删除远程分支。 | @@ -413,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 使用外部差异比较工具(git difftool) | | | `` 0 `` | 聚焦主视图 | | | `` `` | 查看提交 | | -| `` w `` | 查看工作区选项 | | | `` / `` | 通过文本过滤当前视图 | | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index 56d383ff9..b50e25d35 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -141,6 +141,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | 在瀏覽器中開啟提交 | | | `` n `` | 從提交建立新分支 | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | 重設選定的揀選 (複製) 提交 | | @@ -148,7 +149,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | 檢視所選項目的檔案 | | -| `` w `` | 檢視工作目錄選項 | | | `` / `` | 搜尋 | | ## 子模組 @@ -208,13 +208,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | 在瀏覽器中開啟提交 | | | `` n `` | 從提交建立新分支 | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | 開啟外部差異工具 (git difftool) | | | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | 檢視所選項目的檔案 | | -| `` w `` | 檢視工作目錄選項 | | | `` / `` | 搜尋 | | ## 提交摘要 @@ -252,10 +252,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` g `` | 還原 | Apply the stash entry to your working directory and remove the stash entry. | | `` d `` | 捨棄 | Remove the stash entry from the stash list. | | `` n `` | 新分支 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. | +| `` w `` | New worktree | | | `` r `` | 重新命名收藏 | | | `` 0 `` | Focus main view | | | `` `` | 檢視所選項目的檔案 | | -| `` w `` | 檢視工作目錄選項 | | | `` / `` | 搜尋 | | ## 日誌 @@ -268,6 +268,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` o `` | 在瀏覽器中開啟提交 | | | `` n `` | 從提交建立新分支 | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | 重設選定的揀選 (複製) 提交 | | @@ -275,7 +276,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` * `` | Select commits of current branch | | | `` 0 `` | Focus main view | | | `` `` | 檢視提交 | | -| `` w `` | 檢視工作目錄選項 | | | `` / `` | 搜尋 | | ## 本地分支 @@ -287,6 +287,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 檢出 | 檢出選定的項目。 | | `` n `` | 新分支 | | | `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.

Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). | +| `` w `` | New worktree | | | `` o `` | 建立拉取請求 | | | `` O `` | 建立拉取請求選項 | | | `` G `` | Open pull request in browser | | @@ -306,7 +307,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 開啟外部差異工具 (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | 檢視提交 | | -| `` w `` | 檢視工作目錄選項 | | | `` / `` | 搜尋 | | ## 標籤 @@ -316,13 +316,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Copy tag to clipboard | | | `` `` | 檢出 | Checkout the selected tag as a detached HEAD. | | `` n `` | 建立標籤 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. | +| `` w `` | New worktree | | | `` d `` | 刪除 | View delete options for local/remote tag. | | `` P `` | 推送標籤 | Push the selected tag to a remote. You'll be prompted to select a remote. | | `` g `` | 重設 | View reset options (soft/mixed/hard) for resetting onto selected item. | | `` `` | 開啟外部差異工具 (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | 檢視提交 | | -| `` w `` | 檢視工作目錄選項 | | | `` / `` | 搜尋 | | ## 檔案 @@ -404,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 複製分支名稱到剪貼簿 | | | `` `` | 檢出 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. | | `` n `` | 新分支 | | +| `` w `` | New worktree | | | `` M `` | 合併到當前檢出的分支 | View options for merging the selected item into the current branch (regular merge, squash merge) | | `` r `` | 將已檢出的分支變基至此分支 | Rebase the checked-out branch onto the selected branch. | | `` d `` | 刪除 | Delete the remote branch from the remote. | @@ -413,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 開啟外部差異工具 (git difftool) | | | `` 0 `` | Focus main view | | | `` `` | 檢視提交 | | -| `` w `` | 檢視工作目錄選項 | | | `` / `` | 搜尋 | | diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index e7267158a..313d71dcf 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -287,6 +287,26 @@ func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([] } } + pathsToMove := []struct { + oldPath []string + newPath []string + }{ + { + []string{"keybinding", "worktrees", "viewWorktreeOptions"}, + []string{"keybinding", "universal", "newWorktree"}, + }, + } + + for _, pathToMove := range pathsToMove { + err, didMove := yaml_utils.MoveYamlKey(&rootNode, pathToMove.oldPath, pathToMove.newPath) + if err != nil { + return nil, false, fmt.Errorf("Couldn't migrate config file at `%s` for key %s: %w", path, strings.Join(pathToMove.oldPath, "."), err) + } + if didMove { + changes.Add(fmt.Sprintf("Moved '%s' to '%s'", strings.Join(pathToMove.oldPath, "."), strings.Join(pathToMove.newPath, "."))) + } + } + err = changeNullKeybindingsToDisabled(&rootNode, changes) if err != nil { return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %w", path, err) @@ -449,7 +469,8 @@ func migrateAllBranchesLogCmd(rootNode *yaml.Node, changes *ChangesSet) error { // We will later populate it with the individual allBranchesLogCmd record cmdsKeyNode = &yaml.Node{Kind: yaml.ScalarNode, Value: "allBranchesLogCmds"} cmdsValueNode = &yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{}} - gitNode.Content = append(gitNode.Content, + gitNode.Content = append( + gitNode.Content, cmdsKeyNode, cmdsValueNode, ) diff --git a/pkg/config/app_config_test.go b/pkg/config/app_config_test.go index 8e6c85f32..913bc47dc 100644 --- a/pkg/config/app_config_test.go +++ b/pkg/config/app_config_test.go @@ -78,6 +78,73 @@ keybinding: } } +func TestMigrationOfMovedKeys(t *testing.T) { + scenarios := []struct { + name string + input string + expected string + expectedDidChange bool + expectedChanges []string + }{ + { + name: "Empty String", + input: "", + expectedDidChange: false, + expectedChanges: []string{}, + }, + { + name: "No move needed", + input: `foo: + bar: 5 +`, + expectedDidChange: false, + expectedChanges: []string{}, + }, + { + name: "Move worktree keybinding into the universal section", + input: `keybinding: + universal: + quit: q + worktrees: + viewWorktreeOptions: w +`, + expected: `keybinding: + universal: + quit: q + newWorktree: w +`, + expectedDidChange: true, + expectedChanges: []string{"Moved 'keybinding.worktrees.viewWorktreeOptions' to 'keybinding.universal.newWorktree'"}, + }, + { + name: "Create the universal section if it doesn't exist", + input: `keybinding: + worktrees: + viewWorktreeOptions: w +`, + expected: `keybinding: + universal: + newWorktree: w +`, + expectedDidChange: true, + expectedChanges: []string{"Moved 'keybinding.worktrees.viewWorktreeOptions' to 'keybinding.universal.newWorktree'"}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + changes := NewChangesSet() + actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes) + assert.NoError(t, err) + assert.Equal(t, s.expectedDidChange, didChange) + if didChange { + assert.Equal(t, s.expected, string(actual)) + } + assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest()) + }) + } +} + func TestMigrateNullKeybindingsToDisabled(t *testing.T) { scenarios := []struct { name string diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 1b582c1a0..8314701ba 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -11,6 +11,8 @@ type UserConfig struct { Gui GuiConfig `yaml:"gui"` // Config relating to git Git GitConfig `yaml:"git"` + // Config relating to git worktrees + Worktree WorktreeConfig `yaml:"worktree"` // Periodic update checks Update UpdateConfig `yaml:"update"` // Background refreshes @@ -422,6 +424,13 @@ type CommitPrefixConfig struct { Replace string `yaml:"replace" jsonschema:"example=[$1]"` } +type WorktreeConfig struct { + // Default parent directory for new worktrees. It is offered as a candidate location alongside the parent directories of any worktrees you already have. + // A relative path is resolved against the repository's root directory, so "../worktrees" sits beside the repo and ".worktrees" sits inside it. + // A leading "~" is expanded to your home directory, so "~/worktrees" works. + DefaultPath string `yaml:"defaultPath"` +} + type UpdateConfig struct { // One of: 'prompt' (default) | 'background' | 'never' Method string `yaml:"method" jsonschema:"enum=prompt,enum=background,enum=never"` @@ -434,7 +443,6 @@ type KeybindingConfig struct { Status KeybindingStatusConfig `yaml:"status"` Files KeybindingFilesConfig `yaml:"files"` Branches KeybindingBranchesConfig `yaml:"branches"` - Worktrees KeybindingWorktreesConfig `yaml:"worktrees"` Commits KeybindingCommitsConfig `yaml:"commits"` AmendAttribute KeybindingAmendAttributeConfig `yaml:"amendAttribute"` Stash KeybindingStashConfig `yaml:"stash"` @@ -502,6 +510,7 @@ type KeybindingUniversalConfig struct { ConfirmInEditorAlt Keybinding `yaml:"confirmInEditor-alt"` Remove Keybinding `yaml:"remove"` New Keybinding `yaml:"new"` + NewWorktree Keybinding `yaml:"newWorktree"` Edit Keybinding `yaml:"edit"` OpenFile Keybinding `yaml:"openFile"` ScrollUpMain Keybinding `yaml:"scrollUpMain"` @@ -596,10 +605,6 @@ type KeybindingBranchesConfig struct { SortOrder Keybinding `yaml:"sortOrder"` } -type KeybindingWorktreesConfig struct { - ViewWorktreeOptions Keybinding `yaml:"viewWorktreeOptions"` -} - type KeybindingCommitsConfig struct { SquashDown Keybinding `yaml:"squashDown"` RenameCommit Keybinding `yaml:"renameCommit"` @@ -959,6 +964,9 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { ParseEmoji: false, TruncateCopiedCommitHashesTo: 12, }, + Worktree: WorktreeConfig{ + DefaultPath: "", + }, Refresher: RefresherConfig{ RefreshInterval: 10, FetchInterval: 60, @@ -1024,6 +1032,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { ConfirmInEditorAlt: Keybinding{""}, Remove: Keybinding{"d"}, New: Keybinding{"n"}, + NewWorktree: Keybinding{"w"}, Edit: Keybinding{"e"}, OpenFile: Keybinding{"o"}, OpenRecentRepos: Keybinding{""}, @@ -1109,9 +1118,6 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { AddForkRemote: Keybinding{"F"}, SortOrder: Keybinding{"s"}, }, - Worktrees: KeybindingWorktreesConfig{ - ViewWorktreeOptions: Keybinding{"w"}, - }, Commits: KeybindingCommitsConfig{ SquashDown: Keybinding{"s"}, RenameCommit: Keybinding{"r"}, diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 51e240a5d..a7eb35919 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -207,18 +207,6 @@ func (gui *Gui) resetHelpersAndControllers() { controllers.AttachControllers(context, searchControllerFactory.Create(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)) - } - // allow for navigating between side window contexts for _, context := range []types.Context{ gui.State.Contexts.Status, diff --git a/pkg/gui/controllers/basic_commits_controller.go b/pkg/gui/controllers/basic_commits_controller.go index 2ddb5055e..410335712 100644 --- a/pkg/gui/controllers/basic_commits_controller.go +++ b/pkg/gui/controllers/basic_commits_controller.go @@ -89,6 +89,12 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ Description: self.c.Tr.MoveCommitsToNewBranch, Tooltip: self.c.Tr.MoveCommitsToNewBranchTooltip, }, + { + Keys: opts.GetKeys(opts.Config.Universal.NewWorktree), + Handler: self.withItem(self.c.Helpers().Worktree.NewWorktreeMenuForCommit), + Description: self.c.Tr.NewWorktree, + OpensMenu: true, + }, { Keys: opts.GetKeys(opts.Config.Commits.ViewResetOptions), Handler: self.withItem(self.createResetMenu), diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index c2c0595c3..27bef4b66 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -69,6 +69,12 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty Description: self.c.Tr.MoveCommitsToNewBranch, Tooltip: self.c.Tr.MoveCommitsToNewBranchTooltip, }, + { + Keys: opts.GetKeys(opts.Config.Universal.NewWorktree), + Handler: self.withItem(self.c.Helpers().Worktree.NewWorktreeMenuForBranch), + Description: self.c.Tr.NewWorktree, + OpensMenu: true, + }, { Keys: opts.GetKeys(opts.Config.Branches.CreatePullRequest), Handler: self.withItem(self.handleCreatePullRequest), diff --git a/pkg/gui/controllers/helpers/suggestions_helper.go b/pkg/gui/controllers/helpers/suggestions_helper.go index e88fe3822..d26f96f1c 100644 --- a/pkg/gui/controllers/helpers/suggestions_helper.go +++ b/pkg/gui/controllers/helpers/suggestions_helper.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/jesseduffield/generics/set" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/presentation" @@ -84,6 +85,28 @@ func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*ty } } +// GetWorktreeBranchNameSuggestionsFunc suggests branches you can base a new +// worktree on: local branches that aren't checked out in any worktree (you can't +// make a second worktree for them), plus remote branches that don't yet have a +// local branch of the same name. Picking a remote branch creates a new local +// tracking branch, which would fail if that local branch already existed (whether +// or not it's checked out), so we leave those out and you reach the branch via its +// local entry instead. +func (self *SuggestionsHelper) GetWorktreeBranchNameSuggestionsFunc() func(string) []*types.Suggestion { + localBranchNames := lo.FilterMap(self.c.Model().Branches, func(branch *models.Branch, _ int) (string, bool) { + _, checkedOut := git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees) + return branch.Name, !checkedOut + }) + + existingLocalBranches := set.NewFromSlice(self.getBranchNames()) + remoteBranchNames := lo.Filter(self.getRemoteBranchNames("/"), func(remoteBranchName string, _ int) bool { + _, branchName, _ := strings.Cut(remoteBranchName, "/") + return !existingLocalBranches.Includes(branchName) + }) + + return FilterFunc(append(localBranchNames, remoteBranchNames...), self.c.UserConfig().Gui.UseFuzzySearch()) +} + // here we asynchronously fetch the latest set of paths in the repo and store in // self.c.Model().FilesTrie. On the main thread we'll be doing a fuzzy search via // self.c.Model().FilesTrie. So if we've looked for a file previously, we'll start with diff --git a/pkg/gui/controllers/helpers/worktree_helper.go b/pkg/gui/controllers/helpers/worktree_helper.go index 6cb22084b..7a32d898c 100644 --- a/pkg/gui/controllers/helpers/worktree_helper.go +++ b/pkg/gui/controllers/helpers/worktree_helper.go @@ -2,6 +2,7 @@ package helpers import ( "errors" + "path/filepath" "strings" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" @@ -10,6 +11,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" ) type WorktreeHelper struct { @@ -55,105 +57,58 @@ func (self *WorktreeHelper) GetLinkedWorktreeName() string { } func (self *WorktreeHelper) NewWorktree() error { - branch := self.refsHelper.GetCheckedOutRef() - currentBranchName := branch.RefName() - - f := func(detached bool) { - 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 { - f(false) - return nil - }, - }, - { - LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFromDetached, placeholders)}, - OnPress: func() error { - f(true) - return nil - }, - }, - }, - }) -} - -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) - }) - } - 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 - self.c.Prompt(types.PromptOpts{ - Title: title, - HandleConfirm: func(branchName string) error { - opts.Branch = branchName - - return f() - }, - AllowEmptyInput: true, - }) - - return nil - } - - // prompt for the new branch name - self.c.Prompt(types.PromptOpts{ - Title: self.c.Tr.NewBranchName, - HandleConfirm: func(branchName string) error { - opts.Branch = branchName - - return f() - }, - AllowEmptyInput: false, - }) - - return nil + Title: self.c.Tr.NewWorktreeForBranchTitle, + FindSuggestionsFunc: self.suggestionsHelper.GetWorktreeBranchNameSuggestionsFunc(), + HandleConfirm: func(value string) error { + return self.newWorktreeForPickerValue(value) }, }) return nil } +// newWorktreeForPickerValue classifies the value the user picked or typed in the +// worktrees-panel picker and routes to the matching creation flow: +// - an existing local branch -> a worktree that checks it out; +// - a remote branch -> a new local tracking branch + worktree; +// - anything else -> a new branch off the current ref + worktree. +// +// All three then feed the shared location menu. The picker filters out branches +// already checked out somewhere, but a verbatim type-in is still guarded here. +func (self *WorktreeHelper) newWorktreeForPickerValue(value string) error { + if branch, ok := lo.Find(self.c.Model().Branches, func(branch *models.Branch) bool { + return branch.Name == value + }); ok { + if worktree, ok := git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees); ok { + return errors.New(utils.ResolvePlaceholderString(self.c.Tr.BranchCheckedOutByWorktree, + map[string]string{"branchName": branch.Name, "worktreeName": worktree.Name})) + } + + prompt := utils.ResolvePlaceholderString(self.c.Tr.WorktreeLocationPromptCheckout, + map[string]string{"branchName": branch.Name}) + return self.promptForWorktreeLocation(branch.Name, prompt, func(path string) error { + return self.createWorktree(git_commands.NewWorktreeOpts{Path: path, Base: branch.RefName()}, context.WORKTREES_CONTEXT_KEY) + }) + } + + if _, branchName, ok := self.refsHelper.ParseRemoteBranchName(value); ok { + prompt := utils.ResolvePlaceholderString(self.c.Tr.WorktreeLocationPromptTrackingBranch, + map[string]string{"name": branchName, "ref": value}) + return self.promptForWorktreeLocation(branchName, prompt, func(path string) error { + return self.createWorktree(git_commands.NewWorktreeOpts{Path: path, Base: value, Branch: branchName}, context.WORKTREES_CONTEXT_KEY) + }) + } + + name := SanitizedBranchName(value) + base := self.refsHelper.GetCheckedOutRef().RefName() + prompt := utils.ResolvePlaceholderString(self.c.Tr.WorktreeLocationPromptNewBranch, + map[string]string{"name": name, "base": base}) + return self.promptForWorktreeLocation(name, prompt, func(path string) error { + return self.createWorktree(git_commands.NewWorktreeOpts{Path: path, Base: base, Branch: name}, context.WORKTREES_CONTEXT_KEY) + }) +} + func (self *WorktreeHelper) Switch(worktree *models.Worktree, contextKey types.ContextKey) error { if worktree.IsCurrent { return errors.New(self.c.Tr.AlreadyInWorktree) @@ -219,31 +174,252 @@ func (self *WorktreeHelper) Detach(worktree *models.Worktree) error { }) } -func (self *WorktreeHelper) ViewWorktreeOptions(context types.IListContext, ref string) error { - currentBranch := self.refsHelper.GetCheckedOutRef() - canCheckoutBase := context == self.c.Contexts().Branches && ref != currentBranch.RefName() +// worktreeParentDirCandidates returns the candidate parent directories in which +// to create a new worktree, in priority order and de-duplicated: +// +// 1. the parent directory of each existing linked worktree (in worktree order); +// 2. the configured default path (relative paths are resolved against repoPath); +// 3. the repo's parent directory, if nothing else is available. +// +// repoPath is RepoPaths.RepoPath(), which is stable regardless of which worktree +// we're currently standing in. All returned paths are absolute. +func worktreeParentDirCandidates(repoPath string, linkedWorktreePaths []string, defaultPath string) []string { + candidates := lo.Map(linkedWorktreePaths, func(path string, _ int) string { + return filepath.Dir(path) + }) - return self.ViewBranchWorktreeOptions(ref, canCheckoutBase) + if defaultPath != "" { + if filepath.IsAbs(defaultPath) { + defaultPath = filepath.Clean(defaultPath) + } else { + defaultPath = filepath.Join(repoPath, defaultPath) + } + candidates = append(candidates, defaultPath) + } + + candidates = lo.Uniq(candidates) + + if len(candidates) == 0 { + candidates = append(candidates, filepath.Dir(repoPath)) + } + + return candidates } -func (self *WorktreeHelper) ViewBranchWorktreeOptions(branchName string, canCheckoutBase bool) error { - placeholders := map[string]string{"ref": branchName} +func (self *WorktreeHelper) NewWorktreeMenuForBranch(branch *models.Branch) error { + return self.worktreeMenu( + self.newBranchAndWorktreeItem(branch.Name, branch.RefName()), + self.worktreeForBranchItem(branch), + self.detachedWorktreeItem(branch.Name, branch.RefName(), branch.Name), + ) +} +func (self *WorktreeHelper) NewWorktreeMenuForCommit(commit *models.Commit) error { + return self.worktreeMenu( + self.newBranchAndWorktreeItem(commit.ShortHash(), commit.RefName()), + self.detachedWorktreeItem(commit.ShortHash(), commit.RefName(), ""), + ) +} + +func (self *WorktreeHelper) NewWorktreeMenuForTag(tag *models.Tag) error { + return self.worktreeMenu( + self.newBranchAndWorktreeItem(tag.Name, tag.RefName()), + self.detachedWorktreeItem(tag.Name, tag.RefName(), ""), + ) +} + +func (self *WorktreeHelper) NewWorktreeMenuForStash(stash *models.StashEntry) error { + return self.worktreeMenu( + self.newBranchAndWorktreeItem(stash.RefName(), stash.FullRefName()), + self.detachedWorktreeItem(stash.RefName(), stash.FullRefName(), ""), + ) +} + +func (self *WorktreeHelper) NewWorktreeMenuForRemoteBranch(remoteBranch *models.RemoteBranch) error { + // e.g. "origin/foo" -> "foo": the local branch's (and worktree's) default name + strippedName := strings.SplitAfterN(remoteBranch.RefName(), "/", 2)[1] + + return self.worktreeMenu( + self.newLocalBranchAndWorktreeItem(remoteBranch, strippedName), + self.detachedWorktreeItem(remoteBranch.FullName(), remoteBranch.FullName(), strippedName), + ) +} + +func (self *WorktreeHelper) worktreeMenu(items ...*types.MenuItem) error { 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) - }, - }, - }, + Title: self.c.Tr.NewWorktree, + Items: items, + }) +} + +// newBranchAndWorktreeItem is the "new branch + worktree" action for a ref that +// isn't a remote branch (a branch, commit, tag or stash). ref is what we show the +// user (branch name, short hash, stash@{n}, ...); base is what we hand to +// `git worktree add`. +func (self *WorktreeHelper) newBranchAndWorktreeItem(ref string, base string) *types.MenuItem { + return &types.MenuItem{ + Label: utils.ResolvePlaceholderString(self.c.Tr.NewBranchAndWorktreeFromRef, map[string]string{"ref": ref}), + Keys: menuKey('b'), + OnPress: func() error { + return self.startNewBranchWorktree("", base, func(name string) string { + return utils.ResolvePlaceholderString(self.c.Tr.WorktreeLocationPromptNewBranch, + map[string]string{"name": name, "base": ref}) + }) + }, + } +} + +// newLocalBranchAndWorktreeItem is the "new branch + worktree" action for a remote +// branch: the new local branch tracks the remote one, and its name defaults to the +// remote branch name with the remote stripped off. +func (self *WorktreeHelper) newLocalBranchAndWorktreeItem(remoteBranch *models.RemoteBranch, strippedName string) *types.MenuItem { + return &types.MenuItem{ + Label: utils.ResolvePlaceholderString(self.c.Tr.NewLocalBranchAndWorktreeFromRef, map[string]string{"ref": remoteBranch.FullName()}), + Keys: menuKey('b'), + OnPress: func() error { + return self.startNewBranchWorktree(strippedName, remoteBranch.FullName(), func(name string) string { + return utils.ResolvePlaceholderString(self.c.Tr.WorktreeLocationPromptTrackingBranch, + map[string]string{"name": name, "ref": remoteBranch.FullName()}) + }) + }, + } +} + +// startNewBranchWorktree runs the shared name -> location -> create pipeline for the +// new-branch actions: prompt for the branch (and worktree) name, ask for the +// location, then create a worktree on a freshly created branch of that name. +// locationPrompt builds the location-menu prompt once the name is known. +func (self *WorktreeHelper) startNewBranchWorktree(nameInitialContent string, base string, locationPrompt func(name string) string) error { + return self.promptForName(self.c.Tr.NewBranchAndWorktreeName, nameInitialContent, func(name string) error { + return self.promptForWorktreeLocation(name, locationPrompt(name), func(path string) error { + return self.createWorktree(git_commands.NewWorktreeOpts{Path: path, Base: base, Branch: name}, context.LOCAL_BRANCHES_CONTEXT_KEY) + }) + }) +} + +// worktreeForBranchItem is the "check out an existing branch in a new worktree" +// action. It's disabled when the branch is already checked out somewhere. +func (self *WorktreeHelper) worktreeForBranchItem(branch *models.Branch) *types.MenuItem { + return &types.MenuItem{ + Label: utils.ResolvePlaceholderString(self.c.Tr.WorktreeForRef, map[string]string{"ref": branch.Name}), + Keys: menuKey('w'), + OnPress: func() error { + prompt := utils.ResolvePlaceholderString(self.c.Tr.WorktreeLocationPromptCheckout, + map[string]string{"branchName": branch.Name}) + return self.promptForWorktreeLocation(branch.Name, prompt, func(path string) error { + return self.createWorktree(git_commands.NewWorktreeOpts{Path: path, Base: branch.RefName()}, context.LOCAL_BRANCHES_CONTEXT_KEY) + }) + }, + DisabledReason: self.branchCheckedOutDisabledReason(branch), + } +} + +// detachedWorktreeItem is the "detached worktree at a ref" action. ref is shown to +// the user; base is handed to `git worktree add`. defaultDirName is the worktree +// directory name to use; when it's empty we prompt for one instead (commits, tags +// and stashes have no good name to derive). +func (self *WorktreeHelper) detachedWorktreeItem(ref string, base string, defaultDirName string) *types.MenuItem { + prompt := utils.ResolvePlaceholderString(self.c.Tr.WorktreeLocationPromptDetached, map[string]string{"ref": ref}) + create := func(path string) error { + return self.createWorktree(git_commands.NewWorktreeOpts{Path: path, Base: base, Detach: true}, context.LOCAL_BRANCHES_CONTEXT_KEY) + } + + return &types.MenuItem{ + Label: utils.ResolvePlaceholderString(self.c.Tr.DetachedWorktreeAtRef, map[string]string{"ref": ref}), + Keys: menuKey('d'), + OnPress: func() error { + if defaultDirName != "" { + return self.promptForWorktreeLocation(defaultDirName, prompt, create) + } + return self.promptForName(self.c.Tr.NewWorktreeName, "", func(name string) error { + return self.promptForWorktreeLocation(name, prompt, create) + }) + }, + } +} + +func (self *WorktreeHelper) branchCheckedOutDisabledReason(branch *models.Branch) *types.DisabledReason { + if worktree, ok := git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees); ok { + return &types.DisabledReason{ + Text: utils.ResolvePlaceholderString(self.c.Tr.BranchCheckedOutByWorktree, + map[string]string{"branchName": branch.Name, "worktreeName": worktree.Name}), + } + } + + return nil +} + +// promptForName asks for a branch/worktree name and sanitizes the response (most +// notably turning spaces into dashes so it's a valid branch name) before +// continuing. +func (self *WorktreeHelper) promptForName(title string, initialContent string, onConfirm func(name string) error) error { + self.c.Prompt(types.PromptOpts{ + Title: title, + InitialContent: initialContent, + HandleConfirm: func(response string) error { + return onConfirm(SanitizedBranchName(response)) + }, + }) + + return nil +} + +// promptForWorktreeLocation shows the location menu: one item per candidate parent +// directory (each labelled with the absolute path the worktree would end up at), +// plus an "Other…" item that opens a free-form path prompt. The chosen absolute +// path is passed to onConfirm. +func (self *WorktreeHelper) promptForWorktreeLocation(dirName string, prompt string, onConfirm func(path string) error) error { + linkedWorktreePaths := []string{} + for _, worktree := range self.c.Model().Worktrees { + if !worktree.IsMain { + linkedWorktreePaths = append(linkedWorktreePaths, worktree.Path) + } + } + parentDirs := worktreeParentDirCandidates( + self.c.Git().RepoPaths.RepoPath(), + linkedWorktreePaths, + utils.ExpandTilde(self.c.UserConfig().Worktree.DefaultPath), + ) + + targets := lo.Map(parentDirs, func(parentDir string, _ int) string { + return filepath.Join(parentDir, dirName) + }) + + menuItems := lo.Map(targets, func(target string, _ int) *types.MenuItem { + return &types.MenuItem{ + Label: target, + OnPress: func() error { return onConfirm(target) }, + } + }) + + menuItems = append(menuItems, &types.MenuItem{ + Label: self.c.Tr.WorktreeLocationOther, + OnPress: func() error { + self.c.Prompt(types.PromptOpts{ + Title: self.c.Tr.NewWorktreePath, + InitialContent: targets[0], + HandleConfirm: func(response string) error { + return onConfirm(utils.ExpandTilde(response)) + }, + }) + return nil + }, + }) + + return self.c.Menu(types.CreateMenuOptions{ + Title: self.c.Tr.WorktreeLocationTitle, + Prompt: prompt, + Items: menuItems, + }) +} + +func (self *WorktreeHelper) createWorktree(opts git_commands.NewWorktreeOpts, contextKey types.ContextKey) 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) }) } diff --git a/pkg/gui/controllers/helpers/worktree_helper_test.go b/pkg/gui/controllers/helpers/worktree_helper_test.go new file mode 100644 index 000000000..ea975d79d --- /dev/null +++ b/pkg/gui/controllers/helpers/worktree_helper_test.go @@ -0,0 +1,94 @@ +package helpers + +import ( + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" +) + +// nativePath rewrites a forward-slash test path into one that is valid on the +// host OS, so the scenarios below can be written with readable Unix-style +// paths. On Windows a leading slash is not absolute (filepath.IsAbs wants a +// drive letter), so we graft one on; relative paths are left untouched. +func nativePath(p string) string { + if runtime.GOOS == "windows" && strings.HasPrefix(p, "/") { + p = "C:" + p + } + return filepath.FromSlash(p) +} + +func TestWorktreeParentDirCandidates(t *testing.T) { + scenarios := []struct { + name string + repoPath string + linkedWorktreePaths []string + defaultPath string + expected []string + }{ + { + name: "no worktrees and no default path falls back to the repo's parent", + repoPath: "/code/myrepo", + linkedWorktreePaths: nil, + defaultPath: "", + expected: []string{"/code"}, + }, + { + name: "uses the parent of each linked worktree, in order", + repoPath: "/code/myrepo", + linkedWorktreePaths: []string{"/code/worktrees/foo", "/elsewhere/bar"}, + defaultPath: "", + expected: []string{"/code/worktrees", "/elsewhere"}, + }, + { + name: "de-duplicates parents shared by multiple worktrees", + repoPath: "/code/myrepo", + linkedWorktreePaths: []string{"/code/worktrees/foo", "/code/worktrees/bar"}, + defaultPath: "", + expected: []string{"/code/worktrees"}, + }, + { + name: "appends the default path after the worktree parents", + repoPath: "/code/myrepo", + linkedWorktreePaths: []string{"/code/worktrees/foo"}, + defaultPath: "/somewhere/else", + expected: []string{"/code/worktrees", "/somewhere/else"}, + }, + { + name: "resolves a relative default path against the repo path", + repoPath: "/code/myrepo", + linkedWorktreePaths: nil, + defaultPath: "../worktrees", + expected: []string{"/code/worktrees"}, + }, + { + name: "resolves a dot-relative default path inside the repo", + repoPath: "/code/myrepo", + linkedWorktreePaths: nil, + defaultPath: ".worktrees", + expected: []string{"/code/myrepo/.worktrees"}, + }, + { + name: "de-duplicates the default path against a worktree parent", + repoPath: "/code/myrepo", + linkedWorktreePaths: []string{"/code/worktrees/foo"}, + defaultPath: "/code/worktrees", + expected: []string{"/code/worktrees"}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := worktreeParentDirCandidates( + nativePath(s.repoPath), + lo.Map(s.linkedWorktreePaths, func(p string, _ int) string { return nativePath(p) }), + nativePath(s.defaultPath), + ) + expected := lo.Map(s.expected, func(p string, _ int) string { return nativePath(p) }) + assert.Equal(t, expected, result) + }) + } +} diff --git a/pkg/gui/controllers/remote_branches_controller.go b/pkg/gui/controllers/remote_branches_controller.go index 3a49c0114..f70145d7b 100644 --- a/pkg/gui/controllers/remote_branches_controller.go +++ b/pkg/gui/controllers/remote_branches_controller.go @@ -48,6 +48,12 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts) GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.NewBranch, }, + { + Keys: opts.GetKeys(opts.Config.Universal.NewWorktree), + Handler: self.withItem(self.c.Helpers().Worktree.NewWorktreeMenuForRemoteBranch), + Description: self.c.Tr.NewWorktree, + OpensMenu: true, + }, { Keys: opts.GetKeys(opts.Config.Branches.MergeIntoCurrentBranch), Handler: opts.Guards.OutsideFilterMode(self.withItem(self.merge)), diff --git a/pkg/gui/controllers/stash_controller.go b/pkg/gui/controllers/stash_controller.go index 49abedd9f..06e6991c6 100644 --- a/pkg/gui/controllers/stash_controller.go +++ b/pkg/gui/controllers/stash_controller.go @@ -66,6 +66,12 @@ func (self *StashController) GetKeybindings(opts types.KeybindingsOpts) []*types Description: self.c.Tr.NewBranch, Tooltip: self.c.Tr.NewBranchFromStashTooltip, }, + { + Keys: opts.GetKeys(opts.Config.Universal.NewWorktree), + Handler: self.withItem(self.c.Helpers().Worktree.NewWorktreeMenuForStash), + Description: self.c.Tr.NewWorktree, + OpensMenu: true, + }, { Keys: opts.GetKeys(opts.Config.Stash.RenameStash), Handler: self.withItem(self.handleRenameStashEntry), diff --git a/pkg/gui/controllers/tags_controller.go b/pkg/gui/controllers/tags_controller.go index a10f9a374..879a73628 100644 --- a/pkg/gui/controllers/tags_controller.go +++ b/pkg/gui/controllers/tags_controller.go @@ -53,6 +53,12 @@ func (self *TagsController) GetKeybindings(opts types.KeybindingsOpts) []*types. Tooltip: self.c.Tr.NewTagTooltip, DisplayOnScreen: true, }, + { + Keys: opts.GetKeys(opts.Config.Universal.NewWorktree), + Handler: self.withItem(self.c.Helpers().Worktree.NewWorktreeMenuForTag), + Description: self.c.Tr.NewWorktree, + OpensMenu: true, + }, { Keys: opts.GetKeys(opts.Config.Universal.Remove), Handler: self.withItem(self.delete), diff --git a/pkg/gui/controllers/worktree_options_controller.go b/pkg/gui/controllers/worktree_options_controller.go deleted file mode 100644 index b1123e2a8..000000000 --- a/pkg/gui/controllers/worktree_options_controller.go +++ /dev/null @@ -1,51 +0,0 @@ -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 - *ListControllerTrait[string] - c *ControllerCommon - context CanViewWorktreeOptions -} - -func NewWorktreeOptionsController(c *ControllerCommon, context CanViewWorktreeOptions) *WorktreeOptionsController { - return &WorktreeOptionsController{ - baseController: baseController{}, - ListControllerTrait: NewListControllerTrait( - c, - context, - context.GetSelectedItemId, - context.GetSelectedItemIds, - ), - c: c, - context: context, - } -} - -func (self *WorktreeOptionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { - bindings := []*types.Binding{ - { - Keys: opts.GetKeys(opts.Config.Worktrees.ViewWorktreeOptions), - Handler: self.withItem(self.viewWorktreeOptions), - Description: self.c.Tr.ViewWorktreeOptions, - OpensMenu: true, - }, - } - - return bindings -} - -func (self *WorktreeOptionsController) viewWorktreeOptions(ref string) error { - return self.c.Helpers().Worktree.ViewWorktreeOptions(self.context, ref) -} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index db8cdbd5e..822e47c10 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -890,13 +890,21 @@ type TranslationSet struct { MainWorktree string NewWorktree string NewWorktreePath string - NewWorktreeBase string RemoveWorktreeTooltip string NewBranchName string - NewBranchNameLeaveBlank string - ViewWorktreeOptions string - CreateWorktreeFrom string - CreateWorktreeFromDetached string + NewWorktreeName string + NewWorktreeForBranchTitle string + NewBranchAndWorktreeName string + NewBranchAndWorktreeFromRef string + NewLocalBranchAndWorktreeFromRef string + WorktreeForRef string + DetachedWorktreeAtRef string + WorktreeLocationTitle string + WorktreeLocationOther string + WorktreeLocationPromptNewBranch string + WorktreeLocationPromptTrackingBranch string + WorktreeLocationPromptCheckout string + WorktreeLocationPromptDetached string LcWorktree string ChangingDirectoryTo string DirenvApprovalTitle string @@ -2024,13 +2032,21 @@ func EnglishTranslationSet() *TranslationSet { MainWorktree: "(main worktree)", NewWorktree: "New worktree", NewWorktreePath: "New worktree path", - NewWorktreeBase: "New worktree base ref", RemoveWorktreeTooltip: "Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory.", 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)", + NewWorktreeName: "New worktree name", + NewWorktreeForBranchTitle: "New worktree for branch", + NewBranchAndWorktreeName: "New branch and worktree name", + NewBranchAndWorktreeFromRef: "New branch and worktree from '{{.ref}}'", + NewLocalBranchAndWorktreeFromRef: "New local branch and worktree from '{{.ref}}'", + WorktreeForRef: "New worktree for '{{.ref}}'", + DetachedWorktreeAtRef: "New detached worktree at '{{.ref}}'", + WorktreeLocationTitle: "Worktree location", + WorktreeLocationOther: "Other…", + WorktreeLocationPromptNewBranch: "New branch '{{.name}}' from '{{.base}}':", + WorktreeLocationPromptTrackingBranch: "New branch '{{.name}}' tracking '{{.ref}}':", + WorktreeLocationPromptCheckout: "Worktree for branch '{{.branchName}}':", + WorktreeLocationPromptDetached: "Detached worktree at '{{.ref}}':", LcWorktree: "worktree", ChangingDirectoryTo: "Changing directory to {{.path}}", DirenvApprovalTitle: "Approve .envrc?", diff --git a/pkg/integration/tests/demo/worktree_create_from_branches.go b/pkg/integration/tests/demo/worktree_create_from_branches.go index 39d2cb73e..64e0761cc 100644 --- a/pkg/integration/tests/demo/worktree_create_from_branches.go +++ b/pkg/integration/tests/demo/worktree_create_from_branches.go @@ -34,24 +34,30 @@ var WorktreeCreateFromBranches = NewIntegrationTest(NewIntegrationTestArgs{ Focus(). NavigateToLine(Contains("master")). Wait(500). - Press(keys.Worktrees.ViewWorktreeOptions). + Press(keys.Universal.NewWorktree). Tap(func() { t.Wait(500) t.ExpectPopup().Menu(). - Title(Equals("Worktree")). - Select(Contains("Create worktree from master").DoesNotContain("detached")). + Title(Equals("New worktree")). + Select(Contains("New branch and worktree from 'master'")). + Confirm() + + t.ExpectPopup().Prompt(). + Title(Equals("New branch and worktree name")). + Type("hotfix/db-on-fire"). + Confirm() + + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Select(Contains("Other…")). Confirm() t.ExpectPopup().Prompt(). Title(Equals("New worktree path")). + Clear(). Type("../hotfix"). Confirm() - - t.ExpectPopup().Prompt(). - Title(Contains("New branch name")). - Type("hotfix/db-on-fire"). - Confirm() }) }, }) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 74e471713..136cd579b 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -495,15 +495,20 @@ var tests = []*components.IntegrationTest{ undo.UndoCheckoutAndDrop, undo.UndoCommit, undo.UndoDrop, + worktree.AddForExistingBranch, worktree.AddFromBranch, worktree.AddFromBranchDetached, worktree.AddFromCommit, + worktree.AddFromRemoteBranch, + worktree.AddFromStash, + worktree.AddFromTag, worktree.AssociateBranchBisect, worktree.AssociateBranchRebase, worktree.BareRepo, worktree.BareRepoWorktreeConfig, worktree.Crud, worktree.CustomCommand, + worktree.DefaultPathTilde, worktree.DetachWorktreeFromBranch, worktree.DotfileBareRepo, worktree.DoubleNestedLinkedSubmodule, @@ -512,6 +517,9 @@ var tests = []*components.IntegrationTest{ worktree.FastForwardWorktreeBranchShouldNotPolluteCurrentWorktree, worktree.ForceRemoveWorktree, worktree.ForceRemoveWorktreeWithSubmodules, + worktree.LocationCandidates, + worktree.NewWorktreePicker, + worktree.NewWorktreePickerRemote, worktree.RemoveWorktreeFromBranch, worktree.ResetWindowTabs, worktree.SymlinkIntoRepoSubdir, diff --git a/pkg/integration/tests/worktree/add_for_existing_branch.go b/pkg/integration/tests/worktree/add_for_existing_branch.go new file mode 100644 index 000000000..7fbbc3fc8 --- /dev/null +++ b/pkg/integration/tests/worktree/add_for_existing_branch.go @@ -0,0 +1,67 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var AddForExistingBranch = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Create a worktree that checks out an existing branch (no new branch), and confirm the option is disabled for an already-checked-out 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.NewBranchFrom("otherbranch", "mybranch") + shell.Checkout("mybranch") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + Lines( + Contains("mybranch").IsSelected(), + Contains("otherbranch"), + ). + // the current branch is checked out by this worktree, so "Worktree + // for 'mybranch'" is disabled + Press(keys.Universal.NewWorktree). + Tap(func() { + t.ExpectPopup(). + Menu(). + Title(Equals("New worktree")). + Select(Contains("New worktree for 'mybranch'")). + Tooltip(Contains("Branch mybranch is checked out by worktree repo")). + Confirm(). + Tap(func() { + t.ExpectToast(Contains("Branch mybranch is checked out by worktree repo")) + }). + Cancel() + }). + // otherbranch is not checked out anywhere, so we can make a worktree for it + NavigateToLine(Contains("otherbranch")). + Press(keys.Universal.NewWorktree). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("New worktree")). + Select(Contains("New worktree for 'otherbranch'")). + Confirm() + + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Confirm() + }) + + // we've switched into the new worktree, which has otherbranch checked out + t.Views().Branches(). + IsFocused(). + Lines( + Contains("otherbranch").IsSelected(), + Contains("mybranch (worktree repo)"), + ) + + t.Views().Status(). + Content(Contains("repo(otherbranch) → otherbranch")) + }, +}) diff --git a/pkg/integration/tests/worktree/add_from_branch.go b/pkg/integration/tests/worktree/add_from_branch.go index ab7807571..46dbc09f5 100644 --- a/pkg/integration/tests/worktree/add_from_branch.go +++ b/pkg/integration/tests/worktree/add_from_branch.go @@ -6,7 +6,7 @@ import ( ) var AddFromBranch = NewIntegrationTest(NewIntegrationTestArgs{ - Description: "Add a worktree via the branches view, then switch back to the main worktree via the branches view", + Description: "Create a new branch and worktree from a branch, then switch back to the main worktree via the branches view", ExtraCmdArgs: []string{}, Skip: false, SetupConfig: func(config *config.AppConfig) {}, @@ -21,22 +21,23 @@ var AddFromBranch = NewIntegrationTest(NewIntegrationTestArgs{ Lines( Contains("mybranch"), ). - Press(keys.Worktrees.ViewWorktreeOptions). + Press(keys.Universal.NewWorktree). Tap(func() { t.ExpectPopup().Menu(). - Title(Equals("Worktree")). - Select(Contains(`Create worktree from mybranch`).DoesNotContain("detached")). + Title(Equals("New worktree")). + Select(Contains("New branch and worktree from 'mybranch'")). Confirm() t.ExpectPopup().Prompt(). - Title(Equals("New worktree path")). - Type("../linked-worktree"). - Confirm() - - t.ExpectPopup().Prompt(). - Title(Equals("New branch name")). + Title(Equals("New branch and worktree name")). Type("newbranch"). Confirm() + + // no existing worktrees and no configured default path, so the + // only candidate location is the repo's parent directory; accept it + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Confirm() }). // confirm we're still focused on the branches view IsFocused(). @@ -54,7 +55,9 @@ var AddFromBranch = NewIntegrationTest(NewIntegrationTestArgs{ }). Lines( Contains("mybranch").IsSelected(), - Contains("newbranch (worktree linked-worktree)"), + // the worktree's directory name matches the branch name, so the + // branches view shows the compact "(worktree)" with no name + Contains("newbranch (worktree)"), ). // Confirm the files view is still showing in the files window Press(keys.Universal.PrevBlock) diff --git a/pkg/integration/tests/worktree/add_from_branch_detached.go b/pkg/integration/tests/worktree/add_from_branch_detached.go index 70f27dc81..f17a50fef 100644 --- a/pkg/integration/tests/worktree/add_from_branch_detached.go +++ b/pkg/integration/tests/worktree/add_from_branch_detached.go @@ -6,7 +6,7 @@ import ( ) var AddFromBranchDetached = NewIntegrationTest(NewIntegrationTestArgs{ - Description: "Add a detached worktree via the branches view", + Description: "Add a detached worktree at a branch via the branches view, choosing a custom location", ExtraCmdArgs: []string{}, Skip: false, SetupConfig: func(config *config.AppConfig) {}, @@ -21,15 +21,24 @@ var AddFromBranchDetached = NewIntegrationTest(NewIntegrationTestArgs{ Lines( Contains("mybranch"), ). - Press(keys.Worktrees.ViewWorktreeOptions). + Press(keys.Universal.NewWorktree). Tap(func() { t.ExpectPopup().Menu(). - Title(Equals("Worktree")). - Select(Contains(`Create worktree from mybranch (detached)`)). + Title(Equals("New worktree")). + Select(Contains("New detached worktree at 'mybranch'")). + Confirm() + + // the location menu defaults the directory name to the branch + // name; pick "Other…" to type a different path instead + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Select(Contains("Other…")). Confirm() t.ExpectPopup().Prompt(). Title(Equals("New worktree path")). + InitialText(Contains("mybranch")). + Clear(). Type("../linked-worktree"). Confirm() }). diff --git a/pkg/integration/tests/worktree/add_from_commit.go b/pkg/integration/tests/worktree/add_from_commit.go index 49b69697b..21f26e4a2 100644 --- a/pkg/integration/tests/worktree/add_from_commit.go +++ b/pkg/integration/tests/worktree/add_from_commit.go @@ -6,7 +6,7 @@ import ( ) var AddFromCommit = NewIntegrationTest(NewIntegrationTestArgs{ - Description: "Add a worktree via the commits view", + Description: "Create a new branch and worktree from a commit via the commits view", ExtraCmdArgs: []string{}, Skip: false, SetupConfig: func(config *config.AppConfig) {}, @@ -24,22 +24,21 @@ var AddFromCommit = NewIntegrationTest(NewIntegrationTestArgs{ Contains("initial commit"), ). NavigateToLine(Contains("initial commit")). - Press(keys.Worktrees.ViewWorktreeOptions). + Press(keys.Universal.NewWorktree). Tap(func() { t.ExpectPopup().Menu(). - Title(Equals("Worktree")). - Select(MatchesRegexp(`Create worktree from .*`).DoesNotContain("detached")). + Title(Equals("New worktree")). + Select(Contains("New branch and worktree from")). Confirm() t.ExpectPopup().Prompt(). - Title(Equals("New worktree path")). - Type("../linked-worktree"). - Confirm() - - t.ExpectPopup().Prompt(). - Title(Equals("New branch name")). + Title(Equals("New branch and worktree name")). Type("newbranch"). Confirm() + + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Confirm() }). Lines( Contains("initial commit"), diff --git a/pkg/integration/tests/worktree/add_from_remote_branch.go b/pkg/integration/tests/worktree/add_from_remote_branch.go new file mode 100644 index 000000000..ac3d421fa --- /dev/null +++ b/pkg/integration/tests/worktree/add_from_remote_branch.go @@ -0,0 +1,61 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var AddFromRemoteBranch = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Create a new local tracking branch and worktree from a remote branch", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("README.md", "hello world") + shell.Commit("initial commit") + shell.NewBranch("feature") + shell.CloneIntoRemote("origin") + shell.Checkout("master") + // drop the local branch so only the remote one remains + shell.RunCommand([]string{"git", "branch", "-D", "feature"}) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Remotes(). + Focus(). + Lines( + Contains("origin").IsSelected(), + ). + PressEnter() + + t.Views().RemoteBranches(). + IsFocused(). + NavigateToLine(Contains("feature")). + Press(keys.Universal.NewWorktree). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("New worktree")). + Select(Contains("New local branch and worktree from 'origin/feature'")). + Confirm() + + // the new branch name defaults to the remote branch name with + // the remote stripped off + t.ExpectPopup().Prompt(). + Title(Equals("New branch and worktree name")). + InitialText(Equals("feature")). + Confirm() + + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Confirm() + }) + + // we've switched into the new worktree, on a local branch that tracks + // the remote one (the ✓ confirms tracking is set up) + t.Views().Branches(). + IsFocused(). + Lines( + Contains("feature").Contains("✓").IsSelected(), + Contains("master (worktree repo)"), + ) + }, +}) diff --git a/pkg/integration/tests/worktree/add_from_stash.go b/pkg/integration/tests/worktree/add_from_stash.go new file mode 100644 index 000000000..c2541183c --- /dev/null +++ b/pkg/integration/tests/worktree/add_from_stash.go @@ -0,0 +1,51 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var AddFromStash = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Create a new branch and worktree from a stash entry", + 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.UpdateFile("README.md", "work in progress") + shell.Stash("my stash") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Stash(). + Focus(). + Lines( + Contains("my stash").IsSelected(), + ). + Press(keys.Universal.NewWorktree). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("New worktree")). + Select(Contains("New branch and worktree from 'stash@{0}'")). + Confirm() + + t.ExpectPopup().Prompt(). + Title(Equals("New branch and worktree name")). + Type("from-stash"). + Confirm() + + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Confirm() + }) + + // we've switched into the new worktree, on the new branch + t.Views().Branches(). + IsFocused(). + Lines( + Contains("from-stash").IsSelected(), + Contains("mybranch (worktree repo)"), + ) + }, +}) diff --git a/pkg/integration/tests/worktree/add_from_tag.go b/pkg/integration/tests/worktree/add_from_tag.go new file mode 100644 index 000000000..bab7c03d8 --- /dev/null +++ b/pkg/integration/tests/worktree/add_from_tag.go @@ -0,0 +1,54 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var AddFromTag = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Create a detached worktree at a tag, entering a worktree name", + 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.CreateLightweightTag("v1.0", "HEAD") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Tags(). + Focus(). + Lines( + Contains("v1.0").IsSelected(), + ). + Press(keys.Universal.NewWorktree). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("New worktree")). + Select(Contains("New detached worktree at 'v1.0'")). + Confirm() + + // a tag has no good name to derive, so we're asked for one + t.ExpectPopup().Prompt(). + Title(Equals("New worktree name")). + Type("tag-worktree"). + Confirm() + + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Confirm() + }) + + // we've switched into the new worktree, with a detached head + t.Views().Branches(). + IsFocused(). + Lines( + Contains("(no branch)").IsSelected(), + Contains("mybranch (worktree repo)"), + ) + + t.Views().Status(). + Content(Contains("repo(tag-worktree)")) + }, +}) diff --git a/pkg/integration/tests/worktree/crud.go b/pkg/integration/tests/worktree/crud.go index cd539d10b..6cda94141 100644 --- a/pkg/integration/tests/worktree/crud.go +++ b/pkg/integration/tests/worktree/crud.go @@ -33,25 +33,23 @@ var Crud = NewIntegrationTest(NewIntegrationTestArgs{ ). Press(keys.Universal.New). Tap(func() { - t.ExpectPopup().Menu(). - Title(Equals("Worktree")). - Select(Contains(`Create worktree from ref`).DoesNotContain(("detached"))). + // a name that isn't an existing branch creates a new branch off + // the current one + t.ExpectPopup().Prompt(). + Title(Equals("New worktree for branch")). + Type("newbranch"). Confirm() - t.ExpectPopup().Prompt(). - Title(Equals("New worktree base ref")). - InitialText(Equals("mybranch")). + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Select(Contains("Other…")). Confirm() t.ExpectPopup().Prompt(). Title(Equals("New worktree path")). + Clear(). 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(), diff --git a/pkg/integration/tests/worktree/default_path_tilde.go b/pkg/integration/tests/worktree/default_path_tilde.go new file mode 100644 index 000000000..57486f1a8 --- /dev/null +++ b/pkg/integration/tests/worktree/default_path_tilde.go @@ -0,0 +1,51 @@ +package worktree + +import ( + "os" + "path/filepath" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DefaultPathTilde = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "A leading ~ in the worktree.defaultPath config is expanded to the home directory", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Worktree.DefaultPath = "~/my-worktrees" + }, + 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(). + NavigateToLine(Contains("mybranch")). + Press(keys.Universal.NewWorktree). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("New worktree")). + Select(Contains("New branch and worktree from 'mybranch'")). + Confirm() + + t.ExpectPopup().Prompt(). + Title(Equals("New branch and worktree name")). + Type("newbranch"). + Confirm() + + // The default path's "~" is expanded to an absolute home-directory + // path; without expansion it would stay a literal "~" resolved + // against the repo, so the candidate would still contain a "~". + home, _ := os.UserHomeDir() + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + ContainsLines( + Contains(filepath.Join(home, "my-worktrees", "newbranch")).DoesNotContain("~"), + ). + Cancel() + }) + }, +}) diff --git a/pkg/integration/tests/worktree/location_candidates.go b/pkg/integration/tests/worktree/location_candidates.go new file mode 100644 index 000000000..5fd835626 --- /dev/null +++ b/pkg/integration/tests/worktree/location_candidates.go @@ -0,0 +1,52 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var LocationCandidates = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "The location menu offers the parents of existing worktrees and the configured default path, and sanitizes the typed name", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Worktree.DefaultPath = "../config-worktrees" + }, + SetupRepo: func(shell *Shell) { + shell.NewBranch("mybranch") + shell.CreateFileAndAdd("README.md", "hello world") + shell.Commit("initial commit") + // a pre-existing linked worktree, so its parent directory is offered as + // a candidate location alongside the configured default path + shell.RunCommand([]string{"git", "worktree", "add", "-b", "existing", "../manual-worktrees/existing"}) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + NavigateToLine(Contains("mybranch")). + Press(keys.Universal.NewWorktree). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("New worktree")). + Select(Contains("New branch and worktree from 'mybranch'")). + Confirm() + + // the space is sanitized to a dash so it's a valid branch (and + // directory) name + t.ExpectPopup().Prompt(). + Title(Equals("New branch and worktree name")). + Type("new feature"). + Confirm() + + // the parent of the existing worktree comes first, then the + // configured default path; both target the sanitized name + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + ContainsLines( + Contains("manual-worktrees").Contains("new-feature"), + Contains("config-worktrees").Contains("new-feature"), + ). + Cancel() + }) + }, +}) diff --git a/pkg/integration/tests/worktree/new_worktree_picker.go b/pkg/integration/tests/worktree/new_worktree_picker.go new file mode 100644 index 000000000..58915e8b5 --- /dev/null +++ b/pkg/integration/tests/worktree/new_worktree_picker.go @@ -0,0 +1,64 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var NewWorktreePicker = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "From the worktrees panel, the picker suggests only branches not already checked out, guards verbatim type-ins, and checks out an existing 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.NewBranchFrom("feature", "mybranch") + shell.Checkout("mybranch") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Worktrees(). + Focus(). + Lines( + Contains("(main worktree)"), + ). + Press(keys.Universal.New). + Tap(func() { + // mybranch is checked out by the current worktree, so it's not + // suggested; feature is + t.ExpectPopup().Prompt(). + Title(Equals("New worktree for branch")). + SuggestionLines(Contains("feature")). + // typing a checked-out branch verbatim is still rejected + Type("mybranch"). + Confirm() + + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Contains("Branch mybranch is checked out by worktree repo")). + Confirm() + }). + Press(keys.Universal.New). + Tap(func() { + // picking an existing branch checks it out (no new branch) + t.ExpectPopup().Prompt(). + Title(Equals("New worktree for branch")). + Type("feature"). + Confirm() + + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Confirm() + }). + // we stay in the worktrees panel, now switched into the new worktree + IsFocused(). + Lines( + Contains("feature").IsSelected(), + Contains("(main worktree)"), + ) + + t.Views().Status(). + Content(Contains("repo(feature) → feature")) + }, +}) diff --git a/pkg/integration/tests/worktree/new_worktree_picker_remote.go b/pkg/integration/tests/worktree/new_worktree_picker_remote.go new file mode 100644 index 000000000..55b156d7f --- /dev/null +++ b/pkg/integration/tests/worktree/new_worktree_picker_remote.go @@ -0,0 +1,60 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var NewWorktreePickerRemote = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "From the worktrees panel, picking a remote branch creates a new local tracking branch and worktree; remote branches whose local branch already exists are filtered out", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("README.md", "hello world") + shell.Commit("initial commit") + shell.NewBranch("feature") + shell.NewBranch("existing") + shell.CloneIntoRemote("origin") + shell.Checkout("master") + // "feature" now exists only on the remote; "existing" stays as a local + // branch (not checked out) that also has a remote counterpart + shell.RunCommand([]string{"git", "branch", "-D", "feature"}) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Worktrees(). + Focus(). + Press(keys.Universal.New). + Tap(func() { + // master is checked out, so neither it nor origin/master is + // offered; "existing" already has a local branch, so + // origin/existing is left out too (you'd reach it via the local + // entry); origin/feature has no local branch, so it's offered + t.ExpectPopup().Prompt(). + Title(Equals("New worktree for branch")). + SuggestionLines( + Contains("existing"), + Contains("origin/feature"), + ). + Type("origin/feature"). + Confirm() + + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Confirm() + }). + IsFocused(). + Lines( + Contains("feature").IsSelected(), + Contains("(main worktree)"), + ) + + // the new worktree is on a local branch that tracks the remote one (the + // ✓ confirms tracking is set up) + t.Views().Branches(). + Focus(). + ContainsLines( + Contains("feature").Contains("✓").IsSelected(), + ) + }, +}) diff --git a/pkg/integration/tests/worktree/worktree_in_repo.go b/pkg/integration/tests/worktree/worktree_in_repo.go index 5f0ee66ec..f6b5b44a3 100644 --- a/pkg/integration/tests/worktree/worktree_in_repo.go +++ b/pkg/integration/tests/worktree/worktree_in_repo.go @@ -28,25 +28,21 @@ var WorktreeInRepo = NewIntegrationTest(NewIntegrationTestArgs{ ). Press(keys.Universal.New). Tap(func() { - t.ExpectPopup().Menu(). - Title(Equals("Worktree")). - Select(Contains(`Create worktree from ref`).DoesNotContain(("detached"))). + t.ExpectPopup().Prompt(). + Title(Equals("New worktree for branch")). + Type("newbranch"). Confirm() - t.ExpectPopup().Prompt(). - Title(Equals("New worktree base ref")). - InitialText(Equals("mybranch")). + t.ExpectPopup().Menu(). + Title(Equals("Worktree location")). + Select(Contains("Other…")). Confirm() t.ExpectPopup().Prompt(). Title(Equals("New worktree path")). + Clear(). 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(), diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 40411d520..0494bb035 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "regexp" "runtime" "strconv" @@ -96,3 +97,26 @@ func FilePath(skip int) string { _, path, _, _ := runtime.Caller(skip) return path } + +// ExpandTilde expands a leading "~" that refers to the current user's home +// directory: "~" and "~/foo" become e.g. "/home/user" and "/home/user/foo". A +// tilde anywhere other than the start, or one immediately followed by a +// username ("~other/foo"), is left untouched, as is the path if the home +// directory can't be determined. We expand it ourselves because lazygit runs +// git directly, with no shell to do it for us. +func ExpandTilde(path string) string { + if path != "~" && !strings.HasPrefix(path, "~/") && + !(runtime.GOOS == "windows" && strings.HasPrefix(path, `~\`)) { + return path + } + + home, err := os.UserHomeDir() + if err != nil { + return path + } + + if path == "~" { + return home + } + return filepath.Join(home, path[2:]) +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 41b40cd9f..8304e7ba4 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -1,6 +1,8 @@ package utils import ( + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -98,3 +100,28 @@ func TestModuloWithWrap(t *testing.T) { } } } + +func TestExpandTilde(t *testing.T) { + home, err := os.UserHomeDir() + assert.NoError(t, err) + + scenarios := []struct { + name string + path string + expected string + }{ + {"bare tilde", "~", home}, + {"tilde with subpath", "~/worktrees", filepath.Join(home, "worktrees")}, + {"absolute path is untouched", "/absolute/path", "/absolute/path"}, + {"relative path is untouched", "relative/path", "relative/path"}, + {"tilde not at the start is untouched", "/foo/~/bar", "/foo/~/bar"}, + {"tilde followed by a username is untouched", "~other/worktrees", "~other/worktrees"}, + {"empty string is untouched", "", ""}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + assert.Equal(t, s.expected, ExpandTilde(s.path)) + }) + } +} diff --git a/pkg/utils/yaml_utils/yaml_utils.go b/pkg/utils/yaml_utils/yaml_utils.go index f8f7f0679..251d4dc01 100644 --- a/pkg/utils/yaml_utils/yaml_utils.go +++ b/pkg/utils/yaml_utils/yaml_utils.go @@ -101,6 +101,98 @@ func renameYamlKey(node *yaml.Node, path []string, newKey string) (error, bool) return renameYamlKey(valueNode, path[1:], newKey) } +// Takes the root node of a yaml document, the path to an existing key, and the +// path at which it should live instead. If the key exists, it (and its value) +// is moved to the new path, creating intermediate mapping nodes as needed, and +// any mapping nodes left empty behind it are removed. Does nothing if the key +// at oldPath doesn't exist. Returns an error if a key already exists at newPath, +// or if a node along either path exists but isn't a mapping. +func MoveYamlKey(rootNode *yaml.Node, oldPath []string, newPath []string) (error, bool) { + // Empty document: nothing to do. + if len(rootNode.Content) == 0 { + return nil, false + } + + body := rootNode.Content[0] + + // Bail out early if there's nothing to move. + oldParent, err := findContainingMap(body, oldPath, false) + if err != nil { + return err, false + } + if oldParent == nil { + return nil, false + } + keyNode, valueNode := LookupKey(oldParent, oldPath[len(oldPath)-1]) + if keyNode == nil { + return nil, false + } + + // Find or create the destination map, and make sure it's free. + newParent, err := findContainingMap(body, newPath, true) + if err != nil { + return err, false + } + newKey := newPath[len(newPath)-1] + if existing, _ := LookupKey(newParent, newKey); existing != nil { + return fmt.Errorf("new key `%s' already exists", newKey), false + } + + // Move the key, then prune any maps that became empty behind it. The + // destination is populated first so that a map shared by both paths isn't + // mistaken for empty during pruning. + RemoveKey(oldParent, oldPath[len(oldPath)-1]) + keyNode.Value = newKey + newParent.Content = append(newParent.Content, keyNode, valueNode) + removeEmptyMaps(body, oldPath[:len(oldPath)-1]) + + return nil, true +} + +// Descends path (excluding its final element) and returns the mapping node that +// should directly contain that final element. With create set, missing +// intermediate maps are created; otherwise a missing intermediate yields a nil +// result. Returns an error if a node along the path exists but isn't a mapping. +func findContainingMap(node *yaml.Node, path []string, create bool) (*yaml.Node, error) { + for _, key := range path[:len(path)-1] { + if node.Kind != yaml.MappingNode { + return nil, errors.New("yaml node in path is not a dictionary") + } + _, valueNode := LookupKey(node, key) + if valueNode == nil { + if !create { + return nil, nil + } + valueNode = &yaml.Node{Kind: yaml.MappingNode} + node.Content = append(node.Content, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}, + valueNode) + } + node = valueNode + } + if node.Kind != yaml.MappingNode { + return nil, errors.New("yaml node in path is not a dictionary") + } + return node, nil +} + +// Walks path from node and removes any mapping that is empty once its child has +// been removed, cascading upward. Stops at the first non-empty ancestor (which +// keeps every ancestor above it non-empty too). +func removeEmptyMaps(node *yaml.Node, path []string) { + if len(path) == 0 { + return + } + _, child := LookupKey(node, path[0]) + if child == nil { + return + } + removeEmptyMaps(child, path[1:]) + if child.Kind == yaml.MappingNode && len(child.Content) == 0 { + RemoveKey(node, path[0]) + } +} + // Traverses a yaml document, calling the callback function for each node. The // callback is expected to modify the node in place func Walk(rootNode *yaml.Node, callback func(node *yaml.Node, path string)) error { diff --git a/pkg/utils/yaml_utils/yaml_utils_test.go b/pkg/utils/yaml_utils/yaml_utils_test.go index d4d1fe074..059f65022 100644 --- a/pkg/utils/yaml_utils/yaml_utils_test.go +++ b/pkg/utils/yaml_utils/yaml_utils_test.go @@ -103,6 +103,88 @@ func TestRenameYamlKey(t *testing.T) { } } +func TestMoveYamlKey(t *testing.T) { + tests := []struct { + name string + in string + oldPath []string + newPath []string + expectedOut string + expectedDidMove bool + expectedErr string + }{ + { + name: "move key into an existing section", + in: "keybinding:\n worktrees:\n viewWorktreeOptions: w\n universal:\n quit: q\n", + oldPath: []string{"keybinding", "worktrees", "viewWorktreeOptions"}, + newPath: []string{"keybinding", "universal", "newWorktree"}, + expectedOut: "keybinding:\n universal:\n quit: q\n newWorktree: w\n", + expectedDidMove: true, + }, + { + name: "create the destination section if it doesn't exist", + in: "keybinding:\n worktrees:\n viewWorktreeOptions: w\n", + oldPath: []string{"keybinding", "worktrees", "viewWorktreeOptions"}, + newPath: []string{"keybinding", "universal", "newWorktree"}, + expectedOut: "keybinding:\n universal:\n newWorktree: w\n", + expectedDidMove: true, + }, + { + name: "keep non-empty siblings when pruning the old section", + in: "keybinding:\n worktrees:\n viewWorktreeOptions: w\n other: x\n", + oldPath: []string{"keybinding", "worktrees", "viewWorktreeOptions"}, + newPath: []string{"keybinding", "universal", "newWorktree"}, + expectedOut: "keybinding:\n worktrees:\n other: x\n universal:\n newWorktree: w\n", + expectedDidMove: true, + }, + { + name: "don't rewrite file if the key doesn't exist", + in: "keybinding:\n universal:\n quit: q\n", + oldPath: []string{"keybinding", "worktrees", "viewWorktreeOptions"}, + newPath: []string{"keybinding", "universal", "newWorktree"}, + expectedOut: "keybinding:\n universal:\n quit: q\n", + expectedDidMove: false, + }, + + // Error cases + { + name: "destination key already exists", + in: "keybinding:\n worktrees:\n viewWorktreeOptions: w\n universal:\n newWorktree: x\n", + oldPath: []string{"keybinding", "worktrees", "viewWorktreeOptions"}, + newPath: []string{"keybinding", "universal", "newWorktree"}, + expectedOut: "keybinding:\n worktrees:\n viewWorktreeOptions: w\n universal:\n newWorktree: x\n", + expectedDidMove: false, + expectedErr: "new key `newWorktree' already exists", + }, + { + name: "node in path is not a dictionary", + in: "keybinding:\n worktrees: nonsense\n", + oldPath: []string{"keybinding", "worktrees", "viewWorktreeOptions"}, + newPath: []string{"keybinding", "universal", "newWorktree"}, + expectedOut: "keybinding:\n worktrees: nonsense\n", + expectedDidMove: false, + expectedErr: "yaml node in path is not a dictionary", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + node := unmarshalForTest(t, test.in) + actualErr, didMove := MoveYamlKey(&node, test.oldPath, test.newPath) + if test.expectedErr == "" { + assert.NoError(t, actualErr) + } else { + assert.EqualError(t, actualErr, test.expectedErr) + } + out := marshalForTest(t, &node) + + assert.Equal(t, test.expectedOut, out) + + assert.Equal(t, test.expectedDidMove, didMove) + }) + } +} + func TestWalk_paths(t *testing.T) { tests := []struct { name string diff --git a/schema-master/config.json b/schema-master/config.json index 85246d640..0dd1d5d20 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -1658,9 +1658,6 @@ "branches": { "$ref": "#/$defs/KeybindingBranchesConfig" }, - "worktrees": { - "$ref": "#/$defs/KeybindingWorktreesConfig" - }, "commits": { "$ref": "#/$defs/KeybindingCommitsConfig" }, @@ -2881,6 +2878,20 @@ ], "default": "n" }, + "newWorktree": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "default": "w" + }, "edit": { "oneOf": [ { @@ -3407,26 +3418,6 @@ "additionalProperties": false, "type": "object" }, - "KeybindingWorktreesConfig": { - "properties": { - "viewWorktreeOptions": { - "oneOf": [ - { - "type": "string" - }, - { - "items": { - "type": "string" - }, - "type": "array" - } - ], - "default": "w" - } - }, - "additionalProperties": false, - "type": "object" - }, "LogConfig": { "properties": { "order": { @@ -3834,6 +3825,10 @@ "$ref": "#/$defs/GitConfig", "description": "Config relating to git" }, + "worktree": { + "$ref": "#/$defs/WorktreeConfig", + "description": "Config relating to git worktrees" + }, "update": { "$ref": "#/$defs/UpdateConfig", "description": "Periodic update checks" @@ -3899,6 +3894,17 @@ }, "additionalProperties": false, "type": "object" + }, + "WorktreeConfig": { + "properties": { + "defaultPath": { + "type": "string", + "description": "Default parent directory for new worktrees. It is offered as a candidate location alongside the parent directories of any worktrees you already have.\nA relative path is resolved against the repository's root directory, so \"../worktrees\" sits beside the repo and \".worktrees\" sits inside it.\nA leading \"~\" is expanded to your home directory, so \"~/worktrees\" works." + } + }, + "additionalProperties": false, + "type": "object", + "description": "Config relating to git worktrees" } } }