mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-13 05:15:53 +02:00
Support building and moving patches
WIP
This commit is contained in:
parent
a3c84296bf
commit
d5e443e8e3
28 changed files with 992 additions and 349 deletions
|
@ -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
|
|
@ -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)}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
|
@ -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)
|
||||||
},
|
},
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
194
pkg/commands/patch_manager.go
Normal file
194
pkg/commands/patch_manager.go
Normal 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
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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")
|
||||||
}
|
}
|
153
pkg/commands/patch_rebases.go
Normal file
153
pkg/commands/patch_rebases.go
Normal 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")
|
||||||
|
}
|
7
pkg/git/testdata/addedFile.diff
vendored
7
pkg/git/testdata/addedFile.diff
vendored
|
@ -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
|
|
13
pkg/git/testdata/testPatchAfter1.diff
vendored
13
pkg/git/testdata/testPatchAfter1.diff
vendored
|
@ -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
|
|
14
pkg/git/testdata/testPatchAfter2.diff
vendored
14
pkg/git/testdata/testPatchAfter2.diff
vendored
|
@ -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
|
|
25
pkg/git/testdata/testPatchAfter3.diff
vendored
25
pkg/git/testdata/testPatchAfter3.diff
vendored
|
@ -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)
|
|
||||||
|
|
19
pkg/git/testdata/testPatchAfter4.diff
vendored
19
pkg/git/testdata/testPatchAfter4.diff
vendored
|
@ -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 {
|
|
15
pkg/git/testdata/testPatchBefore.diff
vendored
15
pkg/git/testdata/testPatchBefore.diff
vendored
|
@ -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
|
|
57
pkg/git/testdata/testPatchBefore2.diff
vendored
57
pkg/git/testdata/testPatchBefore2.diff
vendored
|
@ -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 {
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"} {
|
||||||
|
|
89
pkg/gui/patch_options_panel.go
Normal file
89
pkg/gui/patch_options_panel.go
Normal 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()
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue