/
githubmirror
/
lazygit
Обзор
Документация
Войти
/
githubmirror
/
lazygit
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
pkg/commands/git_commands/file_loader.go
292 строки
8 KB
Stefan Haller
Honor the conflict-marker-size gitattribute
08 авг 2026, 13:43
08 авг 2026, 13:43
5481436
Код
Авторство
О чём код?
package git_commands import ( "fmt" "path/filepath" "strconv" "strings" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/samber/lo" ) 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 it // keeps the default suppression of optional locks (avoiding index.lock // contention with git commands the user runs in a terminal, at the cost of // not persisting git's refreshed stat-cache). A foreground status opts back // in; see gitStatus. 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) } self.setConflictMarkerSizes(files) // 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 } // Looks up how long the conflict markers in the conflicted files are. We ask // git for all of them at once, because spawning a process per file would be // painfully slow when hundreds of files are conflicted (especially on Windows). func (self *FileLoader) setConflictMarkerSizes(files []*models.File) { conflictedFiles := lo.Filter(files, func(file *models.File, _ int) bool { return file.HasInlineMergeConflicts }) if len(conflictedFiles) == 0 { return } paths := lo.Map(conflictedFiles, func(file *models.File, _ int) string { return file.Path }) markerSizes, err := self.getConflictMarkerSizes(paths) if err != nil { self.Log.Error(err) return } for _, file := range conflictedFiles { file.ConflictMarkerSize = markerSizes[file.Path] } } func (self *FileLoader) getConflictMarkerSizes(paths []string) (map[string]int, error) { cmdArgs := NewGitCmd("check-attr"). Arg("-z"). Arg("--stdin"). Arg("conflict-marker-size"). ToArgv() // -z makes git both read the paths and write its output NUL-separated, so // that paths containing newlines don't throw us off. output, _, err := self.cmd.New(cmdArgs). SetStdin(strings.Join(paths, "\x00")). DontLog(). RunWithOutputs() if err != nil { return nil, err } markerSizes := map[string]int{} fields := strings.Split(output, "\x00") // Each path yields a path/attribute/value triple; the value is either a // number or something like "unspecified", in which case we leave the marker // size at 0 to say that git's default applies. for i := 0; i+2 < len(fields); i += 3 { if markerSize, err := strconv.Atoi(fields[i+2]); err == nil && markerSize > 0 { markerSizes[fields[i]] = markerSize } } return markerSizes, nil } 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"). Arg(opts.UntrackedFilesArg). Arg("--porcelain"). Arg("-z"). ArgIfElse( opts.NoRenames, "--no-renames", fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold), ). ToArgv() cmdObj := self.cmd.New(cmdArgs).DontLog() if !opts.Background { // Every git command suppresses optional locks by default (see // OptionalLocksEnvVar). A foreground refresh is the one exception: we let // it take the lock so it persists git's refreshed stat-cache, which keeps // subsequent status calls fast. Background refreshes leave it suppressed so // they can't contend for index.lock. cmdObj.RemoveEnvVar(OptionalLocksEnvVar) } statusLines, _, err := cmdObj.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 }