Support building and moving patches

WIP
This commit is contained in:
Jesse Duffield 2019-11-04 19:47:25 +11:00
parent a3c84296bf
commit d5e443e8e3
28 changed files with 992 additions and 349 deletions

View file

@ -1,10 +1,9 @@
package git package commands
import ( import (
"regexp" "regexp"
"strings" "strings"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -26,28 +25,28 @@ import (
// BranchListBuilder returns a list of Branch objects for the current repo // BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct { type BranchListBuilder struct {
Log *logrus.Entry Log *logrus.Entry
GitCommand *commands.GitCommand GitCommand *GitCommand
} }
// NewBranchListBuilder builds a new branch list builder // NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*BranchListBuilder, error) { func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand) (*BranchListBuilder, error) {
return &BranchListBuilder{ return &BranchListBuilder{
Log: log, Log: log,
GitCommand: gitCommand, GitCommand: gitCommand,
}, nil }, nil
} }
func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch { func (b *BranchListBuilder) obtainCurrentBranch() *Branch {
branchName, err := b.GitCommand.CurrentBranchName() branchName, err := b.GitCommand.CurrentBranchName()
if err != nil { if err != nil {
panic(err.Error()) panic(err.Error())
} }
return &commands.Branch{Name: strings.TrimSpace(branchName)} return &Branch{Name: strings.TrimSpace(branchName)}
} }
func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch { func (b *BranchListBuilder) obtainReflogBranches() []*Branch {
branches := make([]*commands.Branch, 0) branches := make([]*Branch, 0)
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
if err != nil { if err != nil {
return branches return branches
@ -57,14 +56,14 @@ func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
for _, line := range branchLines { for _, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line) timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit) timeUnit = abbreviatedTimeUnit(timeUnit)
branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit} branch := &Branch{Name: branchName, Recency: timeNumber + timeUnit}
branches = append(branches, branch) branches = append(branches, branch)
} }
return uniqueByName(branches) return uniqueByName(branches)
} }
func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch { func (b *BranchListBuilder) obtainSafeBranches() []*Branch {
branches := make([]*commands.Branch, 0) branches := make([]*Branch, 0)
bIter, err := b.GitCommand.Repo.Branches() bIter, err := b.GitCommand.Repo.Branches()
if err != nil { if err != nil {
@ -72,14 +71,14 @@ func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
} }
bIter.ForEach(func(b *plumbing.Reference) error { bIter.ForEach(func(b *plumbing.Reference) error {
name := b.Name().Short() name := b.Name().Short()
branches = append(branches, &commands.Branch{Name: name}) branches = append(branches, &Branch{Name: name})
return nil return nil
}) })
return branches return branches
} }
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*commands.Branch, included bool) []*commands.Branch { func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*Branch, included bool) []*Branch {
for _, newBranch := range newBranches { for _, newBranch := range newBranches {
if included == branchIncluded(newBranch.Name, existingBranches) { if included == branchIncluded(newBranch.Name, existingBranches) {
finalBranches = append(finalBranches, newBranch) finalBranches = append(finalBranches, newBranch)
@ -88,7 +87,7 @@ func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existi
return finalBranches return finalBranches
} }
func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands.Branch) string { func sanitisedReflogName(reflogBranch *Branch, safeBranches []*Branch) string {
for _, safeBranch := range safeBranches { for _, safeBranch := range safeBranches {
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) { if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
return safeBranch.Name return safeBranch.Name
@ -98,8 +97,8 @@ func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands
} }
// Build the list of branches for the current repo // Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*commands.Branch { func (b *BranchListBuilder) Build() []*Branch {
branches := make([]*commands.Branch, 0) branches := make([]*Branch, 0)
head := b.obtainCurrentBranch() head := b.obtainCurrentBranch()
safeBranches := b.obtainSafeBranches() safeBranches := b.obtainSafeBranches()
@ -112,7 +111,7 @@ func (b *BranchListBuilder) Build() []*commands.Branch {
branches = b.appendNewBranches(branches, safeBranches, branches, false) branches = b.appendNewBranches(branches, safeBranches, branches, false)
if len(branches) == 0 || branches[0].Name != head.Name { if len(branches) == 0 || branches[0].Name != head.Name {
branches = append([]*commands.Branch{head}, branches...) branches = append([]*Branch{head}, branches...)
} }
branches[0].Recency = " *" branches[0].Recency = " *"
@ -120,7 +119,7 @@ func (b *BranchListBuilder) Build() []*commands.Branch {
return branches return branches
} }
func branchIncluded(branchName string, branches []*commands.Branch) bool { func branchIncluded(branchName string, branches []*Branch) bool {
for _, existingBranch := range branches { for _, existingBranch := range branches {
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) { if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
return true return true
@ -129,8 +128,8 @@ func branchIncluded(branchName string, branches []*commands.Branch) bool {
return false return false
} }
func uniqueByName(branches []*commands.Branch) []*commands.Branch { func uniqueByName(branches []*Branch) []*Branch {
finalBranches := make([]*commands.Branch, 0) finalBranches := make([]*Branch, 0)
for _, branch := range branches { for _, branch := range branches {
if branchIncluded(branch.Name, finalBranches) { if branchIncluded(branch.Name, finalBranches) {
continue continue

View file

@ -1,13 +1,42 @@
package commands package commands
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/theme"
)
// CommitFile : A git commit file // CommitFile : A git commit file
type CommitFile struct { type CommitFile struct {
Sha string Sha string
Name string Name string
DisplayString string DisplayString string
Status int // one of 'WHOLE' 'PART' 'NONE'
} }
const (
// UNSELECTED is for when the commit file has not been added to the patch in any way
UNSELECTED = iota
// WHOLE is for when you want to add the whole diff of a file to the patch,
// including e.g. if it was deleted
WHOLE = iota
// PART is for when you're only talking about specific lines that have been modified
PART
)
// GetDisplayStrings is a function. // GetDisplayStrings is a function.
func (f *CommitFile) GetDisplayStrings(isFocused bool) []string { func (f *CommitFile) GetDisplayStrings(isFocused bool) []string {
return []string{f.DisplayString} yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
defaultColor := color.New(theme.DefaultTextColor)
var colour *color.Color
switch f.Status {
case UNSELECTED:
colour = defaultColor
case WHOLE:
colour = green
case PART:
colour = yellow
}
return []string{colour.Sprint(f.DisplayString)}
} }

View file

@ -1,4 +1,4 @@
package git package commands
import ( import (
"fmt" "fmt"
@ -9,7 +9,6 @@ import (
"strings" "strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -27,15 +26,15 @@ import (
// CommitListBuilder returns a list of Branch objects for the current repo // CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct { type CommitListBuilder struct {
Log *logrus.Entry Log *logrus.Entry
GitCommand *commands.GitCommand GitCommand *GitCommand
OSCommand *commands.OSCommand OSCommand *OSCommand
Tr *i18n.Localizer Tr *i18n.Localizer
CherryPickedCommits []*commands.Commit CherryPickedCommits []*Commit
DiffEntries []*commands.Commit DiffEntries []*Commit
} }
// NewCommitListBuilder builds a new commit list builder // NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, osCommand *commands.OSCommand, tr *i18n.Localizer, cherryPickedCommits []*commands.Commit, diffEntries []*commands.Commit) (*CommitListBuilder, error) { func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit, diffEntries []*Commit) (*CommitListBuilder, error) {
return &CommitListBuilder{ return &CommitListBuilder{
Log: log, Log: log,
GitCommand: gitCommand, GitCommand: gitCommand,
@ -47,9 +46,9 @@ func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, os
} }
// GetCommits obtains the commits of the current branch // GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) { func (c *CommitListBuilder) GetCommits() ([]*Commit, error) {
commits := []*commands.Commit{} commits := []*Commit{}
var rebasingCommits []*commands.Commit var rebasingCommits []*Commit
rebaseMode, err := c.GitCommand.RebaseMode() rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil { if err != nil {
return nil, err return nil, err
@ -74,7 +73,7 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
sha := splitLine[0] sha := splitLine[0]
_, unpushed := unpushedCommits[sha] _, unpushed := unpushedCommits[sha]
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed] status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
commits = append(commits, &commands.Commit{ commits = append(commits, &Commit{
Sha: sha, Sha: sha,
Name: strings.Join(splitLine[1:], " "), Name: strings.Join(splitLine[1:], " "),
Status: status, Status: status,
@ -110,7 +109,7 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
} }
// getRebasingCommits obtains the commits that we're in the process of rebasing // getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.Commit, error) { func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*Commit, error) {
switch rebaseMode { switch rebaseMode {
case "normal": case "normal":
return c.getNormalRebasingCommits() return c.getNormalRebasingCommits()
@ -121,7 +120,7 @@ func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.C
} }
} }
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, error) { func (c *CommitListBuilder) getNormalRebasingCommits() ([]*Commit, error) {
rewrittenCount := 0 rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-apply/rewritten", c.GitCommand.DotGitDir)) bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-apply/rewritten", c.GitCommand.DotGitDir))
if err == nil { if err == nil {
@ -130,7 +129,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, erro
} }
// we know we're rebasing, so lets get all the files whose names have numbers // we know we're rebasing, so lets get all the files whose names have numbers
commits := []*commands.Commit{} commits := []*Commit{}
err = filepath.Walk(fmt.Sprintf("%s/rebase-apply", c.GitCommand.DotGitDir), func(path string, f os.FileInfo, err error) error { err = filepath.Walk(fmt.Sprintf("%s/rebase-apply", c.GitCommand.DotGitDir), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 { if rewrittenCount > 0 {
rewrittenCount-- rewrittenCount--
@ -152,7 +151,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, erro
if err != nil { if err != nil {
return err return err
} }
commits = append([]*commands.Commit{commit}, commits...) commits = append([]*Commit{commit}, commits...)
return nil return nil
}) })
if err != nil { if err != nil {
@ -174,7 +173,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, erro
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files // getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go // and extracts out the sha and names of commits that we still have to go
// in the rebase: // in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, error) { func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.GitCommand.DotGitDir)) bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.GitCommand.DotGitDir))
if err != nil { if err != nil {
c.Log.Info(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error())) c.Log.Info(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
@ -182,14 +181,14 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit,
return nil, nil return nil, nil
} }
commits := []*commands.Commit{} commits := []*Commit{}
lines := strings.Split(string(bytesContent), "\n") lines := strings.Split(string(bytesContent), "\n")
for _, line := range lines { for _, line := range lines {
if line == "" || line == "noop" { if line == "" || line == "noop" {
return commits, nil return commits, nil
} }
splitLine := strings.Split(line, " ") splitLine := strings.Split(line, " ")
commits = append([]*commands.Commit{{ commits = append([]*Commit{{
Sha: splitLine[1][0:7], Sha: splitLine[1][0:7],
Name: strings.Join(splitLine[2:], " "), Name: strings.Join(splitLine[2:], " "),
Status: "rebasing", Status: "rebasing",
@ -205,18 +204,18 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit,
// From: Lazygit Tester <test@example.com> // From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100 // Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master // Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*commands.Commit, error) { func (c *CommitListBuilder) commitFromPatch(content string) (*Commit, error) {
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1][0:7] sha := strings.Split(lines[0], " ")[1][0:7]
name := strings.TrimPrefix(lines[3], "Subject: ") name := strings.TrimPrefix(lines[3], "Subject: ")
return &commands.Commit{ return &Commit{
Sha: sha, Sha: sha,
Name: name, Name: name,
Status: "rebasing", Status: "rebasing",
}, nil }, nil
} }
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) ([]*commands.Commit, error) { func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
ancestor, err := c.getMergeBase() ancestor, err := c.getMergeBase()
if err != nil { if err != nil {
return nil, err return nil, err
@ -239,7 +238,7 @@ func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit)
return commits, nil return commits, nil
} }
func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*commands.Commit) ([]*commands.Commit, error) { func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*Commit) ([]*Commit, error) {
for _, commit := range commits { for _, commit := range commits {
for _, cherryPickedCommit := range c.CherryPickedCommits { for _, cherryPickedCommit := range c.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha { if commit.Sha == cherryPickedCommit.Sha {

View file

@ -1,24 +1,23 @@
package git package commands
import ( import (
"os/exec" "os/exec"
"testing" "testing"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing // NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder { func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := commands.NewDummyOSCommand() osCommand := NewDummyOSCommand()
return &CommitListBuilder{ return &CommitListBuilder{
Log: commands.NewDummyLog(), Log: NewDummyLog(),
GitCommand: commands.NewDummyGitCommandWithOSCommand(osCommand), GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand, OSCommand: osCommand,
Tr: i18n.NewLocalizer(commands.NewDummyLog()), Tr: i18n.NewLocalizer(NewDummyLog()),
CherryPickedCommits: []*commands.Commit{}, CherryPickedCommits: []*Commit{},
} }
} }
@ -199,7 +198,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string
command func(string, ...string) *exec.Cmd command func(string, ...string) *exec.Cmd
test func([]*commands.Commit, error) test func([]*Commit, error)
} }
scenarios := []scenario{ scenarios := []scenario{
@ -225,7 +224,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
return nil return nil
}, },
func(commits []*commands.Commit, err error) { func(commits []*Commit, err error) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, commits, 0) assert.Len(t, commits, 0)
}, },
@ -252,10 +251,10 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
return nil return nil
}, },
func(commits []*commands.Commit, err error) { func(commits []*Commit, err error) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, commits, 2) assert.Len(t, commits, 2)
assert.EqualValues(t, []*commands.Commit{ assert.EqualValues(t, []*Commit{
{ {
Sha: "8a2bb0e", Sha: "8a2bb0e",
Name: "commit 1", Name: "commit 1",
@ -298,7 +297,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
return nil return nil
}, },
func(commits []*commands.Commit, err error) { func(commits []*Commit, err error) {
assert.Error(t, err) assert.Error(t, err)
assert.Len(t, commits, 0) assert.Len(t, commits, 0)
}, },

View file

@ -63,16 +63,17 @@ func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repositor
// GitCommand is our main git interface // GitCommand is our main git interface
type GitCommand struct { type GitCommand struct {
Log *logrus.Entry Log *logrus.Entry
OSCommand *OSCommand OSCommand *OSCommand
Worktree *gogit.Worktree Worktree *gogit.Worktree
Repo *gogit.Repository Repo *gogit.Repository
Tr *i18n.Localizer Tr *i18n.Localizer
Config config.AppConfigurer Config config.AppConfigurer
getGlobalGitConfig func(string) (string, error) getGlobalGitConfig func(string) (string, error)
getLocalGitConfig func(string) (string, error) getLocalGitConfig func(string) (string, error)
removeFile func(string) error removeFile func(string) error
DotGitDir string DotGitDir string
onSuccessfulContinue func() error
} }
// NewGitCommand it runs git commands // NewGitCommand it runs git commands
@ -376,7 +377,7 @@ func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
// AmendHead amends HEAD with whatever is staged in your working tree // AmendHead amends HEAD with whatever is staged in your working tree
func (c *GitCommand) AmendHead() (*exec.Cmd, error) { func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
command := "git commit --amend --no-edit" command := "git commit --amend --no-edit --allow-empty"
if c.usingGpg() { if c.usingGpg() {
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
} }
@ -530,7 +531,7 @@ func (c *GitCommand) Ignore(filename string) error {
// Show shows the diff of a commit // Show shows the diff of a commit
func (c *GitCommand) Show(sha string) (string, error) { func (c *GitCommand) Show(sha string) (string, error) {
show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha)) show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color --no-renames %s", sha))
if err != nil { if err != nil {
return "", err return "", err
} }
@ -605,11 +606,11 @@ func (c *GitCommand) Diff(file *File, plain bool, cached bool) string {
return s return s
} }
func (c *GitCommand) ApplyPatch(patch string, reverse bool, cached bool) (string, error) { func (c *GitCommand) ApplyPatch(patch string, reverse bool, cached bool, extraFlags string) error {
filename, err := c.OSCommand.CreateTempFile("patch", patch) filename, err := c.OSCommand.CreateTempFile("patch", patch)
if err != nil { if err != nil {
c.Log.Error(err) c.Log.Error(err)
return "", err return err
} }
defer func() { _ = c.OSCommand.Remove(filename) }() defer func() { _ = c.OSCommand.Remove(filename) }()
@ -624,7 +625,7 @@ func (c *GitCommand) ApplyPatch(patch string, reverse bool, cached bool) (string
cachedFlag = "--cached" cachedFlag = "--cached"
} }
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply %s %s %s", cachedFlag, reverseFlag, c.OSCommand.Quote(filename))) return c.OSCommand.RunCommand(fmt.Sprintf("git apply %s %s %s %s", cachedFlag, reverseFlag, extraFlags, c.OSCommand.Quote(filename)))
} }
func (c *GitCommand) FastForward(branchName string) error { func (c *GitCommand) FastForward(branchName string) error {
@ -645,13 +646,29 @@ func (c *GitCommand) RunSkipEditorCommand(command string) error {
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue" // GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
// By default we skip the editor in the case where a commit will be made // By default we skip the editor in the case where a commit will be made
func (c *GitCommand) GenericMerge(commandType string, command string) error { func (c *GitCommand) GenericMerge(commandType string, command string) error {
return c.RunSkipEditorCommand( err := c.RunSkipEditorCommand(
fmt.Sprintf( fmt.Sprintf(
"git %s --%s", "git %s --%s",
commandType, commandType,
command, command,
), ),
) )
if err != nil {
return err
}
// sometimes we need to do a sequence of things in a rebase but the user needs to
// fix merge conflicts along the way. When this happens we queue up the next step
// so that after the next successful rebase continue we can continue from where we left off
if commandType == "rebase" && command == "continue" && c.onSuccessfulContinue != nil {
f := c.onSuccessfulContinue
c.onSuccessfulContinue = nil
return f()
}
if command == "abort" {
c.onSuccessfulContinue = nil
}
return nil
} }
func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, error) { func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, error) {
@ -852,8 +869,8 @@ func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
} }
// GetCommitFiles get the specified commit files // GetCommitFiles get the specified commit files
func (c *GitCommand) GetCommitFiles(commitSha string) ([]*CommitFile, error) { func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager) ([]*CommitFile, error) {
cmd := fmt.Sprintf("git show --pretty= --name-only %s", commitSha) cmd := fmt.Sprintf("git show --pretty= --name-only --no-renames %s", commitSha)
files, err := c.OSCommand.RunCommandWithOutput(cmd) files, err := c.OSCommand.RunCommandWithOutput(cmd)
if err != nil { if err != nil {
return nil, err return nil, err
@ -862,10 +879,16 @@ func (c *GitCommand) GetCommitFiles(commitSha string) ([]*CommitFile, error) {
commitFiles := make([]*CommitFile, 0) commitFiles := make([]*CommitFile, 0)
for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") { for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") {
status := UNSELECTED
if patchManager != nil && patchManager.CommitSha == commitSha {
status = patchManager.GetFileStatus(file)
}
commitFiles = append(commitFiles, &CommitFile{ commitFiles = append(commitFiles, &CommitFile{
Sha: commitSha, Sha: commitSha,
Name: file, Name: file,
DisplayString: file, DisplayString: file,
Status: status,
}) })
} }
@ -873,8 +896,12 @@ func (c *GitCommand) GetCommitFiles(commitSha string) ([]*CommitFile, error) {
} }
// ShowCommitFile get the diff of specified commit file // ShowCommitFile get the diff of specified commit file
func (c *GitCommand) ShowCommitFile(commitSha, fileName string) (string, error) { func (c *GitCommand) ShowCommitFile(commitSha, fileName string, plain bool) (string, error) {
cmd := fmt.Sprintf("git show --color %s -- %s", commitSha, fileName) colorArg := "--color"
if plain {
colorArg = ""
}
cmd := fmt.Sprintf("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName)
return c.OSCommand.RunCommandWithOutput(cmd) return c.OSCommand.RunCommandWithOutput(cmd)
} }
@ -886,28 +913,7 @@ func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
// DiscardOldFileChanges discards changes to a file from an old commit // DiscardOldFileChanges discards changes to a file from an old commit
func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, fileName string) error { func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, fileName string) error {
if len(commits)-1 < commitIndex { if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return errors.New("index outside of range of commits")
}
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if c.usingGpg() {
return errors.New(c.Tr.SLocalize("DisabledForGPG"))
}
todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
if err != nil {
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
return err return err
} }
@ -924,7 +930,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, f
} }
// amend the commit // amend the commit
cmd, err = c.AmendHead() cmd, err := c.AmendHead()
if cmd != nil { if cmd != nil {
return errors.New("received unexpected pointer to cmd") return errors.New("received unexpected pointer to cmd")
} }
@ -1016,3 +1022,34 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
return nil return nil
} }
// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current
// commit and pick all others. After this you'll want to call `c.GenericMerge("rebase", "continue")`
func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*Commit, commitIndex int) error {
if len(commits)-1 < commitIndex {
return errors.New("index outside of range of commits")
}
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if c.usingGpg() {
return errors.New(c.Tr.SLocalize("DisabledForGPG"))
}
todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
if err != nil {
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
return err
}
return nil
}

View file

@ -1685,7 +1685,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string
command func(string, ...string) *exec.Cmd command func(string, ...string) *exec.Cmd
test func(string, error) test func(error)
} }
scenarios := []scenario{ scenarios := []scenario{
@ -1702,9 +1702,8 @@ func TestGitCommandApplyPatch(t *testing.T) {
return exec.Command("echo", "done") return exec.Command("echo", "done")
}, },
func(output string, err error) { func(err error) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, "done\n", output)
}, },
}, },
{ {
@ -1724,7 +1723,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
return exec.Command("test") return exec.Command("test")
}, },
func(output string, err error) { func(err error) {
assert.Error(t, err) assert.Error(t, err)
}, },
}, },
@ -1734,7 +1733,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand() gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command gitCmd.OSCommand.command = s.command
s.test(gitCmd.ApplyPatch("test", false, true)) s.test(gitCmd.ApplyPatch("test", false, true, ""))
}) })
} }
} }
@ -1962,7 +1961,7 @@ func TestGitCommandShowCommitFile(t *testing.T) {
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.command = s.command gitCmd.OSCommand.command = s.command
s.test(gitCmd.ShowCommitFile(s.commitSha, s.fileName)) s.test(gitCmd.ShowCommitFile(s.commitSha, s.fileName, true))
}) })
} }
} }
@ -2001,7 +2000,7 @@ func TestGitCommandGetCommitFiles(t *testing.T) {
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.command = s.command gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetCommitFiles(s.commitSha)) s.test(gitCmd.GetCommitFiles(s.commitSha, nil))
}) })
} }
} }

