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 {
2022-01-18 12:26:21 +02:00
* GitCommon
2022-01-02 01:34:33 +02:00
submodule * SubmoduleCommands
fileLoader * loaders . FileLoader
}
func NewWorkingTreeCommands (
2022-01-18 12:26:21 +02:00
gitCommon * GitCommon ,
submodule * SubmoduleCommands ,
2022-01-02 01:34:33 +02:00
fileLoader * loaders . FileLoader ,
) * WorkingTreeCommands {
return & WorkingTreeCommands {
2022-01-18 12:26:21 +02:00
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
}
2022-06-24 23:10:13 +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
}