1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-14 11:23:09 +02:00
lazygit/pkg/commands/git_commands/working_tree.go

378 lines
11 KiB
Go
Raw Normal View History

2022-01-08 05:00:36 +02:00
package git_commands
2022-01-02 01:34:33 +02:00
import (
"fmt"
"os"
"path/filepath"
2022-01-25 16:20:19 +02:00
"strings"
2022-01-02 01:34:33 +02:00
"time"
"github.com/go-errors/errors"
2022-03-19 07:34:46 +02:00
"github.com/jesseduffield/generics/slices"
2022-01-02 01:34:33 +02:00
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type WorkingTreeCommands struct {
*GitCommon
2022-01-02 01:34:33 +02:00
submodule *SubmoduleCommands
fileLoader *loaders.FileLoader
}
func NewWorkingTreeCommands(
gitCommon *GitCommon,
submodule *SubmoduleCommands,
2022-01-02 01:34:33 +02:00
fileLoader *loaders.FileLoader,
) *WorkingTreeCommands {
return &WorkingTreeCommands{
GitCommon: gitCommon,
submodule: submodule,
2022-01-02 01:34:33 +02:00
fileLoader: fileLoader,
}
}
func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj {
return self.cmd.New("git mergetool")
}
func (self *WorkingTreeCommands) OpenMergeTool() error {
return self.OpenMergeToolCmdObj().Run()
}
// StageFile stages a file
2022-01-25 16:20:19 +02:00
func (self *WorkingTreeCommands) StageFile(path string) error {
return self.StageFiles([]string{path})
}
func (self *WorkingTreeCommands) StageFiles(paths []string) error {
2022-03-19 07:34:46 +02:00
quotedPaths := slices.Map(paths, func(path string) string {
return self.cmd.Quote(path)
})
2022-01-25 16:20:19 +02:00
return self.cmd.New(fmt.Sprintf("git add -- %s", strings.Join(quotedPaths, " "))).Run()
2022-01-02 01:34:33 +02:00
}
// StageAll stages all files
func (self *WorkingTreeCommands) StageAll() error {
return self.cmd.New("git add -A").Run()
}
// UnstageAll unstages all files
func (self *WorkingTreeCommands) UnstageAll() error {
return self.cmd.New("git reset").Run()
}
// UnStageFile unstages a file
// we accept an array of filenames for the cases where a file has been renamed i.e.
// we accept the current name and the previous name
func (self *WorkingTreeCommands) UnStageFile(fileNames []string, reset bool) error {
command := "git rm --cached --force -- %s"
if reset {
command = "git reset HEAD -- %s"
}
for _, name := range fileNames {
err := self.cmd.New(fmt.Sprintf(command, self.cmd.Quote(name))).Run()
if err != nil {
return err
}
}
return nil
}
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) {
2022-01-02 01:34:33 +02:00
if !file.IsRename() {
return nil, nil, errors.New("Expected renamed file")
}
// we've got a file that represents a rename from one file to another. Here we will refetch
// all files, passing the --no-renames flag and then recursively call the function
// again for the before file and after file.
2022-01-08 05:00:36 +02:00
filesWithoutRenames := self.fileLoader.GetStatusFiles(loaders.GetStatusFileOptions{NoRenames: true})
2022-01-02 01:34:33 +02:00
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
if f.Name == file.PreviousName {
beforeFile = f
}
if f.Name == file.Name {
afterFile = f
}
}
if beforeFile == nil || afterFile == nil {
return nil, nil, errors.New("Could not find deleted file or new file for file rename")
}
if beforeFile.IsRename() || afterFile.IsRename() {
// probably won't happen but we want to ensure we don't get an infinite loop
return nil, nil, errors.New("Nested rename found")
}
return beforeFile, afterFile, nil
}
// DiscardAllFileChanges directly
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error {
2022-01-02 01:34:33 +02:00
if file.IsRename() {
2022-01-08 05:00:36 +02:00
beforeFile, afterFile, err := self.BeforeAndAfterFileForRename(file)
2022-01-02 01:34:33 +02:00
if err != nil {
return err
}
2022-01-08 05:00:36 +02:00
if err := self.DiscardAllFileChanges(beforeFile); err != nil {
2022-01-02 01:34:33 +02:00
return err
}
2022-01-08 05:00:36 +02:00
if err := self.DiscardAllFileChanges(afterFile); err != nil {
2022-01-02 01:34:33 +02:00
return err
}
return nil
}
2022-01-08 05:00:36 +02:00
quotedFileName := self.cmd.Quote(file.Name)
2022-01-02 01:34:33 +02:00
if file.ShortStatus == "AA" {
2022-01-08 05:00:36 +02:00
if err := self.cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil {
2022-01-02 01:34:33 +02:00
return err
}
2022-01-08 05:00:36 +02:00
if err := self.cmd.New("git add -- " + quotedFileName).Run(); err != nil {
2022-01-02 01:34:33 +02:00
return err
}
return nil
}
if file.ShortStatus == "DU" {
2022-01-08 05:00:36 +02:00
return self.cmd.New("git rm -- " + quotedFileName).Run()
2022-01-02 01:34:33 +02:00
}
// if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges || file.HasMergeConflicts {
2022-01-08 05:00:36 +02:00
if err := self.cmd.New("git reset -- " + quotedFileName).Run(); err != nil {
2022-01-02 01:34:33 +02:00
return err
}
}
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
return nil
}
if file.Added {
2022-01-08 05:00:36 +02:00
return self.os.RemoveFile(file.Name)
2022-01-02 01:34:33 +02:00
}
2022-01-08 05:00:36 +02:00
return self.DiscardUnstagedFileChanges(file)
2022-01-02 01:34:33 +02:00
}
2022-01-21 15:13:51 +02:00
type IFileNode interface {
ForEachFile(cb func(*models.File) error) error
GetFilePathsMatching(test func(*models.File) bool) []string
GetPath() string
}
func (self *WorkingTreeCommands) DiscardAllDirChanges(node IFileNode) error {
2022-01-02 01:34:33 +02:00
// this could be more efficient but we would need to handle all the edge cases
2022-01-08 05:00:36 +02:00
return node.ForEachFile(self.DiscardAllFileChanges)
2022-01-02 01:34:33 +02:00
}
2022-01-21 15:13:51 +02:00
func (self *WorkingTreeCommands) DiscardUnstagedDirChanges(node IFileNode) error {
2022-01-08 05:00:36 +02:00
if err := self.RemoveUntrackedDirFiles(node); err != nil {
2022-01-02 01:34:33 +02:00
return err
}
2022-01-08 05:00:36 +02:00
quotedPath := self.cmd.Quote(node.GetPath())
if err := self.cmd.New("git checkout -- " + quotedPath).Run(); err != nil {
2022-01-02 01:34:33 +02:00
return err
}
return nil
}
2022-01-21 15:13:51 +02:00
func (self *WorkingTreeCommands) RemoveUntrackedDirFiles(node IFileNode) error {
untrackedFilePaths := node.GetFilePathsMatching(
func(file *models.File) bool { return !file.GetIsTracked() },
2022-01-02 01:34:33 +02:00
)
for _, path := range untrackedFilePaths {
err := os.Remove(path)
if err != nil {
return err
}
}
return nil
}
// DiscardUnstagedFileChanges directly
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := self.cmd.Quote(file.Name)
return self.cmd.New("git checkout -- " + quotedFileName).Run()
2022-01-02 01:34:33 +02:00
}
// Ignore adds a file to the gitignore for the repo
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) Ignore(filename string) error {
return self.os.AppendLineToFile(".gitignore", filename)
2022-01-02 01:34:33 +02:00
}
// Exclude adds a file to the .git/info/exclude for the repo
func (self *WorkingTreeCommands) Exclude(filename string) error {
return self.os.AppendLineToFile(".git/info/exclude", filename)
}
2022-01-02 01:34:33 +02:00
// WorktreeFileDiff returns the diff of a file
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string {
2022-01-02 01:34:33 +02:00
// for now we assume an error means the file was deleted
2022-01-08 05:00:36 +02:00
s, _ := self.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace).RunWithOutput()
2022-01-02 01:34:33 +02:00
return s
}
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj {
2022-01-02 01:34:33 +02:00
cachedArg := ""
trackedArg := "--"
2022-01-08 05:00:36 +02:00
colorArg := self.UserConfig.Git.Paging.ColorArg
quotedPath := self.cmd.Quote(node.GetPath())
2022-04-05 16:35:41 +02:00
quotedPrevPath := ""
2022-01-02 01:34:33 +02:00
ignoreWhitespaceArg := ""
2022-01-08 05:00:36 +02:00
contextSize := self.UserConfig.Git.DiffContextSize
2022-01-02 01:34:33 +02:00
if cached {
2022-01-08 07:02:56 +02:00
cachedArg = " --cached"
2022-01-02 01:34:33 +02:00
}
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
trackedArg = "--no-index -- /dev/null"
}
if plain {
colorArg = "never"
}
if ignoreWhitespace {
2022-01-08 07:02:56 +02:00
ignoreWhitespaceArg = " --ignore-all-space"
2022-01-02 01:34:33 +02:00
}
2022-04-05 16:35:41 +02:00
if prevPath := node.GetPreviousPath(); prevPath != "" {
quotedPrevPath = " " + self.cmd.Quote(prevPath)
}
2022-01-02 01:34:33 +02:00
2022-04-05 16:35:41 +02:00
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s%s%s %s %s%s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath, quotedPrevPath)
2022-01-02 01:34:33 +02:00
2022-01-08 05:00:36 +02:00
return self.cmd.New(cmdStr).DontLog()
2022-01-02 01:34:33 +02:00
}
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) ApplyPatch(patch string, flags ...string) error {
2022-05-04 14:20:45 +02:00
filepath, err := self.SaveTemporaryPatch(patch)
if err != nil {
2022-01-02 01:34:33 +02:00
return err
}
2022-05-04 14:20:45 +02:00
return self.ApplyPatchFile(filepath, flags...)
}
func (self *WorkingTreeCommands) ApplyPatchFile(filepath string, flags ...string) error {
2022-01-02 01:34:33 +02:00
flagStr := ""
for _, flag := range flags {
flagStr += " --" + flag
}
2022-01-08 05:00:36 +02:00
return self.cmd.New(fmt.Sprintf("git apply%s %s", flagStr, self.cmd.Quote(filepath))).Run()
2022-01-02 01:34:33 +02:00
}
2022-05-04 14:20:45 +02:00
func (self *WorkingTreeCommands) SaveTemporaryPatch(patch string) (string, error) {
filepath := filepath.Join(self.os.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
self.Log.Infof("saving temporary patch to %s", filepath)
if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
return "", err
}
return filepath, nil
}
2022-01-02 01:34:33 +02:00
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
2022-01-02 01:34:33 +02:00
}
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
colorArg := self.UserConfig.Git.Paging.ColorArg
contextSize := self.UserConfig.Git.DiffContextSize
2022-01-02 01:34:33 +02:00
if plain {
colorArg = "never"
}
reverseFlag := ""
if reverse {
2022-01-08 07:02:56 +02:00
reverseFlag = " -R"
2022-01-02 01:34:33 +02:00
}
2022-01-08 05:00:36 +02:00
return self.cmd.
2022-01-02 01:34:33 +02:00
New(
fmt.Sprintf(
2022-01-08 07:02:56 +02:00
"git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s%s%s%s -- %s",
contextSize, colorArg, pad(from), pad(to), reverseFlag, self.cmd.Quote(fileName)),
2022-01-02 01:34:33 +02:00
).
DontLog()
}
// CheckoutFile checks out the file for the given commit
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) CheckoutFile(commitSha, fileName string) error {
return self.cmd.New(fmt.Sprintf("git checkout %s -- %s", commitSha, self.cmd.Quote(fileName))).Run()
2022-01-02 01:34:33 +02:00
}
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) DiscardAnyUnstagedFileChanges() error {
return self.cmd.New("git checkout -- .").Run()
2022-01-02 01:34:33 +02:00
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) RemoveTrackedFiles(name string) error {
return self.cmd.New("git rm -r --cached -- " + self.cmd.Quote(name)).Run()
2022-01-02 01:34:33 +02:00
}
// RemoveUntrackedFiles runs `git clean -fd`
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) RemoveUntrackedFiles() error {
return self.cmd.New("git clean -fd").Run()
2022-01-02 01:34:33 +02:00
}
// ResetAndClean removes all unstaged changes and removes all untracked files
2022-01-08 05:00:36 +02:00
func (self *WorkingTreeCommands) ResetAndClean() error {
submoduleConfigs, err := self.submodule.GetConfigs()
2022-01-02 01:34:33 +02:00
if err != nil {
return err
}
if len(submoduleConfigs) > 0 {
2022-01-08 05:00:36 +02:00
if err := self.submodule.ResetSubmodules(submoduleConfigs); err != nil {
2022-01-02 01:34:33 +02:00
return err
}
}
2022-01-08 05:00:36 +02:00
if err := self.ResetHard("HEAD"); err != nil {
2022-01-02 01:34:33 +02:00
return err
}
2022-01-08 05:00:36 +02:00
return self.RemoveUntrackedFiles()
2022-01-02 01:34:33 +02:00
}
// ResetHardHead runs `git reset --hard`
func (self *WorkingTreeCommands) ResetHard(ref string) error {
return self.cmd.New("git reset --hard " + self.cmd.Quote(ref)).Run()
}
// ResetSoft runs `git reset --soft HEAD`
func (self *WorkingTreeCommands) ResetSoft(ref string) error {
return self.cmd.New("git reset --soft " + self.cmd.Quote(ref)).Run()
}
func (self *WorkingTreeCommands) ResetMixed(ref string) error {
return self.cmd.New("git reset --mixed " + self.cmd.Quote(ref)).Run()
}
2022-01-08 07:02:56 +02:00
// so that we don't have unnecessary space in our commands we use this helper function to prepend spaces to args so that in the format string we can go '%s%s%s' and if any args are missing we won't have gaps.
func pad(str string) string {
if str == "" {
return ""
}
return " " + str
}