1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-12 11:15:00 +02:00
lazygit/pkg/commands/files.go

364 lines
10 KiB
Go
Raw Normal View History

2020-09-29 12:03:39 +02:00
package commands
import (
"fmt"
2021-08-23 15:35:19 +02:00
"io/ioutil"
2021-03-15 13:29:34 +02:00
"os"
2020-09-29 12:03:39 +02:00
"path/filepath"
"strconv"
2020-09-29 12:03:39 +02:00
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
2021-03-21 06:58:15 +02:00
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
2020-09-29 12:03:39 +02:00
"github.com/jesseduffield/lazygit/pkg/utils"
)
// CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) {
2021-08-23 15:35:19 +02:00
buf, err := ioutil.ReadFile(fileName)
if err != nil {
return "", nil
}
return string(buf), nil
2020-09-29 12:03:39 +02:00
}
2021-04-11 02:05:39 +02:00
func (c *GitCommand) OpenMergeToolCmd() string {
return "git mergetool"
}
func (c *GitCommand) OpenMergeTool() error {
return c.OSCommand.RunCommand("git mergetool")
}
2020-09-29 12:03:39 +02:00
// StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error {
return c.RunCommand("git add -- %s", c.OSCommand.Quote(fileName))
2020-09-29 12:03:39 +02:00
}
// StageAll stages all files
func (c *GitCommand) StageAll() error {
return c.RunCommand("git add -A")
2020-09-29 12:03:39 +02:00
}
2021-03-16 00:07:00 +02:00
// UnstageAll unstages all files
2020-09-29 12:03:39 +02:00
func (c *GitCommand) UnstageAll() error {
return c.RunCommand("git reset")
2020-09-29 12:03:39 +02:00
}
// UnStageFile unstages a file
2021-03-20 23:41:06 +02:00
// 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 (c *GitCommand) UnStageFile(fileNames []string, reset bool) error {
command := "git rm --cached --force -- %s"
2021-03-20 23:41:06 +02:00
if reset {
command = "git reset HEAD -- %s"
2020-09-29 12:03:39 +02:00
}
for _, name := range fileNames {
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
return err
}
}
return nil
}
func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) {
if !file.IsRename() {
return nil, nil, errors.New("Expected renamed file")
}
2021-03-20 23:41:06 +02:00
// we've got a file that represents a rename from one file to another. Here we will refetch
2020-09-29 12:03:39 +02:00
// all files, passing the --no-renames flag and then recursively call the function
2021-03-20 23:41:06 +02:00
// again for the before file and after file.
2020-09-29 12:03:39 +02:00
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
2021-03-20 23:41:06 +02:00
if f.Name == file.PreviousName {
2020-09-29 12:03:39 +02:00
beforeFile = f
}
2021-03-20 23:41:06 +02:00
if f.Name == file.Name {
2020-09-29 12:03:39 +02:00
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
func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
if file.IsRename() {
beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file)
if err != nil {
return err
}
if err := c.DiscardAllFileChanges(beforeFile); err != nil {
return err
}
if err := c.DiscardAllFileChanges(afterFile); err != nil {
return err
}
return nil
}
quotedFileName := c.OSCommand.Quote(file.Name)
2021-03-20 03:07:11 +02:00
if file.ShortStatus == "AA" {
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
2021-03-20 03:07:11 +02:00
return err
}
if err := c.RunCommand("git add -- %s", quotedFileName); err != nil {
2021-03-20 03:07:11 +02:00
return err
}
return nil
}
if file.ShortStatus == "DU" {
return c.RunCommand("git rm -- %s", quotedFileName)
2021-03-20 03:07:11 +02:00
}
// if the file isn't tracked, we assume you want to delete it
2020-09-29 12:03:39 +02:00
if file.HasStagedChanges || file.HasMergeConflicts {
if err := c.RunCommand("git reset -- %s", quotedFileName); err != nil {
2020-09-29 12:03:39 +02:00
return err
}
}
2021-03-20 03:07:11 +02:00
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
return nil
}
if file.Added {
return c.OSCommand.RemoveFile(file.Name)
2020-09-29 12:03:39 +02:00
}
return c.DiscardUnstagedFileChanges(file)
}
func (c *GitCommand) DiscardAllDirChanges(node *filetree.FileNode) error {
2021-03-20 04:49:43 +02:00
// this could be more efficient but we would need to handle all the edge cases
return node.ForEachFile(c.DiscardAllFileChanges)
2021-03-15 13:29:34 +02:00
}
func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
2021-03-15 13:29:34 +02:00
if err := c.RemoveUntrackedDirFiles(node); err != nil {
return err
}
quotedPath := c.OSCommand.Quote(node.GetPath())
if err := c.RunCommand("git checkout -- %s", quotedPath); err != nil {
2021-03-15 13:29:34 +02:00
return err
}
return nil
}
func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
2021-03-15 13:29:34 +02:00
untrackedFilePaths := node.GetPathsMatching(
func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() },
2021-03-15 13:29:34 +02:00
)
for _, path := range untrackedFilePaths {
err := os.Remove(path)
if err != nil {
return err
}
}
return nil
}
2020-09-29 12:03:39 +02:00
// DiscardUnstagedFileChanges directly
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
return c.RunCommand("git checkout -- %s", quotedFileName)
2020-09-29 12:03:39 +02:00
}
// Ignore adds a file to the gitignore for the repo
func (c *GitCommand) Ignore(filename string) error {
return c.OSCommand.AppendLineToFile(".gitignore", filename)
}
// WorktreeFileDiff returns the diff of a file
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string {
2020-09-29 12:03:39 +02:00
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached, ignoreWhitespace))
2020-09-29 12:03:39 +02:00
return s
}
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) string {
2020-09-29 12:03:39 +02:00
cachedArg := ""
2021-03-02 06:36:04 +02:00
trackedArg := "--"
2020-09-29 12:03:39 +02:00
colorArg := c.colorArg()
quotedPath := c.OSCommand.Quote(node.GetPath())
ignoreWhitespaceArg := ""
2020-09-29 12:03:39 +02:00
if cached {
cachedArg = "--cached"
}
2021-03-14 09:46:22 +02:00
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
2021-03-02 06:36:04 +02:00
trackedArg = "--no-index -- /dev/null"
2020-09-29 12:03:39 +02:00
}
if plain {
colorArg = "never"
}
if ignoreWhitespace {
ignoreWhitespaceArg = "--ignore-all-space"
}
2020-09-29 12:03:39 +02:00
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s %s", colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
2020-09-29 12:03:39 +02:00
}
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
2021-09-27 15:39:20 +02:00
filepath := filepath.Join(c.Config.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
2020-09-29 12:03:39 +02:00
c.Log.Infof("saving temporary patch to %s", filepath)
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
return err
}
flagStr := ""
for _, flag := range flags {
flagStr += " --" + flag
}
return c.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
2020-09-29 12:03:39 +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.
func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
cmdStr := c.ShowFileDiffCmdStr(from, to, reverse, fileName, plain)
return c.OSCommand.RunCommandWithOutput(cmdStr)
}
func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fileName string, plain bool) string {
colorArg := c.colorArg()
if plain {
colorArg = "never"
}
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, c.OSCommand.Quote(fileName))
2020-09-29 12:03:39 +02:00
}
// CheckoutFile checks out the file for the given commit
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
2021-08-13 14:49:40 +02:00
return c.RunCommand("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName))
2020-09-29 12:03:39 +02:00
}
// DiscardOldFileChanges discards changes to a file from an old commit
func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := c.RunCommand("git cat-file -e HEAD^:%s", c.OSCommand.Quote(fileName)); err != nil {
2020-09-29 12:03:39 +02:00
if err := c.OSCommand.Remove(fileName); err != nil {
return err
}
if err := c.StageFile(fileName); err != nil {
return err
}
} else if err := c.CheckoutFile("HEAD^", fileName); err != nil {
return err
}
// amend the commit
2021-04-10 03:40:42 +02:00
err := c.AmendHead()
2020-09-29 12:03:39 +02:00
if err != nil {
return err
}
// continue
return c.GenericMergeOrRebaseAction("rebase", "continue")
}
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
return c.RunCommand("git checkout -- .")
2020-09-29 12:03:39 +02:00
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (c *GitCommand) RemoveTrackedFiles(name string) error {
return c.RunCommand("git rm -r --cached -- %s", c.OSCommand.Quote(name))
2020-09-29 12:03:39 +02:00
}
// RemoveUntrackedFiles runs `git clean -fd`
func (c *GitCommand) RemoveUntrackedFiles() error {
return c.RunCommand("git clean -fd")
2020-09-29 12:03:39 +02:00
}
// ResetAndClean removes all unstaged changes and removes all untracked files
func (c *GitCommand) ResetAndClean() error {
submoduleConfigs, err := c.GetSubmoduleConfigs()
if err != nil {
return err
}
if len(submoduleConfigs) > 0 {
2020-10-01 14:13:32 +02:00
if err := c.ResetSubmodules(submoduleConfigs); err != nil {
2020-09-29 12:03:39 +02:00
return err
}
}
if err := c.ResetHard("HEAD"); err != nil {
return err
}
return c.RemoveUntrackedFiles()
}
func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, error) {
2021-05-20 08:44:58 +02:00
editor := c.Config.GetUserConfig().OS.EditCommand
if editor == "" {
editor = c.GetConfigValue("core.editor")
}
2021-04-01 11:27:06 +02:00
if editor == "" {
editor = c.OSCommand.Getenv("GIT_EDITOR")
}
if editor == "" {
editor = c.OSCommand.Getenv("VISUAL")
}
if editor == "" {
editor = c.OSCommand.Getenv("EDITOR")
}
if editor == "" {
if err := c.OSCommand.RunCommand("which vi"); err == nil {
editor = "vi"
}
}
if editor == "" {
2021-06-01 08:23:33 +02:00
return "", errors.New("No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
2021-04-10 03:40:42 +02:00
}
templateValues := map[string]string{
"editor": editor,
"filename": c.OSCommand.Quote(filename),
"line": strconv.Itoa(lineNumber),
}
2021-08-04 11:43:34 +02:00
editCmdTemplate := c.Config.GetUserConfig().OS.EditCommandTemplate
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
2021-04-10 03:40:42 +02:00
}