View file

@ -0,0 +1,194 @@
package commands
import (
"sort"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type fileInfo struct {
mode int // one of WHOLE/PART
includedLineIndices []int
diff string
}
type applyPatchFunc func(patch string, reverse bool, cached bool, extraFlags string) error
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit)
type PatchManager struct {
CommitSha string
fileInfoMap map[string]*fileInfo
Log *logrus.Entry
ApplyPatch applyPatchFunc
}
// NewPatchManager returns a new PatchModifier
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc, commitSha string, diffMap map[string]string) *PatchManager {
infoMap := map[string]*fileInfo{}
for filename, diff := range diffMap {
infoMap[filename] = &fileInfo{
mode: UNSELECTED,
diff: diff,
}
}
return &PatchManager{
Log: log,
fileInfoMap: infoMap,
CommitSha: commitSha,
ApplyPatch: applyPatch,
}
}
func (p *PatchManager) AddFile(filename string) {
p.fileInfoMap[filename].mode = WHOLE
p.fileInfoMap[filename].includedLineIndices = nil
}
func (p *PatchManager) RemoveFile(filename string) {
p.fileInfoMap[filename].mode = UNSELECTED
p.fileInfoMap[filename].includedLineIndices = nil
}
func (p *PatchManager) ToggleFileWhole(filename string) {
info := p.fileInfoMap[filename]
switch info.mode {
case UNSELECTED:
p.AddFile(filename)
case WHOLE:
p.RemoveFile(filename)
case PART:
p.AddFile(filename)
}
}
func getIndicesForRange(first, last int) []int {
indices := []int{}
for i := first; i <= last; i++ {
indices = append(indices, i)
}
return indices
}
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
info := p.fileInfoMap[filename]
info.mode = PART
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
}
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
info := p.fileInfoMap[filename]
info.mode = PART
info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
if len(info.includedLineIndices) == 0 {
p.RemoveFile(filename)
}
}
func (p *PatchManager) RenderPlainPatchForFile(filename string, reverse bool) string {
info := p.fileInfoMap[filename]
if info == nil {
return ""
}
switch info.mode {
case WHOLE:
// use the whole diff
// the reverse flag is only for part patches so we're ignoring it here
return info.diff
case PART:
// generate a new diff with just the selected lines
m := NewPatchModifier(p.Log, filename, info.diff)
return m.ModifiedPatchForLines(info.includedLineIndices, reverse, true)
default:
return ""
}
}
func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool) string {
patch := p.RenderPlainPatchForFile(filename, reverse)
if plain {
return patch
}
parser, err := NewPatchParser(p.Log, patch)
if err != nil {
// swallowing for now
return ""
}
// not passing included lines because we don't want to see them in the secondary panel
return parser.Render(-1, -1, nil)
}
func (p *PatchManager) RenderEachFilePatch(plain bool) []string {
// sort files by name then iterate through and render each patch
filenames := make([]string, len(p.fileInfoMap))
index := 0
for filename := range p.fileInfoMap {
filenames[index] = filename
index++
}
sort.Strings(filenames)
output := []string{}
for _, filename := range filenames {
patch := p.RenderPatchForFile(filename, plain, false)
if patch != "" {
output = append(output, patch)
}
}
return output
}
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
return strings.Join(p.RenderEachFilePatch(plain), "\n")
}
func (p *PatchManager) GetFileStatus(filename string) int {
info := p.fileInfoMap[filename]
if info == nil {
return UNSELECTED
}
return info.mode
}
func (p *PatchManager) GetFileIncLineIndices(filename string) []int {
info := p.fileInfoMap[filename]
if info == nil {
return []int{}
}
return info.includedLineIndices
}
func (p *PatchManager) ApplyPatches(reverse bool) error {
// for whole patches we'll apply the patch in reverse
// but for part patches we'll apply a reverse patch forwards
for filename, info := range p.fileInfoMap {
if info.mode == UNSELECTED {
continue
}
reverseOnGenerate := false
reverseOnApply := false
if reverse {
if info.mode == WHOLE {
reverseOnApply = true
} else {
reverseOnGenerate = true
}
}
patch := p.RenderPatchForFile(filename, true, reverseOnGenerate)
if patch == "" {
continue
}
p.Log.Warn(patch)
if err := p.ApplyPatch(patch, reverseOnApply, false, "--index --3way"); err != nil {
return err
}
}
return nil
}

