mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-11-06 13:00:59 -04:00
Don't stage nested repositories
If you staged the directory of a removed submodule, a nested repository or a worktree inside the repo, git added it as an embedded repository: a gitlink without an entry in .gitmodules. For a removed submodule, this brings it back in a broken state. git warns about it, but lazygit didn't show the warning. Staging all files did the same to every such directory. Disable staging a selection that contains one of these, and disable staging all files if that would stage one. The disabled reason tells you how to get rid of it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d2675b7dea
commit
a2cf915f8d
@@ -53,6 +53,13 @@ const (
|
||||
RemovedSubmodule
|
||||
)
|
||||
|
||||
// IsNestedRepository reports whether the file is a git repository of its own
|
||||
// inside the working tree, such as a linked worktree or the directory of a
|
||||
// removed submodule
|
||||
func (f *File) IsNestedRepository() bool {
|
||||
return f.DirectoryKind == NestedRepository || f.DirectoryKind == RemovedSubmodule
|
||||
}
|
||||
|
||||
// sometimes we need to deal with either a node (which contains a file) or an actual file
|
||||
type IFile interface {
|
||||
GetHasUnstagedChanges() bool
|
||||
|
||||
@@ -137,7 +137,7 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Files.ToggleStagedAll),
|
||||
Handler: self.toggleStagedAll,
|
||||
GetDisabledReason: self.require(self.anyFilesDisplayed),
|
||||
GetDisabledReason: self.require(self.anyFilesDisplayed, self.canStageAll),
|
||||
Description: self.c.Tr.ToggleStagedAll,
|
||||
Tooltip: self.c.Tr.ToggleStagedAllTooltip,
|
||||
},
|
||||
@@ -842,7 +842,8 @@ func (self *FilesController) conflictNeedsResolutionDialog(file *models.File) bo
|
||||
|
||||
// canStageSelection disables staging when a multiple selection includes a file
|
||||
// with a conflict that must be resolved through a dialog; those have to be
|
||||
// resolved one at a time.
|
||||
// resolved one at a time. It also disables it when the selection includes a
|
||||
// nested repository.
|
||||
func (self *FilesController) canStageSelection(nodes []*filetree.FileNode) *types.DisabledReason {
|
||||
if len(nodes) > 1 {
|
||||
for _, node := range nodes {
|
||||
@@ -858,6 +859,33 @@ func (self *FilesController) canStageSelection(nodes []*filetree.FileNode) *type
|
||||
}
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
if nestedRepository := node.FindFirstFileBy((*models.File).IsNestedRepository); nestedRepository != nil {
|
||||
return self.c.Helpers().WorkingTree.CantStageNestedRepository(nestedRepository)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// canStageAll disables staging all files when that would stage a nested
|
||||
// repository.
|
||||
func (self *FilesController) canStageAll() *types.DisabledReason {
|
||||
var nestedRepository *models.File
|
||||
switch self.stageAllScope() {
|
||||
case stageAllVisibleFiles:
|
||||
nestedRepository = self.context().FileTreeViewModel.GetRoot().FindFirstFileBy((*models.File).IsNestedRepository)
|
||||
case stageAllTrackedFiles:
|
||||
// A nested repository is never tracked
|
||||
return nil
|
||||
default:
|
||||
nestedRepository, _ = lo.Find(self.c.Model().Files, (*models.File).IsNestedRepository)
|
||||
}
|
||||
|
||||
if nestedRepository != nil {
|
||||
return self.c.Helpers().WorkingTree.CantStageNestedRepository(nestedRepository)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@@ -131,6 +132,21 @@ func (self *WorkingTreeHelper) NestedRepositoryPlaceholders(file *models.File) m
|
||||
}
|
||||
}
|
||||
|
||||
// CantStageNestedRepository returns why the given nested repository can't be
|
||||
// staged. git add records a nested repository as an embedded repository: a
|
||||
// gitlink without an entry in .gitmodules. Nobody wants that, so we don't let
|
||||
// you stage one, and say how to get rid of it instead.
|
||||
func (self *WorkingTreeHelper) CantStageNestedRepository(file *models.File) *types.DisabledReason {
|
||||
text := self.c.Tr.StageNestedRepositoryDisabled
|
||||
if file.IsWorktree {
|
||||
text = self.c.Tr.StageWorktreeDisabled
|
||||
}
|
||||
|
||||
return &types.DisabledReason{
|
||||
Text: utils.ResolvePlaceholderString(text, self.NestedRepositoryPlaceholders(file)),
|
||||
}
|
||||
}
|
||||
|
||||
func (self *WorkingTreeHelper) OpenMergeTool() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.OpenMergeTool)
|
||||
return self.c.RunSubprocessAndRefresh(
|
||||
|
||||
@@ -115,6 +115,8 @@ type TranslationSet struct {
|
||||
NestedRepositoryStatus string
|
||||
PlainDirectoryDescription string
|
||||
PlainDirectoryFiles string
|
||||
StageNestedRepositoryDisabled string
|
||||
StageWorktreeDisabled string
|
||||
StageConflictsRangeDisabled string
|
||||
Checkout string
|
||||
CheckoutTooltip string
|
||||
@@ -1291,6 +1293,8 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
NestedRepositoryDiscardWarning: "Pressing {{.discardKey}} discards the directory, including the repository with all of its history.",
|
||||
WorktreeInsideRepoDescription: "'{{.path}}' is a linked worktree of this repository. Git doesn't look into it, so it shows up as a single untracked entry.\n\nTo remove the worktree, press {{.removeKey}}. If you keep worktrees inside the repository, add their directory to .gitignore so that they don't show up here.",
|
||||
NestedRepositoryStatus: "Status of '{{.path}}'",
|
||||
StageNestedRepositoryDisabled: "Can't stage '{{.path}}' because it is a git repository of its own. Discard it with {{.discardKey}}, or ignore it with {{.ignoreKey}}.",
|
||||
StageWorktreeDisabled: "Can't stage '{{.path}}' because it is a worktree. Remove it with {{.removeKey}}, or ignore it with {{.ignoreKey}}.",
|
||||
PlainDirectoryDescription: "'{{.path}}' is an untracked directory. Git shows it as a single entry because status.showUntrackedFiles is set to \"normal\".",
|
||||
PlainDirectoryFiles: "Files in '{{.path}}'",
|
||||
StageConflictsRangeDisabled: "Cannot stage a selection that includes files with merge conflicts; resolve them individually with {{.goIntoKey}} first.",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var StageNestedRepository = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Staging a nested repository, or all files while there is one, is not supported, because git would add it as an embedded repository",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {
|
||||
config.GetUserConfig().Gui.ShowFileTree = false
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("first commit")
|
||||
shell.CloneIntoSubmodule("sub", "modules/sub")
|
||||
shell.Commit("add submodule")
|
||||
shell.RunCommand([]string{"git", "rm", "--cached", "modules/sub"})
|
||||
shell.RunCommand([]string{"git", "rm", ".gitmodules"})
|
||||
shell.Commit("remove submodule")
|
||||
|
||||
shell.CreateFile("file", "content")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Equals("?? file").IsSelected(),
|
||||
Equals("?? modules/sub (removed submodule)"),
|
||||
).
|
||||
Press(keys.Files.ToggleStagedAll).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Disabled: Can't stage 'modules/sub' because it is a git repository of its own. Discard it with d, or ignore it with i."))
|
||||
}).
|
||||
NavigateToLine(Contains("modules/sub")).
|
||||
PressPrimaryAction().
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Disabled: Can't stage 'modules/sub' because it is a git repository of its own. Discard it with d, or ignore it with i."))
|
||||
}).
|
||||
// Staging all files works if the nested repository is filtered out
|
||||
FilterOrSearch("file").
|
||||
Lines(
|
||||
Equals("?? file").IsSelected(),
|
||||
).
|
||||
Press(keys.Files.ToggleStagedAll).
|
||||
Lines(
|
||||
Equals("A file").IsSelected(),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -266,6 +266,7 @@ var tests = []*components.IntegrationTest{
|
||||
file.StageAllWithoutChangedFiles,
|
||||
file.StageChildrenRangeSelect,
|
||||
file.StageDeletedRangeSelect,
|
||||
file.StageNestedRepository,
|
||||
file.StageRangeSelect,
|
||||
file.StagedChangesInLowerPane,
|
||||
filter_and_search.FilterByFileStatus,
|
||||
|
||||
Reference in New Issue
Block a user