Fix #5302: Create .git/info directory before writing exclude file (#5325)

Fixes #5302

## Problem

When `.git/info` directory does not exist (can happen with bare clones,
manual deletion, or certain Git configurations), selecting "Add to
.git/info/exclude" fails with:

```
open <repo-path>/.git/info/exclude: no such file or directory
```

The `Exclude` function used `os.OpenFile` with `os.O_CREATE` which
creates the file but not its parent directory.

## Fix

Added `os.MkdirAll` to create the `.git/info` directory (with 0755
permissions) before attempting to open the exclude file. `MkdirAll` is a
no-op if the directory already exists, so existing behavior is
unchanged.

## Test

Added integration test `file/exclude_without_info_dir` that:
1. Removes `.git/info` directory during repo setup
2. Attempts to exclude a file via the menu
3. Verifies `.git/info/exclude` is created with the correct content

Test fails without the fix, passes with it.

Tested locally on macOS ARM (Apple Silicon). Unit tests and integration
test pass.
This commit is contained in:
Stefan Haller 2026-03-08 16:26:40 +01:00 committed by GitHub
commit 25895bfa73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 38 additions and 1 deletions

View file

@ -246,7 +246,11 @@ func (self *WorkingTreeCommands) Ignore(filename string) error {
// Exclude adds a file to the .git/info/exclude for the repo
func (self *WorkingTreeCommands) Exclude(filename string) error {
excludeFile := filepath.Join(self.repoPaths.repoGitDirPath, "info", "exclude")
infoDir := filepath.Join(self.repoPaths.repoGitDirPath, "info")
if err := os.MkdirAll(infoDir, 0o755); err != nil {
return err
}
excludeFile := filepath.Join(infoDir, "exclude")
return self.os.AppendLineToFile(excludeFile, escapeFilename(filename))
}

View file

@ -0,0 +1,32 @@
package file
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ExcludeWithoutInfoDir = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Exclude a file when .git/info directory does not exist",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
},
SetupRepo: func(shell *Shell) {
// Remove .git/info directory to reproduce #5302
shell.RunCommand([]string{"rm", "-rf", ".git/info"})
shell.CreateFile("toExclude", "")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Focus().
NavigateToLine(Contains("toExclude")).
Press(keys.Files.IgnoreFile).
Tap(func() {
t.ExpectPopup().Menu().Title(Equals("Ignore or exclude file")).Select(Contains("Add to .git/info/exclude")).Confirm()
// Should succeed without error, creating .git/info/ directory automatically
t.FileSystem().FileContent(".git/info/exclude", Contains("/toExclude"))
})
},
})

View file

@ -221,6 +221,7 @@ var tests = []*components.IntegrationTest{
file.DiscardUnstagedRangeSelect,
file.DiscardVariousChanges,
file.DiscardVariousChangesRangeSelect,
file.ExcludeWithoutInfoDir,
file.Gitignore,
file.GitignoreSpecialCharacters,
file.RememberCommitMessageAfterFail,