View file

@ -1,4 +1,4 @@
package git package commands
import ( import (
"fmt" "fmt"
@ -10,7 +10,8 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var headerRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`) var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
type PatchHunk struct { type PatchHunk struct {
header string header string
@ -116,7 +117,7 @@ func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, rev
} }
// get oldstart, newstart, and heading from header // get oldstart, newstart, and heading from header
match := headerRegexp.FindStringSubmatch(hunk.header) match := hunkHeaderRegexp.FindStringSubmatch(hunk.header)
var oldStart int var oldStart int
if reverse { if reverse {
@ -152,9 +153,17 @@ func mustConvertToInt(s string) int {
return i return i
} }
func GetHeaderFromDiff(diff string) string {
match := patchHeaderRegexp.FindStringSubmatch(diff)
if len(match) <= 1 {
return ""
}
return match[1]
}
func GetHunksFromDiff(diff string) []*PatchHunk { func GetHunksFromDiff(diff string) []*PatchHunk {
headers := headerRegexp.FindAllString(diff, -1) headers := hunkHeaderRegexp.FindAllString(diff, -1)
bodies := headerRegexp.Split(diff, -1)[1:] // discarding top bit bodies := hunkHeaderRegexp.Split(diff, -1)[1:] // discarding top bit
headerFirstLineIndices := []int{} headerFirstLineIndices := []int{}
for lineIdx, line := range strings.Split(diff, "\n") { for lineIdx, line := range strings.Split(diff, "\n") {
@ -175,6 +184,7 @@ type PatchModifier struct {
Log *logrus.Entry Log *logrus.Entry
filename string filename string
hunks []*PatchHunk hunks []*PatchHunk
header string
} }
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier { func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
@ -182,10 +192,11 @@ func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *Patc
Log: log, Log: log,
filename: filename, filename: filename,
hunks: GetHunksFromDiff(diffText), hunks: GetHunksFromDiff(diffText),
header: GetHeaderFromDiff(diffText),
} }
} }
func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool) string { func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string {
// step one is getting only those hunks which we care about // step one is getting only those hunks which we care about
hunksInRange := []*PatchHunk{} hunksInRange := []*PatchHunk{}
outer: outer:
@ -212,21 +223,38 @@ outer:
return "" return ""
} }
fileHeader := fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename) var fileHeader string
// for staging/unstaging lines we don't want the original header because
// it makes git confused e.g. when dealing with deleted/added files
// but with building and applying patches the original header gives git
// information it needs to cleanly apply patches
if keepOriginalHeader {
fileHeader = d.header
} else {
fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
}
return fileHeader + formattedHunks return fileHeader + formattedHunks
} }
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool) string { func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
// generate array of consecutive line indices from our range // generate array of consecutive line indices from our range
selectedLines := []int{} selectedLines := []int{}
for i := firstLineIdx; i <= lastLineIdx; i++ { for i := firstLineIdx; i <= lastLineIdx; i++ {
selectedLines = append(selectedLines, i) selectedLines = append(selectedLines, i)
} }
return d.ModifiedPatchForLines(selectedLines, reverse) return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader)
} }
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool) string { func (d *PatchModifier) OriginalPatchLength() int {
p := NewPatchModifier(log, filename, diffText) if len(d.hunks) == 0 {
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse) return 0
}
return d.hunks[len(d.hunks)-1].LastLineIdx
}
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
} }

View file

@ -1,4 +1,4 @@
package git package commands
import ( import (
"fmt" "fmt"
@ -502,7 +502,7 @@ func TestModifyPatchForRange(t *testing.T) {
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex, s.reverse) result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex, s.reverse, true)
if !assert.Equal(t, s.expected, result) { if !assert.Equal(t, s.expected, result) {
fmt.Println(result) fmt.Println(result)
} }

View file

@ -1,4 +1,4 @@
package git package commands
import ( import (
"regexp" "regexp"
@ -12,6 +12,8 @@ import (
const ( const (
PATCH_HEADER = iota PATCH_HEADER = iota
COMMIT_SHA
COMMIT_DESCRIPTION
HUNK_HEADER HUNK_HEADER
ADDITION ADDITION
DELETION DELETION
@ -81,7 +83,10 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
return p.PatchHunks[0] return p.PatchHunks[0]
} }
func (l *PatchLine) render(selected bool) string { // selected means you've got it highlighted with your cursor
// included means the line has been included in the patch (only applicable when
// building a patch)
func (l *PatchLine) render(selected bool, included bool) string {
content := l.Content content := l.Content
if len(content) == 0 { if len(content) == 0 {
content = " " // using the space so that we can still highlight if necessary content = " " // using the space so that we can still highlight if necessary
@ -91,7 +96,7 @@ func (l *PatchLine) render(selected bool) string {
if l.Kind == HUNK_HEADER { if l.Kind == HUNK_HEADER {
re := regexp.MustCompile("(@@.*?@@)(.*)") re := regexp.MustCompile("(@@.*?@@)(.*)")
match := re.FindStringSubmatch(content) match := re.FindStringSubmatch(content)
return coloredString(color.FgCyan, match[1], selected) + coloredString(theme.DefaultTextColor, match[2], selected) return coloredString(color.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
} }
var colorAttr color.Attribute var colorAttr color.Attribute
@ -102,21 +107,34 @@ func (l *PatchLine) render(selected bool) string {
colorAttr = color.FgGreen colorAttr = color.FgGreen
case DELETION: case DELETION:
colorAttr = color.FgRed colorAttr = color.FgRed
case COMMIT_SHA:
colorAttr = color.FgYellow
default: default:
colorAttr = theme.DefaultTextColor colorAttr = theme.DefaultTextColor
} }
return coloredString(colorAttr, content, selected) return coloredString(colorAttr, content, selected, included)
} }
func coloredString(colorAttr color.Attribute, str string, selected bool) string { func coloredString(colorAttr color.Attribute, str string, selected bool, included bool) string {
var cl *color.Color var cl *color.Color
attributes := []color.Attribute{colorAttr}
if selected { if selected {
cl = color.New(colorAttr, color.BgBlue) attributes = append(attributes, color.BgBlue)
} else {
cl = color.New(colorAttr)
} }
return utils.ColoredStringDirect(str, cl) cl = color.New(attributes...)
var clIncluded *color.Color
if included {
clIncluded = color.New(append(attributes, color.BgGreen)...)
} else {
clIncluded = color.New(attributes...)
}
if len(str) < 2 {
return utils.ColoredStringDirect(str, clIncluded)
}
return utils.ColoredStringDirect(str[:1], clIncluded) + utils.ColoredStringDirect(str[1:], cl)
} }
func parsePatch(patch string) ([]int, []int, []*PatchLine, error) { func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
@ -124,16 +142,26 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
hunkStarts := []int{} hunkStarts := []int{}
stageableLines := []int{} stageableLines := []int{}
pastFirstHunkHeader := false pastFirstHunkHeader := false
pastCommitDescription := true
patchLines := make([]*PatchLine, len(lines)) patchLines := make([]*PatchLine, len(lines))
var lineKind int var lineKind int
var firstChar string var firstChar string
for index, line := range lines { for index, line := range lines {
lineKind = PATCH_HEADER
firstChar = " " firstChar = " "
if len(line) > 0 { if len(line) > 0 {
firstChar = line[:1] firstChar = line[:1]
} }
if firstChar == "@" { if index == 0 && strings.HasPrefix(line, "commit") {
lineKind = COMMIT_SHA
pastCommitDescription = false
} else if !pastCommitDescription {
if strings.HasPrefix(line, "diff") || strings.HasPrefix(line, "---") {
pastCommitDescription = true
lineKind = PATCH_HEADER
} else {
lineKind = COMMIT_DESCRIPTION
}
} else if firstChar == "@" {
pastFirstHunkHeader = true pastFirstHunkHeader = true
hunkStarts = append(hunkStarts, index) hunkStarts = append(hunkStarts, index)
lineKind = HUNK_HEADER lineKind = HUNK_HEADER
@ -150,6 +178,8 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
case " ": case " ":
lineKind = CONTEXT lineKind = CONTEXT
} }
} else {
lineKind = PATCH_HEADER
} }
patchLines[index] = &PatchLine{Kind: lineKind, Content: line} patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
} }
@ -157,11 +187,12 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
} }
// Render returns the coloured string of the diff with any selected lines highlighted // Render returns the coloured string of the diff with any selected lines highlighted
func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int) string { func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
renderedLines := make([]string, len(p.PatchLines)) renderedLines := make([]string, len(p.PatchLines))
for index, patchLine := range p.PatchLines { for index, patchLine := range p.PatchLines {
selected := index >= firstLineIndex && index <= lastLineIndex selected := index >= firstLineIndex && index <= lastLineIndex
renderedLines[index] = patchLine.render(selected) included := utils.IncludesInt(incLineIndices, index)
renderedLines[index] = patchLine.render(selected, included)
} }
return strings.Join(renderedLines, "\n") return strings.Join(renderedLines, "\n")
} }

View file

@ -0,0 +1,153 @@
package commands
import "github.com/go-errors/errors"
// DeletePatchesFromCommit applies a patch in reverse for a commit
func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// apply each patch in reverse
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// time to amend the selected commit
if _, err := c.AmendHead(); err != nil {
return err
}
// continue
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *PatchManager) error {
if sourceCommitIdx < destinationCommitIdx {
if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
return err
}
// apply each patch forward
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the destination commit
if _, err := c.AmendHead(); err != nil {
return err
}
// continue
return c.GenericMerge("rebase", "continue")
}
if len(commits)-1 < sourceCommitIdx {
return errors.New("index outside of range of commits")
}
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if c.usingGpg() {
return errors.New(c.Tr.SLocalize("DisabledForGPG"))
}
baseIndex := sourceCommitIdx + 1
todo := ""
for i, commit := range commits[0:baseIndex] {
a := "pick"
if i == sourceCommitIdx || i == destinationCommitIdx {
a = "edit"
}
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
if err != nil {
return err
}
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
return err
}
// apply each patch in reverse
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the source commit
if _, err := c.AmendHead(); err != nil {
return err
}
if c.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
c.onSuccessfulContinue = func() error {
// now we should be up to the destination, so let's apply forward these patches to that.
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the destination commit
if _, err := c.AmendHead(); err != nil {
return err
}
return c.GenericMerge("rebase", "continue")
}
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the commit
if _, err := c.AmendHead(); err != nil {
return err
}
if c.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
c.onSuccessfulContinue = func() error {
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
return nil
}
return c.GenericMerge("rebase", "continue")
}

View file

@ -1,7 +0,0 @@
diff --git a/blah b/blah
new file mode 100644
index 0000000..907b308
--- /dev/null
+++ b/blah
@@ -0,0 +1 @@
+blah

View file

@ -1,13 +0,0 @@
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
index 60ec4e0..db4485d 100644
--- a/pkg/git/branch_list_builder.go
+++ b/pkg/git/branch_list_builder.go
@@ -14,8 +14,7 @@ import (
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
-// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way

View file

@ -1,14 +0,0 @@
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
index 60ec4e0..db4485d 100644
--- a/pkg/git/branch_list_builder.go
+++ b/pkg/git/branch_list_builder.go
@@ -14,8 +14,9 @@ import (
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
+// test 2 - if I remove this, I decrement the end counter
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way

View file

@ -1,25 +0,0 @@
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
index a8fc600..6d8f7d7 100644
--- a/pkg/git/patch_modifier.go
+++ b/pkg/git/patch_modifier.go
@@ -36,18 +36,19 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
hunkEnd = hunkStarts[nextHunkStartIndex]
}
headerLength := 4
output := strings.Join(lines[0:headerLength], "\n") + "\n"
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
return output, nil
}
+func getHeaderLength(patchLines []string) (int, error) {
// ModifyPatchForLine takes the original patch, which may contain several hunks,
// and the line number of the line we want to stage
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
lines := strings.Split(patch, "\n")
headerLength := 4
output := strings.Join(lines[0:headerLength], "\n") + "\n"
hunkStart, err := p.getHunkStart(lines, lineNumber)

View file

@ -1,19 +0,0 @@
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
index a8fc600..6d8f7d7 100644
--- a/pkg/git/patch_modifier.go
+++ b/pkg/git/patch_modifier.go
@@ -124,13 +140,14 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
// @@ -14,8 +14,9 @@ import (
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
// current counter is the number after the second comma
re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
matches := re.FindStringSubmatch(currentHeader)
if len(matches) < 2 {
re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
matches = re.FindStringSubmatch(currentHeader)
}
prevLengthString := matches[1]
+ prevLengthString := re.FindStringSubmatch(currentHeader)[1]
prevLength, err := strconv.Atoi(prevLengthString)
if err != nil {

View file

@ -1,15 +0,0 @@
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
index 60ec4e0..db4485d 100644
--- a/pkg/git/branch_list_builder.go
+++ b/pkg/git/branch_list_builder.go
@@ -14,8 +14,8 @@ import (
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
-// which `git branch -a` gives us, but we also want the recency data that
-// git reflog gives us.
+// test 2 - if I remove this, I decrement the end counter
+// test
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way

View file

@ -1,57 +0,0 @@
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
index a8fc600..6d8f7d7 100644
--- a/pkg/git/patch_modifier.go
+++ b/pkg/git/patch_modifier.go
@@ -36,18 +36,34 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
hunkEnd = hunkStarts[nextHunkStartIndex]
}
- headerLength := 4
+ headerLength, err := getHeaderLength(lines)
+ if err != nil {
+ return "", err
+ }
+
output := strings.Join(lines[0:headerLength], "\n") + "\n"
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
return output, nil
}
+func getHeaderLength(patchLines []string) (int, error) {
+ for index, line := range patchLines {
+ if strings.HasPrefix(line, "@@") {
+ return index, nil
+ }
+ }
+ return 0, errors.New("Could not find any hunks in this patch")
+}
+
// ModifyPatchForLine takes the original patch, which may contain several hunks,
// and the line number of the line we want to stage
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
lines := strings.Split(patch, "\n")
- headerLength := 4
+ headerLength, err := getHeaderLength(lines)
+ if err != nil {
+ return "", err
+ }
output := strings.Join(lines[0:headerLength], "\n") + "\n"
hunkStart, err := p.getHunkStart(lines, lineNumber)
@@ -124,13 +140,8 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
// @@ -14,8 +14,9 @@ import (
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
// current counter is the number after the second comma
- re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
- matches := re.FindStringSubmatch(currentHeader)
- if len(matches) < 2 {
- re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
- matches = re.FindStringSubmatch(currentHeader)
- }
- prevLengthString := matches[1]
+ re := regexp.MustCompile(`(\d+) @@`)
+ prevLengthString := re.FindStringSubmatch(currentHeader)[1]
prevLength, err := strconv.Atoi(prevLengthString)
if err != nil {

View file

@ -6,7 +6,6 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/git"
) )
// list panel functions // list panel functions
@ -67,7 +66,7 @@ func (gui *Gui) RenderSelectedBranchUpstreamDifferences() error {
// be sure there is a state.Branches array to pick the current branch from // be sure there is a state.Branches array to pick the current branch from
func (gui *Gui) refreshBranches(g *gocui.Gui) error { func (gui *Gui) refreshBranches(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error { g.Update(func(g *gocui.Gui) error {
builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand) builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,6 +1,7 @@
package gui package gui
import ( import (
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
) )
@ -23,7 +24,7 @@ func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error {
if err := gui.focusPoint(0, gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles), v); err != nil { if err := gui.focusPoint(0, gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles), v); err != nil {
return err return err
} }
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name) commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, false)
if err != nil { if err != nil {
return err return err
} }
@ -79,16 +80,19 @@ func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) refreshCommitFilesView() error { func (gui *Gui) refreshCommitFilesView() error {
if err := gui.refreshPatchPanel(); err != nil {
return err
}
commit := gui.getSelectedCommit(gui.g) commit := gui.getSelectedCommit(gui.g)
if commit == nil { if commit == nil {
return nil return nil
} }
files, err := gui.GitCommand.GetCommitFiles(commit.Sha) files, err := gui.GitCommand.GetCommitFiles(commit.Sha, gui.State.PatchManager)
if err != nil { if err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(gui.g, err.Error())
} }
gui.State.CommitFiles = files gui.State.CommitFiles = files
gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles)) gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles))
@ -104,3 +108,82 @@ func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedCommitFile(g) file := gui.getSelectedCommitFile(g)
return gui.openFile(file.Name) return gui.openFile(file.Name)
} }
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
commitFile := gui.getSelectedCommitFile(g)
if commitFile == nil {
return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
toggleTheFile := func() error {
if gui.State.PatchManager == nil {
if err := gui.createPatchManager(); err != nil {
return err
}
}
gui.State.PatchManager.ToggleFileWhole(commitFile.Name)
return gui.refreshCommitFilesView()
}
if gui.State.PatchManager != nil && gui.State.PatchManager.CommitSha != commitFile.Sha {
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
gui.State.PatchManager = nil
return toggleTheFile()
}, nil)
}
return toggleTheFile()
}
func (gui *Gui) createPatchManager() error {
diffMap := map[string]string{}
for _, commitFile := range gui.State.CommitFiles {
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
if err != nil {
return err
}
diffMap[commitFile.Name] = commitText
}
commit := gui.getSelectedCommit(gui.g)
if commit == nil {
return errors.New("No commit selected")
}
gui.State.PatchManager = commands.NewPatchManager(gui.Log, gui.GitCommand.ApplyPatch, commit.Sha, diffMap)
return nil
}
func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error {
commitFile := gui.getSelectedCommitFile(g)
if commitFile == nil {
return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
enterTheFile := func() error {
if gui.State.PatchManager == nil {
if err := gui.createPatchManager(); err != nil {
return err
}
}
if err := gui.changeContext("main", "staging"); err != nil {
return err
}
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
return err
}
return gui.refreshStagingPanel()
}
if gui.State.PatchManager != nil && gui.State.PatchManager.CommitSha != commitFile.Sha {
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
gui.State.PatchManager = nil
return enterTheFile()
}, nil)
}
return enterTheFile()
}

View file

@ -9,7 +9,6 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
@ -53,9 +52,26 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
return gui.renderString(g, "main", commitText) return gui.renderString(g, "main", commitText)
} }
func (gui *Gui) refreshPatchPanel() error {
if gui.State.PatchManager != nil {
gui.State.SplitMainPanel = true
secondaryView := gui.getSecondaryView()
secondaryView.Highlight = true
secondaryView.Wrap = false
gui.g.Update(func(*gocui.Gui) error {
return gui.setViewContent(gui.g, gui.getSecondaryView(), gui.State.PatchManager.RenderAggregatedPatchColored(false))
})
} else {
gui.State.SplitMainPanel = false
}
return nil
}
func (gui *Gui) refreshCommits(g *gocui.Gui) error { func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error { g.Update(func(*gocui.Gui) error {
builder, err := git.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries) builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries)
if err != nil { if err != nil {
return err return err
} }
@ -65,6 +81,10 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
} }
gui.State.Commits = commits gui.State.Commits = commits
if err := gui.refreshPatchPanel(); err != nil {
return err
}
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits)) gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
isFocused := gui.g.CurrentView().Name() == "commits" isFocused := gui.g.CurrentView().Name() == "commits"

View file

@ -23,7 +23,6 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates" "github.com/jesseduffield/lazygit/pkg/updates"
@ -84,13 +83,13 @@ type Gui struct {
// non-mutative, so that we don't accidentally end up // non-mutative, so that we don't accidentally end up
// with mismatches of data. We might change this in the future // with mismatches of data. We might change this in the future
type stagingPanelState struct { type stagingPanelState struct {
SelectedLineIdx int SelectedLineIdx int
FirstLineIdx int FirstLineIdx int
LastLineIdx int LastLineIdx int
Diff string Diff string
PatchParser *git.PatchParser PatchParser *commands.PatchParser
SelectMode int // one of LINE, HUNK, or RANGE SelectMode int // one of LINE, HUNK, or RANGE
IndexFocused bool // this is for if we show the left or right panel SecondaryFocused bool // this is for if we show the left or right panel
} }
type mergingPanelState struct { type mergingPanelState struct {
@ -152,8 +151,11 @@ type guiState struct {
Contexts map[string]string Contexts map[string]string
CherryPickedCommits []*commands.Commit CherryPickedCommits []*commands.Commit
SplitMainPanel bool SplitMainPanel bool
PatchManager *commands.PatchManager
} }
// for now the split view will always be on
// NewGui builds a new gui handler // NewGui builds a new gui handler
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) { func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) {
@ -390,7 +392,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
main := "main" main := "main"
secondary := "secondary" secondary := "secondary"
swappingMainPanels := gui.State.Panels.Staging != nil && gui.State.Panels.Staging.IndexFocused swappingMainPanels := gui.State.Panels.Staging != nil && gui.State.Panels.Staging.SecondaryFocused
if swappingMainPanels { if swappingMainPanels {
main = "secondary" main = "secondary"
secondary = "main" secondary = "main"

View file

@ -142,6 +142,11 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: 'x', Key: 'x',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleCreateOptionsMenu, Handler: gui.handleCreateOptionsMenu,
}, {
ViewName: "",
Key: gocui.KeyCtrlP,
Modifier: gocui.ModNone,
Handler: gui.handleCreatePatchOptionsMenu,
}, { }, {
ViewName: "status", ViewName: "status",
Key: 'e', Key: 'e',
@ -523,6 +528,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleOpenOldCommitFile, Handler: gui.handleOpenOldCommitFile,
Description: gui.Tr.SLocalize("openFile"), Description: gui.Tr.SLocalize("openFile"),
}, },
{
ViewName: "commitFiles",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleToggleFileForPatch,
Description: gui.Tr.SLocalize("toggleAddToPatch"),
},
{
ViewName: "commitFiles",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleEnterCommitFile,
Description: gui.Tr.SLocalize("enterFile"),
},
} }
for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} { for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} {

View file

@ -0,0 +1,89 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
)
type patchMenuOption struct {
displayName string
function func() error
}
// GetDisplayStrings is a function.
func (o *patchMenuOption) GetDisplayStrings(isFocused bool) []string {
return []string{o.displayName}
}
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
m := gui.State.PatchManager
if m == nil {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoPatchError"))
}
options := []*patchMenuOption{
{displayName: "discard patch", function: gui.handleDeletePatchFromCommit},
{displayName: "pull patch out into index", function: gui.handlePullPatchIntoWorkingTree},
{displayName: "save patch to file"},
{displayName: "clear patch", function: gui.handleClearPatch},
}
selectedCommit := gui.getSelectedCommit(gui.g)
if selectedCommit != nil && gui.State.PatchManager.CommitSha != selectedCommit.Sha {
options = append(options, &patchMenuOption{
displayName: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
function: gui.handleMovePatchToSelectedCommit,
})
}
handleMenuPress := func(index int) error {
return options[index].function()
}
return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), options, len(options), handleMenuPress)
}
func (gui *Gui) getPatchCommitIndex() int {
for index, commit := range gui.State.Commits {
if commit.Sha == gui.State.PatchManager.CommitSha {
return index
}
}
return -1
}
func (gui *Gui) handleDeletePatchFromCommit() error {
// TODO: deal with when we're already rebasing
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.DeletePatchesFromCommit(gui.State.Commits, commitIndex, gui.State.PatchManager)
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleMovePatchToSelectedCommit() error {
// TODO: deal with when we're already rebasing
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLine, gui.State.PatchManager)
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handlePullPatchIntoWorkingTree() error {
// TODO: deal with when we're already rebasing
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.State.PatchManager)
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleClearPatch() error {
gui.State.PatchManager = nil
return gui.refreshCommitFilesView()
}

View file

@ -1,12 +1,11 @@
package gui package gui
import ( import (
"strings"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/git" "github.com/jesseduffield/lazygit/pkg/commands"
) )
// these represent what select mode we're in
const ( const (
LINE = iota LINE = iota
RANGE RANGE
@ -16,49 +15,65 @@ const (
func (gui *Gui) refreshStagingPanel() error { func (gui *Gui) refreshStagingPanel() error {
state := gui.State.Panels.Staging state := gui.State.Panels.Staging
file, err := gui.getSelectedFile(gui.g) // file, err := gui.getSelectedFile(gui.g)
if err != nil { // if err != nil {
if err != gui.Errors.ErrNoFiles { // if err != gui.Errors.ErrNoFiles {
return err // return err
} // }
return gui.handleStagingEscape(gui.g, nil) // return gui.handleStagingEscape(gui.g, nil)
} // }
gui.State.SplitMainPanel = true gui.State.SplitMainPanel = true
indexFocused := false secondaryFocused := false
if state != nil { if state != nil {
indexFocused = state.IndexFocused secondaryFocused = state.SecondaryFocused
} }
if !file.HasUnstagedChanges && !file.HasStagedChanges { // if !file.HasUnstagedChanges && !file.HasStagedChanges {
return gui.handleStagingEscape(gui.g, nil) // return gui.handleStagingEscape(gui.g, nil)
// }
// if (secondaryFocused && !file.HasStagedChanges) || (!secondaryFocused && !file.HasUnstagedChanges) {
// secondaryFocused = !secondaryFocused
// }
// getDiffs := func() (string, string) {
// // note for custom diffs, we'll need to send a flag here saying not to use the custom diff
// diff := gui.GitCommand.Diff(file, true, secondaryFocused)
// secondaryColorDiff := gui.GitCommand.Diff(file, false, !secondaryFocused)
// return diff, secondaryColorDiff
// }
// diff, secondaryColorDiff := getDiffs()
// // if we have e.g. a deleted file with nothing else to the diff will have only
// // 4-5 lines in which case we'll swap panels
// if len(strings.Split(diff, "\n")) < 5 {
// if len(strings.Split(secondaryColorDiff, "\n")) < 5 {
// return gui.handleStagingEscape(gui.g, nil)
// }
// secondaryFocused = !secondaryFocused
// diff, secondaryColorDiff = getDiffs()
// }
// get diff from commit file that's currently selected
commitFile := gui.getSelectedCommitFile(gui.g)
if commitFile == nil {
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
} }
if (indexFocused && !file.HasStagedChanges) || (!indexFocused && !file.HasUnstagedChanges) { diff, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
indexFocused = !indexFocused if err != nil {
return err
} }
getDiffs := func() (string, string) { secondaryColorDiff := gui.State.PatchManager.RenderPatchForFile(commitFile.Name, false, false)
// note for custom diffs, we'll need to send a flag here saying not to use the custom diff if err != nil {
diff := gui.GitCommand.Diff(file, true, indexFocused) return err
secondaryColorDiff := gui.GitCommand.Diff(file, false, !indexFocused)
return diff, secondaryColorDiff
} }
diff, secondaryColorDiff := getDiffs() patchParser, err := commands.NewPatchParser(gui.Log, diff)
// if we have e.g. a deleted file with nothing else to the diff will have only
// 4-5 lines in which case we'll swap panels
if len(strings.Split(diff, "\n")) < 5 {
if len(strings.Split(secondaryColorDiff, "\n")) < 5 {
return gui.handleStagingEscape(gui.g, nil)
}
indexFocused = !indexFocused
diff, secondaryColorDiff = getDiffs()
}
patchParser, err := git.NewPatchParser(gui.Log, diff)
if err != nil { if err != nil {
return nil return nil
} }
@ -92,13 +107,13 @@ func (gui *Gui) refreshStagingPanel() error {
} }
gui.State.Panels.Staging = &stagingPanelState{ gui.State.Panels.Staging = &stagingPanelState{
PatchParser: patchParser, PatchParser: patchParser,
SelectedLineIdx: selectedLineIdx, SelectedLineIdx: selectedLineIdx,
SelectMode: selectMode, SelectMode: selectMode,
FirstLineIdx: firstLineIdx, FirstLineIdx: firstLineIdx,
LastLineIdx: lastLineIdx, LastLineIdx: lastLineIdx,
Diff: diff, Diff: diff,
IndexFocused: indexFocused, SecondaryFocused: secondaryFocused,
} }
if err := gui.refreshView(); err != nil { if err := gui.refreshView(); err != nil {
@ -123,14 +138,14 @@ func (gui *Gui) refreshStagingPanel() error {
func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.Staging state := gui.State.Panels.Staging
state.IndexFocused = !state.IndexFocused state.SecondaryFocused = !state.SecondaryFocused
return gui.refreshStagingPanel() return gui.refreshStagingPanel()
} }
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Staging = nil gui.State.Panels.Staging = nil
return gui.switchFocus(gui.g, nil, gui.getFilesView()) return gui.switchFocus(gui.g, nil, gui.getCommitFilesView())
} }
func (gui *Gui) handleStagingPrevLine(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleStagingPrevLine(g *gocui.Gui, v *gocui.View) error {
@ -203,7 +218,9 @@ func (gui *Gui) handleCycleLine(change int) error {
func (gui *Gui) refreshView() error { func (gui *Gui) refreshView() error {
state := gui.State.Panels.Staging state := gui.State.Panels.Staging
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx) filename := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, gui.State.PatchManager.GetFileIncLineIndices(filename))
mainView := gui.getMainView() mainView := gui.getMainView()
mainView.Highlight = true mainView.Highlight = true
@ -258,17 +275,57 @@ func (gui *Gui) focusSelection(includeCurrentHunk bool) error {
} }
func (gui *Gui) handleStageSelection(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleStageSelection(g *gocui.Gui, v *gocui.View) error {
return gui.applySelection(false) state := gui.State.Panels.Staging
// add range of lines to those set for the file
commitFile := gui.getSelectedCommitFile(gui.g)
if commitFile == nil {
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
gui.State.PatchManager.AddFileLineRange(commitFile.Name, state.FirstLineIdx, state.LastLineIdx)
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
if err := gui.refreshStagingPanel(); err != nil {
return err
}
return nil
// return gui.applySelection(false)
} }
func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
return gui.applySelection(true) state := gui.State.Panels.Staging
// add range of lines to those set for the file
commitFile := gui.getSelectedCommitFile(gui.g)
if commitFile == nil {
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
gui.State.PatchManager.RemoveFileLineRange(commitFile.Name, state.FirstLineIdx, state.LastLineIdx)
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
if err := gui.refreshStagingPanel(); err != nil {
return err
}
return nil
// return gui.applySelection(true)
} }
func (gui *Gui) applySelection(reverse bool) error { func (gui *Gui) applySelection(reverse bool) error {
state := gui.State.Panels.Staging state := gui.State.Panels.Staging
if !reverse && state.IndexFocused { if !reverse && state.SecondaryFocused {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantStageStaged")) return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantStageStaged"))
} }
@ -277,7 +334,7 @@ func (gui *Gui) applySelection(reverse bool) error {
return err return err
} }
patch := git.ModifiedPatch(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse) patch := commands.ModifiedPatchForRange(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse, false)
if patch == "" { if patch == "" {
return nil return nil
@ -285,7 +342,7 @@ func (gui *Gui) applySelection(reverse bool) error {
// apply the patch then refresh this panel // apply the patch then refresh this panel
// create a new temp file with the patch, then call git apply with that patch // create a new temp file with the patch, then call git apply with that patch
_, err = gui.GitCommand.ApplyPatch(patch, false, !reverse || state.IndexFocused) err = gui.GitCommand.ApplyPatch(patch, false, !reverse || state.SecondaryFocused, "")
if err != nil { if err != nil {
return err return err
} }

View file

@ -368,13 +368,13 @@ func (gui *Gui) changeSelectedLine(line *int, total int, up bool) {
return return
} }
*line -= 1 *line--
} else { } else {
if *line == -1 || *line == total-1 { if *line == -1 || *line == total-1 {
return return
} }
*line += 1 *line++
} }
} }

View file

@ -794,6 +794,24 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "jump", ID: "jump",
Other: "jump to panel", Other: "jump to panel",
}, &i18n.Message{
ID: "DiscardPatch",
Other: "Discard Patch",
}, &i18n.Message{
ID: "DiscardPatchConfirm",
Other: "You can only build a patch from one commit at a time. Discard current patch?",
}, &i18n.Message{
ID: "toggleAddToPatch",
Other: "toggle file included in patch",
}, &i18n.Message{
ID: "PatchOptionsTitle",
Other: "Patch Options",
}, &i18n.Message{
ID: "NoPatchError",
Other: "No patch created yet. To start building a patch, use 'space' on a commit file or enter to add specific lines",
}, &i18n.Message{
ID: "enterFile",
Other: "enter file to add selected lines to the patch",
}, },
) )
} }

View file

@ -261,3 +261,41 @@ func AsJson(i interface{}) string {
bytes, _ := json.MarshalIndent(i, "", " ") bytes, _ := json.MarshalIndent(i, "", " ")
return string(bytes) return string(bytes)
} }
// UnionInt returns the union of two int arrays
func UnionInt(a, b []int) []int {
m := make(map[int]bool)
for _, item := range a {
m[item] = true
}
for _, item := range b {
if _, ok := m[item]; !ok {
// this does not mutate the original a slice
// though it does mutate the backing array I believe
// but that doesn't matter because if you later want to append to the
// original a it must see that the backing array has been changed
// and create a new one
a = append(a, item)
}
}
return a
}
// DifferenceInt returns the difference of two int arrays
func DifferenceInt(a, b []int) []int {
result := []int{}
m := make(map[int]bool)
for _, item := range b {
m[item] = true
}
for _, item := range a {
if _, ok := m[item]; !ok {
result = append(result, item)
}
}
return result
}