mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
We set GIT_OPTIONAL_LOCKS=0 for every git command we run. That env var only affects `git status`: it tells git not to take the optional lock it would otherwise use to write the index back after refreshing the cached stat information. The intent was to avoid contending for index.lock with git commands the user runs in a terminal. The downside is that our `git status` never persists the refreshed stat-cache. So whenever the working tree's cached stat info goes stale (e.g. editing files and discarding the changes, or a checkout), every subsequent status re-hashes the affected files to confirm they're clean, and stays slow until something else writes the index (such as the user running `git status` in a terminal). Fix this by only suppressing optional locks for refreshes that run unattended in the background; foreground refreshes triggered by a user action now run a plain `git status` that writes the refreshed index back, just like the command line does. Background refreshes keep passing --no-optional-locks so they still can't cause lock contention. RefreshOptions gains a Background flag that the background routines set, threaded down to the status command. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
223 lines
5.6 KiB
Go
223 lines
5.6 KiB
Go
package git_commands
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
)
|
|
|
|
type FileLoaderConfig interface {
|
|
GetShowUntrackedFiles() string
|
|
}
|
|
|
|
type FileLoader struct {
|
|
*GitCommon
|
|
cmd oscommands.ICmdObjBuilder
|
|
config FileLoaderConfig
|
|
getFileType func(string) string
|
|
}
|
|
|
|
func NewFileLoader(gitCommon *GitCommon, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader {
|
|
return &FileLoader{
|
|
GitCommon: gitCommon,
|
|
cmd: cmd,
|
|
getFileType: oscommands.FileType,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
type GetStatusFileOptions struct {
|
|
NoRenames bool
|
|
// If true, we'll show untracked files even if the user has set the config to hide them.
|
|
// This is useful for users with bare repos for dotfiles who default to hiding untracked files,
|
|
// but want to occasionally see them to `git add` a new file.
|
|
ForceShowUntracked bool
|
|
// When true, this status is part of an unattended background refresh, so we
|
|
// pass --no-optional-locks to avoid index.lock contention with git commands
|
|
// the user runs in a terminal (at the cost of not persisting git's refreshed
|
|
// stat-cache).
|
|
Background bool
|
|
}
|
|
|
|
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
|
// check if config wants us ignoring untracked files
|
|
untrackedFilesSetting := self.config.GetShowUntrackedFiles()
|
|
|
|
if opts.ForceShowUntracked || untrackedFilesSetting == "" {
|
|
untrackedFilesSetting = "all"
|
|
}
|
|
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
|
|
|
|
statuses, err := self.gitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg, Background: opts.Background})
|
|
if err != nil {
|
|
self.Log.Error(err)
|
|
}
|
|
files := []*models.File{}
|
|
|
|
fileDiffs := map[string]FileDiff{}
|
|
if self.GitCommon.Common.UserConfig().Gui.ShowNumstatInFilesView {
|
|
fileDiffs, err = self.getFileDiffs()
|
|
if err != nil {
|
|
self.Log.Error(err)
|
|
}
|
|
}
|
|
|
|
for _, status := range statuses {
|
|
if strings.HasPrefix(status.StatusString, "warning") {
|
|
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
|
|
continue
|
|
}
|
|
|
|
file := &models.File{
|
|
Path: status.Path,
|
|
PreviousPath: status.PreviousPath,
|
|
DisplayString: status.StatusString,
|
|
}
|
|
|
|
if diff, ok := fileDiffs[status.Path]; ok {
|
|
file.LinesAdded = diff.LinesAdded
|
|
file.LinesDeleted = diff.LinesDeleted
|
|
}
|
|
|
|
models.SetStatusFields(file, status.Change)
|
|
files = append(files, file)
|
|
}
|
|
|
|
// Go through the files to see if any of these files are actually worktrees
|
|
// so that we can render them correctly
|
|
worktreePaths := linkedWortkreePaths(self.Fs, self.repoPaths.RepoGitDirPath())
|
|
for _, file := range files {
|
|
for _, worktreePath := range worktreePaths {
|
|
absFilePath, err := filepath.Abs(file.Path)
|
|
if err != nil {
|
|
self.Log.Error(err)
|
|
continue
|
|
}
|
|
if absFilePath == worktreePath {
|
|
file.IsWorktree = true
|
|
// `git status` renders this worktree as a folder with a trailing slash but we'll represent it as a singular worktree
|
|
// If we include the slash, it will be rendered as a folder with a null file inside.
|
|
file.Path = strings.TrimSuffix(file.Path, "/")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
type FileDiff struct {
|
|
LinesAdded int
|
|
LinesDeleted int
|
|
}
|
|
|
|
func (self *FileLoader) getFileDiffs() (map[string]FileDiff, error) {
|
|
diffs, err := self.gitDiffNumStat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
splitLines := strings.Split(diffs, "\x00")
|
|
|
|
fileDiffs := map[string]FileDiff{}
|
|
for _, line := range splitLines {
|
|
splitLine := strings.Split(line, "\t")
|
|
if len(splitLine) != 3 {
|
|
continue
|
|
}
|
|
|
|
linesAdded, err := strconv.Atoi(splitLine[0])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
linesDeleted, err := strconv.Atoi(splitLine[1])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
fileName := splitLine[2]
|
|
fileDiffs[fileName] = FileDiff{
|
|
LinesAdded: linesAdded,
|
|
LinesDeleted: linesDeleted,
|
|
}
|
|
}
|
|
|
|
return fileDiffs, nil
|
|
}
|
|
|
|
// GitStatus returns the file status of the repo
|
|
type GitStatusOptions struct {
|
|
NoRenames bool
|
|
UntrackedFilesArg string
|
|
Background bool
|
|
}
|
|
|
|
type FileStatus struct {
|
|
StatusString string
|
|
Change string // ??, MM, AM, ...
|
|
Path string
|
|
PreviousPath string
|
|
}
|
|
|
|
func (self *FileLoader) gitDiffNumStat() (string, error) {
|
|
return self.cmd.New(
|
|
NewGitCmd("diff").
|
|
Arg("--numstat").
|
|
Arg("-z").
|
|
Arg("HEAD").
|
|
ToArgv(),
|
|
).DontLog().RunWithOutput()
|
|
}
|
|
|
|
func (self *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
|
cmdArgs := NewGitCmd("status").
|
|
GlobalArgIf(opts.Background, "--no-optional-locks").
|
|
Arg(opts.UntrackedFilesArg).
|
|
Arg("--porcelain").
|
|
Arg("-z").
|
|
ArgIfElse(
|
|
opts.NoRenames,
|
|
"--no-renames",
|
|
fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold),
|
|
).
|
|
ToArgv()
|
|
|
|
statusLines, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs()
|
|
if err != nil {
|
|
return []FileStatus{}, err
|
|
}
|
|
|
|
splitLines := strings.Split(statusLines, "\x00")
|
|
response := []FileStatus{}
|
|
|
|
for i := 0; i < len(splitLines); i++ {
|
|
original := splitLines[i]
|
|
|
|
if len(original) < 3 {
|
|
continue
|
|
}
|
|
|
|
status := FileStatus{
|
|
StatusString: original,
|
|
Change: original[:2],
|
|
Path: original[3:],
|
|
PreviousPath: "",
|
|
}
|
|
|
|
if strings.HasPrefix(status.Change, "R") || strings.HasPrefix(status.Change, "C") {
|
|
// if a line starts with 'R' (rename) or 'C' (copy) then the next line is the original file.
|
|
status.PreviousPath = splitLines[i+1]
|
|
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousPath, status.Path)
|
|
i++
|
|
}
|
|
|
|
response = append(response, status)
|
|
}
|
|
|
|
return response, nil
|
|
}
|