mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-21 22:43:27 +02:00
Support building and moving patches
WIP
This commit is contained in:
parent
a3c84296bf
commit
d5e443e8e3
@ -1,10 +1,9 @@
|
||||
package git
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -26,28 +25,28 @@ import (
|
||||
// BranchListBuilder returns a list of Branch objects for the current repo
|
||||
type BranchListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
GitCommand *GitCommand
|
||||
}
|
||||
|
||||
// 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{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch {
|
||||
func (b *BranchListBuilder) obtainCurrentBranch() *Branch {
|
||||
branchName, err := b.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
return &commands.Branch{Name: strings.TrimSpace(branchName)}
|
||||
return &Branch{Name: strings.TrimSpace(branchName)}
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
|
||||
branches := make([]*commands.Branch, 0)
|
||||
func (b *BranchListBuilder) obtainReflogBranches() []*Branch {
|
||||
branches := make([]*Branch, 0)
|
||||
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
|
||||
if err != nil {
|
||||
return branches
|
||||
@ -57,14 +56,14 @@ func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
|
||||
for _, line := range branchLines {
|
||||
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
|
||||
timeUnit = abbreviatedTimeUnit(timeUnit)
|
||||
branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
||||
branch := &Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
return uniqueByName(branches)
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
|
||||
branches := make([]*commands.Branch, 0)
|
||||
func (b *BranchListBuilder) obtainSafeBranches() []*Branch {
|
||||
branches := make([]*Branch, 0)
|
||||
|
||||
bIter, err := b.GitCommand.Repo.Branches()
|
||||
if err != nil {
|
||||
@ -72,14 +71,14 @@ func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
|
||||
}
|
||||
bIter.ForEach(func(b *plumbing.Reference) error {
|
||||
name := b.Name().Short()
|
||||
branches = append(branches, &commands.Branch{Name: name})
|
||||
branches = append(branches, &Branch{Name: name})
|
||||
return nil
|
||||
})
|
||||
|
||||
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 {
|
||||
if included == branchIncluded(newBranch.Name, existingBranches) {
|
||||
finalBranches = append(finalBranches, newBranch)
|
||||
@ -88,7 +87,7 @@ func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existi
|
||||
return finalBranches
|
||||
}
|
||||
|
||||
func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands.Branch) string {
|
||||
func sanitisedReflogName(reflogBranch *Branch, safeBranches []*Branch) string {
|
||||
for _, safeBranch := range safeBranches {
|
||||
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
|
||||
return safeBranch.Name
|
||||
@ -98,8 +97,8 @@ func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands
|
||||
}
|
||||
|
||||
// Build the list of branches for the current repo
|
||||
func (b *BranchListBuilder) Build() []*commands.Branch {
|
||||
branches := make([]*commands.Branch, 0)
|
||||
func (b *BranchListBuilder) Build() []*Branch {
|
||||
branches := make([]*Branch, 0)
|
||||
head := b.obtainCurrentBranch()
|
||||
safeBranches := b.obtainSafeBranches()
|
||||
|
||||
@ -112,7 +111,7 @@ func (b *BranchListBuilder) Build() []*commands.Branch {
|
||||
branches = b.appendNewBranches(branches, safeBranches, branches, false)
|
||||
|
||||
if len(branches) == 0 || branches[0].Name != head.Name {
|
||||
branches = append([]*commands.Branch{head}, branches...)
|
||||
branches = append([]*Branch{head}, branches...)
|
||||
}
|
||||
|
||||
branches[0].Recency = " *"
|
||||
@ -120,7 +119,7 @@ func (b *BranchListBuilder) Build() []*commands.Branch {
|
||||
return branches
|
||||
}
|
||||
|
||||
func branchIncluded(branchName string, branches []*commands.Branch) bool {
|
||||
func branchIncluded(branchName string, branches []*Branch) bool {
|
||||
for _, existingBranch := range branches {
|
||||
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
|
||||
return true
|
||||
@ -129,8 +128,8 @@ func branchIncluded(branchName string, branches []*commands.Branch) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func uniqueByName(branches []*commands.Branch) []*commands.Branch {
|
||||
finalBranches := make([]*commands.Branch, 0)
|
||||
func uniqueByName(branches []*Branch) []*Branch {
|
||||
finalBranches := make([]*Branch, 0)
|
||||
for _, branch := range branches {
|
||||
if branchIncluded(branch.Name, finalBranches) {
|
||||
continue
|
@ -1,13 +1,42 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
)
|
||||
|
||||
// CommitFile : A git commit file
|
||||
type CommitFile struct {
|
||||
Sha string
|
||||
Name 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.
|
||||
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 (
|
||||
"fmt"
|
||||
@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -27,15 +26,15 @@ import (
|
||||
// CommitListBuilder returns a list of Branch objects for the current repo
|
||||
type CommitListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
OSCommand *commands.OSCommand
|
||||
GitCommand *GitCommand
|
||||
OSCommand *OSCommand
|
||||
Tr *i18n.Localizer
|
||||
CherryPickedCommits []*commands.Commit
|
||||
DiffEntries []*commands.Commit
|
||||
CherryPickedCommits []*Commit
|
||||
DiffEntries []*Commit
|
||||
}
|
||||
|
||||
// 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{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
@ -47,9 +46,9 @@ func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, os
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
|
||||
commits := []*commands.Commit{}
|
||||
var rebasingCommits []*commands.Commit
|
||||
func (c *CommitListBuilder) GetCommits() ([]*Commit, error) {
|
||||
commits := []*Commit{}
|
||||
var rebasingCommits []*Commit
|
||||
rebaseMode, err := c.GitCommand.RebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -74,7 +73,7 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
|
||||
sha := splitLine[0]
|
||||
_, unpushed := unpushedCommits[sha]
|
||||
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
|
||||
commits = append(commits, &commands.Commit{
|
||||
commits = append(commits, &Commit{
|
||||
Sha: sha,
|
||||
Name: strings.Join(splitLine[1:], " "),
|
||||
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
|
||||
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.Commit, error) {
|
||||
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*Commit, error) {
|
||||
switch rebaseMode {
|
||||
case "normal":
|
||||
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
|
||||
bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-apply/rewritten", c.GitCommand.DotGitDir))
|
||||
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
|
||||
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 {
|
||||
if rewrittenCount > 0 {
|
||||
rewrittenCount--
|
||||
@ -152,7 +151,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commits = append([]*commands.Commit{commit}, commits...)
|
||||
commits = append([]*Commit{commit}, commits...)
|
||||
return 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
|
||||
// and extracts out the sha and names of commits that we still have to go
|
||||
// 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))
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
commits := []*commands.Commit{}
|
||||
commits := []*Commit{}
|
||||
lines := strings.Split(string(bytesContent), "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" || line == "noop" {
|
||||
return commits, nil
|
||||
}
|
||||
splitLine := strings.Split(line, " ")
|
||||
commits = append([]*commands.Commit{{
|
||||
commits = append([]*Commit{{
|
||||
Sha: splitLine[1][0:7],
|
||||
Name: strings.Join(splitLine[2:], " "),
|
||||
Status: "rebasing",
|
||||
@ -205,18 +204,18 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit,
|
||||
// From: Lazygit Tester <test@example.com>
|
||||
// Date: Wed, 5 Dec 2018 21:03:23 +1100
|
||||
// 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")
|
||||
sha := strings.Split(lines[0], " ")[1][0:7]
|
||||
name := strings.TrimPrefix(lines[3], "Subject: ")
|
||||
return &commands.Commit{
|
||||
return &Commit{
|
||||
Sha: sha,
|
||||
Name: name,
|
||||
Status: "rebasing",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) ([]*commands.Commit, error) {
|
||||
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
|
||||
ancestor, err := c.getMergeBase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -239,7 +238,7 @@ func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit)
|
||||
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 _, cherryPickedCommit := range c.CherryPickedCommits {
|
||||
if commit.Sha == cherryPickedCommit.Sha {
|
@ -1,24 +1,23 @@
|
||||
package git
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
|
||||
func NewDummyCommitListBuilder() *CommitListBuilder {
|
||||
osCommand := commands.NewDummyOSCommand()
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
return &CommitListBuilder{
|
||||
Log: commands.NewDummyLog(),
|
||||
GitCommand: commands.NewDummyGitCommandWithOSCommand(osCommand),
|
||||
Log: NewDummyLog(),
|
||||
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewLocalizer(commands.NewDummyLog()),
|
||||
CherryPickedCommits: []*commands.Commit{},
|
||||
Tr: i18n.NewLocalizer(NewDummyLog()),
|
||||
CherryPickedCommits: []*Commit{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,7 +198,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*commands.Commit, error)
|
||||
test func([]*Commit, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@ -225,7 +224,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*commands.Commit, err error) {
|
||||
func(commits []*Commit, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, commits, 0)
|
||||
},
|
||||
@ -252,10 +251,10 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*commands.Commit, err error) {
|
||||
func(commits []*Commit, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, commits, 2)
|
||||
assert.EqualValues(t, []*commands.Commit{
|
||||
assert.EqualValues(t, []*Commit{
|
||||
{
|
||||
Sha: "8a2bb0e",
|
||||
Name: "commit 1",
|
||||
@ -298,7 +297,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*commands.Commit, err error) {
|
||||
func(commits []*Commit, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, commits, 0)
|
||||
},
|
@ -63,16 +63,17 @@ func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repositor
|
||||
|
||||
// GitCommand is our main git interface
|
||||
type GitCommand struct {
|
||||
Log *logrus.Entry
|
||||
OSCommand *OSCommand
|
||||
Worktree *gogit.Worktree
|
||||
Repo *gogit.Repository
|
||||
Tr *i18n.Localizer
|
||||
Config config.AppConfigurer
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
getLocalGitConfig func(string) (string, error)
|
||||
removeFile func(string) error
|
||||
DotGitDir string
|
||||
Log *logrus.Entry
|
||||
OSCommand *OSCommand
|
||||
Worktree *gogit.Worktree
|
||||
Repo *gogit.Repository
|
||||
Tr *i18n.Localizer
|
||||
Config config.AppConfigurer
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
getLocalGitConfig func(string) (string, error)
|
||||
removeFile func(string) error
|
||||
DotGitDir string
|
||||
onSuccessfulContinue func() error
|
||||
}
|
||||
|
||||
// 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
|
||||
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
|
||||
command := "git commit --amend --no-edit"
|
||||
command := "git commit --amend --no-edit --allow-empty"
|
||||
if c.usingGpg() {
|
||||
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
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
@ -605,11 +606,11 @@ func (c *GitCommand) Diff(file *File, plain bool, cached bool) string {
|
||||
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)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = c.OSCommand.Remove(filename) }()
|
||||
@ -624,7 +625,7 @@ func (c *GitCommand) ApplyPatch(patch string, reverse bool, cached bool) (string
|
||||
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 {
|
||||
@ -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"
|
||||
// By default we skip the editor in the case where a commit will be made
|
||||
func (c *GitCommand) GenericMerge(commandType string, command string) error {
|
||||
return c.RunSkipEditorCommand(
|
||||
err := c.RunSkipEditorCommand(
|
||||
fmt.Sprintf(
|
||||
"git %s --%s",
|
||||
commandType,
|
||||
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) {
|
||||
@ -852,8 +869,8 @@ func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
|
||||
}
|
||||
|
||||
// GetCommitFiles get the specified commit files
|
||||
func (c *GitCommand) GetCommitFiles(commitSha string) ([]*CommitFile, error) {
|
||||
cmd := fmt.Sprintf("git show --pretty= --name-only %s", commitSha)
|
||||
func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager) ([]*CommitFile, error) {
|
||||
cmd := fmt.Sprintf("git show --pretty= --name-only --no-renames %s", commitSha)
|
||||
files, err := c.OSCommand.RunCommandWithOutput(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -862,10 +879,16 @@ func (c *GitCommand) GetCommitFiles(commitSha string) ([]*CommitFile, error) {
|
||||
commitFiles := make([]*CommitFile, 0)
|
||||
|
||||
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{
|
||||
Sha: commitSha,
|
||||
Name: 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
|
||||
func (c *GitCommand) ShowCommitFile(commitSha, fileName string) (string, error) {
|
||||
cmd := fmt.Sprintf("git show --color %s -- %s", commitSha, fileName)
|
||||
func (c *GitCommand) ShowCommitFile(commitSha, fileName string, plain bool) (string, error) {
|
||||
colorArg := "--color"
|
||||
if plain {
|
||||
colorArg = ""
|
||||
}
|
||||
cmd := fmt.Sprintf("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName)
|
||||
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
|
||||
func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, fileName string) 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 {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -924,7 +930,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, f
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
cmd, err = c.AmendHead()
|
||||
cmd, err := c.AmendHead()
|
||||
if cmd != nil {
|
||||
return errors.New("received unexpected pointer to cmd")
|
||||
}
|
||||
@ -1016,3 +1022,34 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
|
||||
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 {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, error)
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@ -1702,9 +1702,8 @@ func TestGitCommandApplyPatch(t *testing.T) {
|
||||
|
||||
return exec.Command("echo", "done")
|
||||
},
|
||||
func(output string, err error) {
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "done\n", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -1724,7 +1723,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
|
||||
|
||||
return exec.Command("test")
|
||||
},
|
||||
func(output string, err error) {
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
@ -1734,7 +1733,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
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 {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
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 {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
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 (
|
||||
"fmt"
|
||||
@ -10,7 +10,8 @@ import (
|
||||
"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 {
|
||||
header string
|
||||
@ -116,7 +117,7 @@ func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, rev
|
||||
}
|
||||
|
||||
// get oldstart, newstart, and heading from header
|
||||
match := headerRegexp.FindStringSubmatch(hunk.header)
|
||||
match := hunkHeaderRegexp.FindStringSubmatch(hunk.header)
|
||||
|
||||
var oldStart int
|
||||
if reverse {
|
||||
@ -152,9 +153,17 @@ func mustConvertToInt(s string) int {
|
||||
return i
|
||||
}
|
||||
|
||||
func GetHeaderFromDiff(diff string) string {
|
||||
match := patchHeaderRegexp.FindStringSubmatch(diff)
|
||||
if len(match) <= 1 {
|
||||
return ""
|
||||
}
|
||||
return match[1]
|
||||
}
|
||||
|
||||
func GetHunksFromDiff(diff string) []*PatchHunk {
|
||||
headers := headerRegexp.FindAllString(diff, -1)
|
||||
bodies := headerRegexp.Split(diff, -1)[1:] // discarding top bit
|
||||
headers := hunkHeaderRegexp.FindAllString(diff, -1)
|
||||
bodies := hunkHeaderRegexp.Split(diff, -1)[1:] // discarding top bit
|
||||
|
||||
headerFirstLineIndices := []int{}
|
||||
for lineIdx, line := range strings.Split(diff, "\n") {
|
||||
@ -175,6 +184,7 @@ type PatchModifier struct {
|
||||
Log *logrus.Entry
|
||||
filename string
|
||||
hunks []*PatchHunk
|
||||
header string
|
||||
}
|
||||
|
||||
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,
|
||||
filename: filename,
|
||||
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
|
||||
hunksInRange := []*PatchHunk{}
|
||||
outer:
|
||||
@ -212,21 +223,38 @@ outer:
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
selectedLines := []int{}
|
||||
for i := firstLineIdx; i <= lastLineIdx; 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 {
|
||||
p := NewPatchModifier(log, filename, diffText)
|
||||
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse)
|
||||
func (d *PatchModifier) OriginalPatchLength() int {
|
||||
if len(d.hunks) == 0 {
|
||||
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 (
|
||||
"fmt"
|
||||
@ -502,7 +502,7 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
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) {
|
||||
fmt.Println(result)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package git
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
@ -12,6 +12,8 @@ import (
|
||||
|
||||
const (
|
||||
PATCH_HEADER = iota
|
||||
COMMIT_SHA
|
||||
COMMIT_DESCRIPTION
|
||||
HUNK_HEADER
|
||||
ADDITION
|
||||
DELETION
|
||||
@ -81,7 +83,10 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
|
||||
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
|
||||
if len(content) == 0 {
|
||||
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 {
|
||||
re := regexp.MustCompile("(@@.*?@@)(.*)")
|
||||
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
|
||||
@ -102,21 +107,34 @@ func (l *PatchLine) render(selected bool) string {
|
||||
colorAttr = color.FgGreen
|
||||
case DELETION:
|
||||
colorAttr = color.FgRed
|
||||
case COMMIT_SHA:
|
||||
colorAttr = color.FgYellow
|
||||
default:
|
||||
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
|
||||
attributes := []color.Attribute{colorAttr}
|
||||
if selected {
|
||||
cl = color.New(colorAttr, color.BgBlue)
|
||||
} else {
|
||||
cl = color.New(colorAttr)
|
||||
attributes = append(attributes, color.BgBlue)
|
||||
}
|
||||
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) {
|
||||
@ -124,16 +142,26 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
||||
hunkStarts := []int{}
|
||||
stageableLines := []int{}
|
||||
pastFirstHunkHeader := false
|
||||
pastCommitDescription := true
|
||||
patchLines := make([]*PatchLine, len(lines))
|
||||
var lineKind int
|
||||
var firstChar string
|
||||
for index, line := range lines {
|
||||
lineKind = PATCH_HEADER
|
||||
firstChar = " "
|
||||
if len(line) > 0 {
|
||||
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
|
||||
hunkStarts = append(hunkStarts, index)
|
||||
lineKind = HUNK_HEADER
|
||||
@ -150,6 +178,8 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
||||
case " ":
|
||||
lineKind = CONTEXT
|
||||
}
|
||||
} else {
|
||||
lineKind = PATCH_HEADER
|
||||
}
|
||||
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
|
||||
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))
|
||||
for index, patchLine := range p.PatchLines {
|
||||
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")
|
||||
}
|
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/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/git"
|
||||
)
|
||||
|
||||
// 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
|
||||
func (gui *Gui) refreshBranches(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 {
|
||||
return err
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"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 {
|
||||
return err
|
||||
}
|
||||
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name)
|
||||
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -79,16 +80,19 @@ func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshCommitFilesView() error {
|
||||
if err := gui.refreshPatchPanel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commit := gui.getSelectedCommit(gui.g)
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
files, err := gui.GitCommand.GetCommitFiles(commit.Sha)
|
||||
files, err := gui.GitCommand.GetCommitFiles(commit.Sha, gui.State.PatchManager)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
|
||||
gui.State.CommitFiles = files
|
||||
|
||||
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)
|
||||
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/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/git"
|
||||
"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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@ -65,6 +81,10 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
|
||||
}
|
||||
gui.State.Commits = commits
|
||||
|
||||
if err := gui.refreshPatchPanel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
|
||||
|
||||
isFocused := gui.g.CurrentView().Name() == "commits"
|
||||
|
@ -23,7 +23,6 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/git"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
@ -84,13 +83,13 @@ type Gui struct {
|
||||
// non-mutative, so that we don't accidentally end up
|
||||
// with mismatches of data. We might change this in the future
|
||||
type stagingPanelState struct {
|
||||
SelectedLineIdx int
|
||||
FirstLineIdx int
|
||||
LastLineIdx int
|
||||
Diff string
|
||||
PatchParser *git.PatchParser
|
||||
SelectMode int // one of LINE, HUNK, or RANGE
|
||||
IndexFocused bool // this is for if we show the left or right panel
|
||||
SelectedLineIdx int
|
||||
FirstLineIdx int
|
||||
LastLineIdx int
|
||||
Diff string
|
||||
PatchParser *commands.PatchParser
|
||||
SelectMode int // one of LINE, HUNK, or RANGE
|
||||
SecondaryFocused bool // this is for if we show the left or right panel
|
||||
}
|
||||
|
||||
type mergingPanelState struct {
|
||||
@ -152,8 +151,11 @@ type guiState struct {
|
||||
Contexts map[string]string
|
||||
CherryPickedCommits []*commands.Commit
|
||||
SplitMainPanel bool
|
||||
PatchManager *commands.PatchManager
|
||||
}
|
||||
|
||||
// for now the split view will always be on
|
||||
|
||||
// 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) {
|
||||
|
||||
@ -390,7 +392,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
|
||||
main := "main"
|
||||
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 {
|
||||
main = "secondary"
|
||||
secondary = "main"
|
||||
|
@ -142,6 +142,11 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Key: 'x',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCreateOptionsMenu,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyCtrlP,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCreatePatchOptionsMenu,
|
||||
}, {
|
||||
ViewName: "status",
|
||||
Key: 'e',
|
||||
@ -523,6 +528,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Handler: gui.handleOpenOldCommitFile,
|
||||
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"} {
|
||||
|
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
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"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 (
|
||||
LINE = iota
|
||||
RANGE
|
||||
@ -16,49 +15,65 @@ const (
|
||||
func (gui *Gui) refreshStagingPanel() error {
|
||||
state := gui.State.Panels.Staging
|
||||
|
||||
file, err := gui.getSelectedFile(gui.g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
}
|
||||
// file, err := gui.getSelectedFile(gui.g)
|
||||
// if err != nil {
|
||||
// if err != gui.Errors.ErrNoFiles {
|
||||
// return err
|
||||
// }
|
||||
// return gui.handleStagingEscape(gui.g, nil)
|
||||
// }
|
||||
|
||||
gui.State.SplitMainPanel = true
|
||||
|
||||
indexFocused := false
|
||||
secondaryFocused := false
|
||||
if state != nil {
|
||||
indexFocused = state.IndexFocused
|
||||
secondaryFocused = state.SecondaryFocused
|
||||
}
|
||||
|
||||
if !file.HasUnstagedChanges && !file.HasStagedChanges {
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
// if !file.HasUnstagedChanges && !file.HasStagedChanges {
|
||||
// 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) {
|
||||
indexFocused = !indexFocused
|
||||
diff, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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, indexFocused)
|
||||
secondaryColorDiff := gui.GitCommand.Diff(file, false, !indexFocused)
|
||||
return diff, secondaryColorDiff
|
||||
secondaryColorDiff := gui.State.PatchManager.RenderPatchForFile(commitFile.Name, false, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
indexFocused = !indexFocused
|
||||
diff, secondaryColorDiff = getDiffs()
|
||||
}
|
||||
|
||||
patchParser, err := git.NewPatchParser(gui.Log, diff)
|
||||
patchParser, err := commands.NewPatchParser(gui.Log, diff)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@ -92,13 +107,13 @@ func (gui *Gui) refreshStagingPanel() error {
|
||||
}
|
||||
|
||||
gui.State.Panels.Staging = &stagingPanelState{
|
||||
PatchParser: patchParser,
|
||||
SelectedLineIdx: selectedLineIdx,
|
||||
SelectMode: selectMode,
|
||||
FirstLineIdx: firstLineIdx,
|
||||
LastLineIdx: lastLineIdx,
|
||||
Diff: diff,
|
||||
IndexFocused: indexFocused,
|
||||
PatchParser: patchParser,
|
||||
SelectedLineIdx: selectedLineIdx,
|
||||
SelectMode: selectMode,
|
||||
FirstLineIdx: firstLineIdx,
|
||||
LastLineIdx: lastLineIdx,
|
||||
Diff: diff,
|
||||
SecondaryFocused: secondaryFocused,
|
||||
}
|
||||
|
||||
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 {
|
||||
state := gui.State.Panels.Staging
|
||||
|
||||
state.IndexFocused = !state.IndexFocused
|
||||
state.SecondaryFocused = !state.SecondaryFocused
|
||||
return gui.refreshStagingPanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
|
||||
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 {
|
||||
@ -203,7 +218,9 @@ func (gui *Gui) handleCycleLine(change int) error {
|
||||
func (gui *Gui) refreshView() error {
|
||||
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.Highlight = true
|
||||
@ -258,17 +275,57 @@ func (gui *Gui) focusSelection(includeCurrentHunk bool) 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 {
|
||||
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 {
|
||||
state := gui.State.Panels.Staging
|
||||
|
||||
if !reverse && state.IndexFocused {
|
||||
if !reverse && state.SecondaryFocused {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantStageStaged"))
|
||||
}
|
||||
|
||||
@ -277,7 +334,7 @@ func (gui *Gui) applySelection(reverse bool) error {
|
||||
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 == "" {
|
||||
return nil
|
||||
@ -285,7 +342,7 @@ func (gui *Gui) applySelection(reverse bool) error {
|
||||
|
||||
// apply the patch then refresh this panel
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
@ -368,13 +368,13 @@ func (gui *Gui) changeSelectedLine(line *int, total int, up bool) {
|
||||
return
|
||||
}
|
||||
|
||||
*line -= 1
|
||||
*line--
|
||||
} else {
|
||||
if *line == -1 || *line == total-1 {
|
||||
return
|
||||
}
|
||||
|
||||
*line += 1
|
||||
*line++
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -794,6 +794,24 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "jump",
|
||||
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, "", " ")
|
||||
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…
x
Reference in New Issue
Block a user