mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-11-25 22:32:13 +02:00
feat: add fork remote command
The command allows you to quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote.
This commit is contained in:
committed by
Stefan Haller
parent
ba632d4064
commit
3892c40666
@@ -703,6 +703,7 @@ keybinding:
|
|||||||
pushTag: P
|
pushTag: P
|
||||||
setUpstream: u
|
setUpstream: u
|
||||||
fetchRemote: f
|
fetchRemote: f
|
||||||
|
addForkRemote: F
|
||||||
sortOrder: s
|
sortOrder: s
|
||||||
worktrees:
|
worktrees:
|
||||||
viewWorktreeOptions: w
|
viewWorktreeOptions: w
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||||||
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||||
| `` e `` | Edit | Edit the selected remote's name or URL. |
|
| `` e `` | Edit | Edit the selected remote's name or URL. |
|
||||||
| `` f `` | Fetch | Fetch updates from the remote repository. This retrieves new commits and branches without merging them into your local branches. |
|
| `` f `` | Fetch | Fetch updates from the remote repository. This retrieves new commits and branches without merging them into your local branches. |
|
||||||
|
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
|
||||||
| `` / `` | Filter the current view by text | |
|
| `` / `` | Filter the current view by text | |
|
||||||
|
|
||||||
## Secondary
|
## Secondary
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
|
|||||||
| `` d `` | 削除 | 選択したリモートを削除します。そのリモートからのリモートブランチを追跡しているローカルブランチは影響を受けません。 |
|
| `` d `` | 削除 | 選択したリモートを削除します。そのリモートからのリモートブランチを追跡しているローカルブランチは影響を受けません。 |
|
||||||
| `` e `` | 編集 | 選択したリモートの名前またはURLを編集します。 |
|
| `` e `` | 編集 | 選択したリモートの名前またはURLを編集します。 |
|
||||||
| `` f `` | フェッチ | リモートリポジトリから更新をフェッチします。これにより、ローカルブランチにマージせずに新しいコミットとブランチを取得します。 |
|
| `` f `` | フェッチ | リモートリポジトリから更新をフェッチします。これにより、ローカルブランチにマージせずに新しいコミットとブランチを取得します。 |
|
||||||
|
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
|
||||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||||
|
|
||||||
## リモートブランチ
|
## リモートブランチ
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||||||
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||||
| `` e `` | Edit | Remote를 수정 |
|
| `` e `` | Edit | Remote를 수정 |
|
||||||
| `` f `` | Fetch | 원격을 업데이트 |
|
| `` f `` | Fetch | 원격을 업데이트 |
|
||||||
|
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
|
||||||
| `` / `` | Filter the current view by text | |
|
| `` / `` | Filter the current view by text | |
|
||||||
|
|
||||||
## 원격 브랜치
|
## 원격 브랜치
|
||||||
|
|||||||
@@ -295,6 +295,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||||||
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||||
| `` e `` | Edit | Wijzig remote |
|
| `` e `` | Edit | Wijzig remote |
|
||||||
| `` f `` | Fetch | Fetch remote |
|
| `` f `` | Fetch | Fetch remote |
|
||||||
|
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
|
||||||
| `` / `` | Filter the current view by text | |
|
| `` / `` | Filter the current view by text | |
|
||||||
|
|
||||||
## Secondary
|
## Secondary
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
|
|||||||
| `` d `` | Usuń | Usuń wybrany zdalny. Wszelkie lokalne gałęzie śledzące gałąź zdalną z tego zdalnego nie zostaną dotknięte. |
|
| `` d `` | Usuń | Usuń wybrany zdalny. Wszelkie lokalne gałęzie śledzące gałąź zdalną z tego zdalnego nie zostaną dotknięte. |
|
||||||
| `` e `` | Edytuj | Edytuj nazwę lub URL wybranego zdalnego. |
|
| `` e `` | Edytuj | Edytuj nazwę lub URL wybranego zdalnego. |
|
||||||
| `` f `` | Pobierz | Pobierz aktualizacje z zdalnego repozytorium. Pobiera nowe commity i gałęzie bez scalania ich z lokalnymi gałęziami. |
|
| `` f `` | Pobierz | Pobierz aktualizacje z zdalnego repozytorium. Pobiera nowe commity i gałęzie bez scalania ich z lokalnymi gałęziami. |
|
||||||
|
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
|
||||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||||
|
|
||||||
## Zdalne gałęzie
|
## Zdalne gałęzie
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||||||
| `` d `` | Remover | Remover o controle remoto. Quaisquer ramificações locais de rastreamento de um ramo remoto do controle não serão afetadas. |
|
| `` d `` | Remover | Remover o controle remoto. Quaisquer ramificações locais de rastreamento de um ramo remoto do controle não serão afetadas. |
|
||||||
| `` e `` | Editar | Edit the selected remote's name or URL. |
|
| `` e `` | Editar | Edit the selected remote's name or URL. |
|
||||||
| `` f `` | Buscar | Fetch updates from the remote repository. This retrieves new commits and branches without merging them into your local branches. |
|
| `` f `` | Buscar | Fetch updates from the remote repository. This retrieves new commits and branches without merging them into your local branches. |
|
||||||
|
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
|
||||||
| `` / `` | Filter the current view by text | |
|
| `` / `` | Filter the current view by text | |
|
||||||
|
|
||||||
## Secundário
|
## Secundário
|
||||||
|
|||||||
@@ -363,6 +363,7 @@ _Связки клавиш_
|
|||||||
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||||
| `` e `` | Edit | Редактировать удалённый репозитории |
|
| `` e `` | Edit | Редактировать удалённый репозитории |
|
||||||
| `` f `` | Получить изменения | Получение изменения из удалённого репозитория |
|
| `` f `` | Получить изменения | Получение изменения из удалённого репозитория |
|
||||||
|
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
|
||||||
| `` / `` | Filter the current view by text | |
|
| `` / `` | Filter the current view by text | |
|
||||||
|
|
||||||
## Файлы
|
## Файлы
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
|||||||
| `` d `` | 删除 | 删除选中的远程。从远程跟踪远程分支的任何本地分支都不会受到影响。 |
|
| `` d `` | 删除 | 删除选中的远程。从远程跟踪远程分支的任何本地分支都不会受到影响。 |
|
||||||
| `` e `` | 编辑 | 编辑远程仓库 |
|
| `` e `` | 编辑 | 编辑远程仓库 |
|
||||||
| `` f `` | 抓取 | 抓取远程仓库 |
|
| `` f `` | 抓取 | 抓取远程仓库 |
|
||||||
|
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
|
||||||
| `` / `` | 通过文本过滤当前视图 | |
|
| `` / `` | 通过文本过滤当前视图 | |
|
||||||
|
|
||||||
## 远程分支
|
## 远程分支
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
|
|||||||
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||||
| `` e `` | 編輯 | 編輯遠端 |
|
| `` e `` | 編輯 | 編輯遠端 |
|
||||||
| `` f `` | 擷取 | 擷取遠端 |
|
| `` f `` | 擷取 | 擷取遠端 |
|
||||||
|
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
|
||||||
| `` / `` | 搜尋 | |
|
| `` / `` | 搜尋 | |
|
||||||
|
|
||||||
## 遠端分支
|
## 遠端分支
|
||||||
|
|||||||
@@ -529,6 +529,7 @@ type KeybindingBranchesConfig struct {
|
|||||||
PushTag string `yaml:"pushTag"`
|
PushTag string `yaml:"pushTag"`
|
||||||
SetUpstream string `yaml:"setUpstream"`
|
SetUpstream string `yaml:"setUpstream"`
|
||||||
FetchRemote string `yaml:"fetchRemote"`
|
FetchRemote string `yaml:"fetchRemote"`
|
||||||
|
AddForkRemote string `yaml:"addForkRemote"`
|
||||||
SortOrder string `yaml:"sortOrder"`
|
SortOrder string `yaml:"sortOrder"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,6 +986,7 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
PushTag: "P",
|
PushTag: "P",
|
||||||
SetUpstream: "u",
|
SetUpstream: "u",
|
||||||
FetchRemote: "f",
|
FetchRemote: "f",
|
||||||
|
AddForkRemote: "F",
|
||||||
SortOrder: "s",
|
SortOrder: "s",
|
||||||
},
|
},
|
||||||
Worktrees: KeybindingWorktreesConfig{
|
Worktrees: KeybindingWorktreesConfig{
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
@@ -78,6 +81,14 @@ func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*typ
|
|||||||
Tooltip: self.c.Tr.FetchRemoteTooltip,
|
Tooltip: self.c.Tr.FetchRemoteTooltip,
|
||||||
DisplayOnScreen: true,
|
DisplayOnScreen: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Key: opts.GetKey(opts.Config.Branches.AddForkRemote),
|
||||||
|
Handler: self.addFork,
|
||||||
|
GetDisabledReason: self.hasOriginRemote(),
|
||||||
|
Description: self.c.Tr.AddForkRemote,
|
||||||
|
Tooltip: self.c.Tr.AddForkRemoteTooltip,
|
||||||
|
DisplayOnScreen: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
@@ -133,6 +144,55 @@ func (self *RemotesController) enter(remote *models.Remote) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adds a new remote, refreshes and selects it, then fetches and checks out the specified branch if provided.
|
||||||
|
func (self *RemotesController) addAndCheckoutRemote(remoteName string, remoteUrl string, branchToCheckout string) error {
|
||||||
|
err := self.c.Git().Remote.AddRemote(remoteName, remoteUrl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do a sync refresh of the remotes so that we can select
|
||||||
|
// the new one. Loading remotes is not expensive, so we can
|
||||||
|
// afford it.
|
||||||
|
self.c.Refresh(types.RefreshOptions{
|
||||||
|
Scope: []types.RefreshableView{types.REMOTES},
|
||||||
|
Mode: types.SYNC,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Select the remote
|
||||||
|
for idx, remote := range self.c.Model().Remotes {
|
||||||
|
if remote.Name == remoteName {
|
||||||
|
self.c.Contexts().Remotes.SetSelection(idx)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the remote
|
||||||
|
return self.fetchAndCheckout(self.c.Contexts().Remotes.GetSelected(), branchToCheckout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensures the fork remote exists (matching the given URL).
|
||||||
|
// If it exists and matches, it’s selected and fetched; otherwise, it’s created and then fetched and checked out.
|
||||||
|
// If it does exist but with a different URL, an error is returned.
|
||||||
|
func (self *RemotesController) ensureForkRemoteAndCheckout(remoteName string, remoteUrl string, branchToCheckout string) error {
|
||||||
|
for idx, remote := range self.c.Model().Remotes {
|
||||||
|
if remote.Name == remoteName {
|
||||||
|
hasTheSameUrl := slices.Contains(remote.Urls, remoteUrl)
|
||||||
|
if !hasTheSameUrl {
|
||||||
|
return errors.New(utils.ResolvePlaceholderString(
|
||||||
|
self.c.Tr.IncompatibleForkAlreadyExistsError,
|
||||||
|
map[string]string{
|
||||||
|
"remoteName": remoteName,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
self.c.Contexts().Remotes.SetSelection(idx)
|
||||||
|
return self.fetchAndCheckout(remote, branchToCheckout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.addAndCheckoutRemote(remoteName, remoteUrl, branchToCheckout)
|
||||||
|
}
|
||||||
|
|
||||||
func (self *RemotesController) add() error {
|
func (self *RemotesController) add() error {
|
||||||
self.c.Prompt(types.PromptOpts{
|
self.c.Prompt(types.PromptOpts{
|
||||||
Title: self.c.Tr.NewRemoteName,
|
Title: self.c.Tr.NewRemoteName,
|
||||||
@@ -141,28 +201,7 @@ func (self *RemotesController) add() error {
|
|||||||
Title: self.c.Tr.NewRemoteUrl,
|
Title: self.c.Tr.NewRemoteUrl,
|
||||||
HandleConfirm: func(remoteUrl string) error {
|
HandleConfirm: func(remoteUrl string) error {
|
||||||
self.c.LogAction(self.c.Tr.Actions.AddRemote)
|
self.c.LogAction(self.c.Tr.Actions.AddRemote)
|
||||||
if err := self.c.Git().Remote.AddRemote(remoteName, remoteUrl); err != nil {
|
return self.addAndCheckoutRemote(remoteName, remoteUrl, "")
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do a sync refresh of the remotes so that we can select
|
|
||||||
// the new one. Loading remotes is not expensive, so we can
|
|
||||||
// afford it.
|
|
||||||
self.c.Refresh(types.RefreshOptions{
|
|
||||||
Scope: []types.RefreshableView{types.REMOTES},
|
|
||||||
Mode: types.SYNC,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Select the new remote
|
|
||||||
for idx, remote := range self.c.Model().Remotes {
|
|
||||||
if remote.Name == remoteName {
|
|
||||||
self.c.Contexts().Remotes.SetSelection(idx)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the new remote
|
|
||||||
return self.fetch(self.c.Contexts().Remotes.GetSelected())
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -173,6 +212,74 @@ func (self *RemotesController) add() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regex to match and capture parts of a Git remote URL. Supports the following formats:
|
||||||
|
// 1. SCP-like SSH: git@host:owner[/subgroups]/repo(.git)
|
||||||
|
// 2. SSH URL style: ssh://user@host[:port]/owner[/subgroups]/repo(.git)
|
||||||
|
// 3. HTTPS: https://host/owner[/subgroups]/repo(.git)
|
||||||
|
// 4. Only for integration tests: ../repo_name
|
||||||
|
var (
|
||||||
|
urlRegex = regexp.MustCompile(`^(git@[^:]+:|ssh://[^/]+/|https?://[^/]+/)([^/]+(?:/[^/]+)*)/([^/]+?)(\.git)?$`)
|
||||||
|
integrationTestUrlRegex = regexp.MustCompile(`^\.\./.+$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rewrites a Git remote URL to use the given fork username,
|
||||||
|
// keeping the repo name and host intact. Supports SCP-like SSH, SSH URL style, and HTTPS.
|
||||||
|
func replaceForkUsername(originUrl, forkUsername string, isIntegrationTest bool) (string, error) {
|
||||||
|
if urlRegex.MatchString(originUrl) {
|
||||||
|
return urlRegex.ReplaceAllString(originUrl, "${1}"+forkUsername+"/$3$4"), nil
|
||||||
|
} else if isIntegrationTest && integrationTestUrlRegex.MatchString(originUrl) {
|
||||||
|
return "../" + forkUsername, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("unsupported or invalid remote URL: %s", originUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *RemotesController) getOrigin() *models.Remote {
|
||||||
|
for _, remote := range self.c.Model().Remotes {
|
||||||
|
if remote.Name == "origin" {
|
||||||
|
return remote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *RemotesController) hasOriginRemote() func() *types.DisabledReason {
|
||||||
|
return func() *types.DisabledReason {
|
||||||
|
if self.getOrigin() == nil {
|
||||||
|
return &types.DisabledReason{Text: self.c.Tr.NoOriginRemote}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *RemotesController) addFork() error {
|
||||||
|
origin := self.getOrigin()
|
||||||
|
|
||||||
|
self.c.Prompt(types.PromptOpts{
|
||||||
|
Title: self.c.Tr.AddForkRemoteUsername,
|
||||||
|
HandleConfirm: func(forkUsername string) error {
|
||||||
|
branchToCheckout := ""
|
||||||
|
|
||||||
|
parts := strings.SplitN(forkUsername, ":", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
forkUsername = parts[0]
|
||||||
|
branchToCheckout = parts[1]
|
||||||
|
}
|
||||||
|
originUrl := origin.Urls[0]
|
||||||
|
remoteUrl, err := replaceForkUsername(originUrl, forkUsername, self.c.RunningIntegrationTest())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
self.c.LogAction(self.c.Tr.Actions.AddForkRemote)
|
||||||
|
return self.ensureForkRemoteAndCheckout(forkUsername, remoteUrl, branchToCheckout)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (self *RemotesController) remove(remote *models.Remote) error {
|
func (self *RemotesController) remove(remote *models.Remote) error {
|
||||||
self.c.Confirm(types.ConfirmOpts{
|
self.c.Confirm(types.ConfirmOpts{
|
||||||
Title: self.c.Tr.RemoveRemote,
|
Title: self.c.Tr.RemoveRemote,
|
||||||
@@ -244,16 +351,28 @@ func (self *RemotesController) edit(remote *models.Remote) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *RemotesController) fetch(remote *models.Remote) error {
|
func (self *RemotesController) fetch(remote *models.Remote) error {
|
||||||
|
return self.fetchAndCheckout(remote, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *RemotesController) fetchAndCheckout(remote *models.Remote, branchName string) error {
|
||||||
return self.c.WithInlineStatus(remote, types.ItemOperationFetching, context.REMOTES_CONTEXT_KEY, func(task gocui.Task) error {
|
return self.c.WithInlineStatus(remote, types.ItemOperationFetching, context.REMOTES_CONTEXT_KEY, func(task gocui.Task) error {
|
||||||
err := self.c.Git().Sync.FetchRemote(task, remote.Name)
|
err := self.c.Git().Sync.FetchRemote(task, remote.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
refreshOptions := types.RefreshOptions{
|
||||||
self.c.Refresh(types.RefreshOptions{
|
|
||||||
Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES},
|
Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES},
|
||||||
Mode: types.ASYNC,
|
Mode: types.ASYNC,
|
||||||
})
|
}
|
||||||
return nil
|
if branchName != "" {
|
||||||
|
err = self.c.Git().Branch.New(branchName, remote.Name+"/"+branchName)
|
||||||
|
if err == nil {
|
||||||
|
self.c.Context().Push(self.c.Contexts().Branches, types.OnFocusOpts{})
|
||||||
|
self.c.Contexts().Branches.SetSelection(0)
|
||||||
|
refreshOptions.KeepBranchSelectionIndex = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.c.Refresh(refreshOptions)
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
165
pkg/gui/controllers/remotes_controller_test.go
Normal file
165
pkg/gui/controllers/remotes_controller_test.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReplaceForkUsername_SSH_OK(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
forkUser string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "github ssh scp-like basic",
|
||||||
|
in: "git@github.com:old/repo.git",
|
||||||
|
forkUser: "new",
|
||||||
|
expected: "git@github.com:new/repo.git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh scp-like no .git",
|
||||||
|
in: "git@github.com:old/repo",
|
||||||
|
forkUser: "new",
|
||||||
|
expected: "git@github.com:new/repo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gitlab subgroup ssh scp-like",
|
||||||
|
in: "git@gitlab.com:group/sub/repo.git",
|
||||||
|
forkUser: "alice",
|
||||||
|
expected: "git@gitlab.com:alice/repo.git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh url style basic",
|
||||||
|
in: "ssh://git@github.com/old/repo.git",
|
||||||
|
forkUser: "new",
|
||||||
|
expected: "ssh://git@github.com/new/repo.git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh url style with port",
|
||||||
|
in: "ssh://git@github.com:2222/old/repo.git",
|
||||||
|
forkUser: "bob",
|
||||||
|
expected: "ssh://git@github.com:2222/bob/repo.git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh url style multi subgroup",
|
||||||
|
in: "ssh://git@gitlab.com/group/sub/repo.git",
|
||||||
|
forkUser: "alice",
|
||||||
|
expected: "ssh://git@gitlab.com/alice/repo.git",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
got, err := replaceForkUsername(c.in, c.forkUser, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, c.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceForkUsername_HTTPS_OK(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
forkUser string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "github https basic",
|
||||||
|
in: "https://github.com/old/repo.git",
|
||||||
|
forkUser: "new",
|
||||||
|
expected: "https://github.com/new/repo.git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "https no .git",
|
||||||
|
in: "https://github.com/old/repo",
|
||||||
|
forkUser: "new",
|
||||||
|
expected: "https://github.com/new/repo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "https with port",
|
||||||
|
in: "https://git.example.com:8443/group/repo",
|
||||||
|
forkUser: "me",
|
||||||
|
expected: "https://git.example.com:8443/me/repo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gitlab multi subgroup https",
|
||||||
|
in: "https://gitlab.com/group/sub/sub2/repo",
|
||||||
|
forkUser: "bob",
|
||||||
|
expected: "https://gitlab.com/bob/repo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
got, err := replaceForkUsername(c.in, c.forkUser, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, c.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceForkUsername_IntegrationTest_OK(t *testing.T) {
|
||||||
|
got, err := replaceForkUsername("../origin", "bob", true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "../bob", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceForkUsername_Errors(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
forkUser string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "https host only",
|
||||||
|
in: "https://github.com",
|
||||||
|
forkUser: "x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "https host slash only",
|
||||||
|
in: "https://github.com/",
|
||||||
|
forkUser: "x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "https only repo (no owner)",
|
||||||
|
in: "https://github.com/repo.git",
|
||||||
|
forkUser: "x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh missing path",
|
||||||
|
in: "git@github.com",
|
||||||
|
forkUser: "x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh one segment only",
|
||||||
|
in: "git@github.com:repo.git",
|
||||||
|
forkUser: "x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported scheme",
|
||||||
|
in: "ftp://github.com/old/repo.git",
|
||||||
|
forkUser: "x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty url",
|
||||||
|
in: "",
|
||||||
|
forkUser: "x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "integration test URL outside of integration test",
|
||||||
|
in: "../origin",
|
||||||
|
forkUser: "x",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
_, err := replaceForkUsername(c.in, c.forkUser, false)
|
||||||
|
assert.EqualError(t, err, "unsupported or invalid remote URL: "+c.in)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -525,6 +525,11 @@ type TranslationSet struct {
|
|||||||
NewRemote string
|
NewRemote string
|
||||||
NewRemoteName string
|
NewRemoteName string
|
||||||
NewRemoteUrl string
|
NewRemoteUrl string
|
||||||
|
AddForkRemote string
|
||||||
|
AddForkRemoteUsername string
|
||||||
|
AddForkRemoteTooltip string
|
||||||
|
IncompatibleForkAlreadyExistsError string
|
||||||
|
NoOriginRemote string
|
||||||
ViewBranches string
|
ViewBranches string
|
||||||
EditRemoteName string
|
EditRemoteName string
|
||||||
EditRemoteUrl string
|
EditRemoteUrl string
|
||||||
@@ -1021,6 +1026,7 @@ type Actions struct {
|
|||||||
DeleteRemoteBranch string
|
DeleteRemoteBranch string
|
||||||
SetBranchUpstream string
|
SetBranchUpstream string
|
||||||
AddRemote string
|
AddRemote string
|
||||||
|
AddForkRemote string
|
||||||
RemoveRemote string
|
RemoveRemote string
|
||||||
UpdateRemote string
|
UpdateRemote string
|
||||||
ApplyPatch string
|
ApplyPatch string
|
||||||
@@ -1622,6 +1628,11 @@ func EnglishTranslationSet() *TranslationSet {
|
|||||||
NewRemote: `New remote`,
|
NewRemote: `New remote`,
|
||||||
NewRemoteName: `New remote name:`,
|
NewRemoteName: `New remote name:`,
|
||||||
NewRemoteUrl: `New remote url:`,
|
NewRemoteUrl: `New remote url:`,
|
||||||
|
AddForkRemoteUsername: `Fork owner (username/org). Use username:branch to check out a branch`,
|
||||||
|
AddForkRemote: `Add fork remote`,
|
||||||
|
AddForkRemoteTooltip: `Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote.`,
|
||||||
|
IncompatibleForkAlreadyExistsError: `Remote {{.remoteName}} already exists and has different URL`,
|
||||||
|
NoOriginRemote: "Action needs 'origin' remote",
|
||||||
ViewBranches: "View branches",
|
ViewBranches: "View branches",
|
||||||
EditRemoteName: `Enter updated remote name for {{.remoteName}}:`,
|
EditRemoteName: `Enter updated remote name for {{.remoteName}}:`,
|
||||||
EditRemoteUrl: `Enter updated remote url for {{.remoteName}}:`,
|
EditRemoteUrl: `Enter updated remote url for {{.remoteName}}:`,
|
||||||
@@ -2077,6 +2088,7 @@ func EnglishTranslationSet() *TranslationSet {
|
|||||||
DeleteRemoteBranch: "Delete remote branch",
|
DeleteRemoteBranch: "Delete remote branch",
|
||||||
SetBranchUpstream: "Set branch upstream",
|
SetBranchUpstream: "Set branch upstream",
|
||||||
AddRemote: "Add remote",
|
AddRemote: "Add remote",
|
||||||
|
AddForkRemote: "Add fork remote",
|
||||||
RemoveRemote: "Remove remote",
|
RemoveRemote: "Remove remote",
|
||||||
UpdateRemote: "Update remote",
|
UpdateRemote: "Update remote",
|
||||||
ApplyPatch: "Apply patch",
|
ApplyPatch: "Apply patch",
|
||||||
|
|||||||
@@ -392,6 +392,12 @@ func (self *Shell) SetBranchUpstream(branch string, upstream string) *Shell {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *Shell) RemoveBranch(branch string) *Shell {
|
||||||
|
self.RunCommand([]string{"git", "branch", "-d", branch})
|
||||||
|
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
func (self *Shell) RemoveRemoteBranch(remoteName string, branch string) *Shell {
|
func (self *Shell) RemoveRemoteBranch(remoteName string, branch string) *Shell {
|
||||||
self.RunCommand([]string{"git", "-C", "../" + remoteName, "branch", "-d", branch})
|
self.RunCommand([]string{"git", "-C", "../" + remoteName, "branch", "-d", branch})
|
||||||
|
|
||||||
|
|||||||
47
pkg/integration/tests/remote/add_fork_remote.go
Normal file
47
pkg/integration/tests/remote/add_fork_remote.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
|
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AddForkRemote = NewIntegrationTest(NewIntegrationTestArgs{
|
||||||
|
Description: "Use the 'Add fork remote' command to add a fork remote and check out a branch from it",
|
||||||
|
ExtraCmdArgs: []string{},
|
||||||
|
Skip: false,
|
||||||
|
SetupConfig: func(config *config.AppConfig) {},
|
||||||
|
SetupRepo: func(shell *Shell) {
|
||||||
|
shell.EmptyCommit("commit")
|
||||||
|
shell.CloneIntoRemote("origin")
|
||||||
|
shell.NewBranch("feature")
|
||||||
|
shell.Clone("fork")
|
||||||
|
shell.Checkout("master")
|
||||||
|
shell.RemoveBranch("feature")
|
||||||
|
},
|
||||||
|
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||||
|
t.Views().Remotes().
|
||||||
|
Focus().
|
||||||
|
Lines(
|
||||||
|
Contains("origin").IsSelected(),
|
||||||
|
).
|
||||||
|
Press(keys.Branches.AddForkRemote)
|
||||||
|
|
||||||
|
t.ExpectPopup().Prompt().
|
||||||
|
Title(Equals("Fork owner (username/org). Use username:branch to check out a branch")).
|
||||||
|
Type("fork:feature").
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
t.Views().Remotes().
|
||||||
|
Lines(
|
||||||
|
Contains("origin"),
|
||||||
|
Contains("fork").IsSelected(),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Views().Branches().
|
||||||
|
IsFocused().
|
||||||
|
Lines(
|
||||||
|
Contains("feature ✓"),
|
||||||
|
Contains("master"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/misc"
|
"github.com/jesseduffield/lazygit/pkg/integration/tests/misc"
|
||||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/patch_building"
|
"github.com/jesseduffield/lazygit/pkg/integration/tests/patch_building"
|
||||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/reflog"
|
"github.com/jesseduffield/lazygit/pkg/integration/tests/reflog"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/integration/tests/remote"
|
||||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/shell_commands"
|
"github.com/jesseduffield/lazygit/pkg/integration/tests/shell_commands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/staging"
|
"github.com/jesseduffield/lazygit/pkg/integration/tests/staging"
|
||||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/stash"
|
"github.com/jesseduffield/lazygit/pkg/integration/tests/stash"
|
||||||
@@ -351,6 +352,7 @@ var tests = []*components.IntegrationTest{
|
|||||||
reflog.DoNotShowBranchMarkersInReflogSubcommits,
|
reflog.DoNotShowBranchMarkersInReflogSubcommits,
|
||||||
reflog.Patch,
|
reflog.Patch,
|
||||||
reflog.Reset,
|
reflog.Reset,
|
||||||
|
remote.AddForkRemote,
|
||||||
shell_commands.BasicShellCommand,
|
shell_commands.BasicShellCommand,
|
||||||
shell_commands.ComplexShellCommand,
|
shell_commands.ComplexShellCommand,
|
||||||
shell_commands.DeleteFromHistory,
|
shell_commands.DeleteFromHistory,
|
||||||
|
|||||||
@@ -888,6 +888,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "f"
|
"default": "f"
|
||||||
},
|
},
|
||||||
|
"addForkRemote": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "F"
|
||||||
|
},
|
||||||
"sortOrder": {
|
"sortOrder": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "s"
|
"default": "s"
|
||||||
|
|||||||
Reference in New Issue
Block a user