mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
[SQUASHED] show-commit-msg-diff-for-amend-commits
This commit is contained in:
parent
68d6f69d37
commit
822b3be988
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var ErrInvalidCommitIndex = errors.New("invalid commit index")
|
||||
|
|
@ -155,13 +156,39 @@ func (self *CommitCommands) signoffFlag() string {
|
|||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitMessage(commitHash string) (string, error) {
|
||||
messages, err := self.GetCommitMessages([]string{commitHash})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return messages[0], nil
|
||||
}
|
||||
|
||||
// GetCommitMessages returns the messages of the given commits, in the order in
|
||||
// which the hashes were passed in.
|
||||
func (self *CommitCommands) GetCommitMessages(commitHashes []string) ([]string, error) {
|
||||
cmdArgs := NewGitCmd("log").
|
||||
Arg("--format=%B", "--max-count=1", commitHash).
|
||||
Arg("--no-walk=unsorted", "--format=%B%x00").
|
||||
Arg(commitHashes...).
|
||||
Config("log.showsignature=false").
|
||||
ToArgv()
|
||||
|
||||
message, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
return strings.ReplaceAll(strings.TrimSpace(message), "\r\n", "\n"), err
|
||||
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The messages are NUL-terminated, so the split gives us one more element
|
||||
// than we asked for (holding the newline that git prints after the last
|
||||
// message).
|
||||
messages := strings.Split(output, "\x00")
|
||||
if len(messages) <= len(commitHashes) {
|
||||
return nil, errors.New("unexpected output from git log")
|
||||
}
|
||||
|
||||
return lo.Map(messages[:len(commitHashes)], func(message string, _ int) string {
|
||||
return strings.ReplaceAll(strings.TrimSpace(message), "\r\n", "\n")
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitSubject(commitHash string) (string, error) {
|
||||
|
|
|
|||
|
|
@ -378,7 +378,7 @@ func TestGetCommitMsg(t *testing.T) {
|
|||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{
|
||||
runner: oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"-c", "log.showsignature=false", "log", "--format=%B", "--max-count=1", "deadbeef"}, s.input, nil),
|
||||
runner: oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"-c", "log.showsignature=false", "log", "--no-walk=unsorted", "--format=%B%x00", "deadbeef"}, s.input+"\x00\n", nil),
|
||||
})
|
||||
|
||||
output, err := instance.GetCommitMessage("deadbeef")
|
||||
|
|
@ -390,6 +390,21 @@ func TestGetCommitMsg(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetCommitMessages(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{
|
||||
runner: oscommands.NewFakeRunner(t).ExpectGitArgs(
|
||||
[]string{"-c", "log.showsignature=false", "log", "--no-walk=unsorted", "--format=%B%x00", "deadbeef", "1234567"},
|
||||
"first subject\n\nfirst body\n\x00\nsecond subject\n\x00\n",
|
||||
nil,
|
||||
),
|
||||
})
|
||||
|
||||
output, err := instance.GetCommitMessages([]string{"deadbeef", "1234567"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"first subject\n\nfirst body", "second subject"}, output)
|
||||
}
|
||||
|
||||
func TestGetCommitMessageFromHistory(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
|
|
@ -406,7 +421,7 @@ func TestGetCommitMessageFromHistory(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"Default case to retrieve a commit in history",
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"log", "-1", "--skip=2", "--pretty=%H"}, "hash3 \n", nil).ExpectGitArgs([]string{"-c", "log.showsignature=false", "log", "--format=%B", "--max-count=1", "hash3"}, `use generics to DRY up context code`, nil),
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"log", "-1", "--skip=2", "--pretty=%H"}, "hash3 \n", nil).ExpectGitArgs([]string{"-c", "log.showsignature=false", "log", "--no-walk=unsorted", "--format=%B%x00", "hash3"}, "use generics to DRY up context code\x00\n", nil),
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "use generics to DRY up context code", output)
|
||||
|
|
|
|||
|
|
@ -121,6 +121,11 @@ func buildSubmoduleCommands(deps commonDeps) *SubmoduleCommands {
|
|||
return NewSubmoduleCommands(gitCommon)
|
||||
}
|
||||
|
||||
func buildDiffCommands(deps commonDeps) *DiffCommands {
|
||||
gitCommon := buildGitCommon(deps)
|
||||
return NewDiffCommands(gitCommon)
|
||||
}
|
||||
|
||||
func buildCommitCommands(deps commonDeps) *CommitCommands {
|
||||
gitCommon := buildGitCommon(deps)
|
||||
return NewCommitCommands(gitCommon)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ package git_commands
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
|
|
@ -189,6 +191,92 @@ func (self *DiffCommands) GetDiff(staged bool, additionalArgs ...string) (string
|
|||
).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
// NamedText is a text to be diffed by RenderedTextDiff, along with the name to
|
||||
// show for it in the diff. The name doubles as the name of the temporary file
|
||||
// holding the text, so it has to be usable as a file name.
|
||||
type NamedText struct {
|
||||
Name string
|
||||
Content string
|
||||
}
|
||||
|
||||
// RenderedTextDiff returns a diff of the two given texts, produced by the
|
||||
// configured diff renderer in the same way as every other diff we show. width
|
||||
// and height are the size of the view the diff is going to be shown in; a diff
|
||||
// renderer lays its output out for them.
|
||||
//
|
||||
// git can only diff files, so the texts are written to temporary files named
|
||||
// after them. Under a diff renderer those names are what the diff calls the two
|
||||
// sides; under git's own diff they go away with the rest of the header.
|
||||
func (self *DiffCommands) RenderedTextDiff(before NamedText, after NamedText, width int, height int) (string, error) {
|
||||
dir, err := os.MkdirTemp(self.os.GetTempDir(), "textdiff-")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
for _, text := range []NamedText{before, after} {
|
||||
content := text.Content
|
||||
// End the file with a newline, or git says that it doesn't, which tells
|
||||
// a reader of the diff nothing about the two texts.
|
||||
if content != "" && !strings.HasSuffix(content, "\n") {
|
||||
content += "\n"
|
||||
}
|
||||
if err := self.os.CreateFileWithContent(filepath.Join(dir, text.Name), content); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
cmdObj := self.cmd.New(
|
||||
NewGitCmd("diff").
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), DiffModeRendered).
|
||||
Arg("--no-index", "--no-prefix").
|
||||
Arg(fmt.Sprintf("--color=%s", self.diffRendererConfigManager.GetColorArg())).
|
||||
Arg("--", before.Name, after.Name).
|
||||
Dir(dir).
|
||||
ToArgv(),
|
||||
).DontLog()
|
||||
|
||||
// --no-index implies --exit-code, so git exits with a non-zero status
|
||||
// whenever the two texts differ; that is only an error if it left us with
|
||||
// nothing to show.
|
||||
if self.diffRendererConfigManager.GetDiffRendererType() == config.DiffRendererType_RawGit {
|
||||
// git renders the diff itself here, so no terminal is needed to get it.
|
||||
output, err := cmdObj.RunWithOutput()
|
||||
if output == "" && err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return stripDiffHeaders(output), nil
|
||||
}
|
||||
|
||||
oscommands.SetDumbTerminalEnv(cmdObj.GetCmd())
|
||||
cmdObj.AddEnvVars(
|
||||
// For diff renderer scripts that can't query the terminal width directly.
|
||||
fmt.Sprintf("LAZYGIT_COLUMNS=%d", width),
|
||||
"GIT_PAGER="+self.diffRendererConfigManager.GetStdinFilterCommand(width),
|
||||
)
|
||||
|
||||
output, err := oscommands.RunInPtyWithOutput(cmdObj.GetCmd(), uint16(width), uint16(height))
|
||||
if output == "" && err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// Strips the file header and the first hunk header from a diff. For a diff of
|
||||
// two temporary files these say nothing that a reader could make sense of.
|
||||
func stripDiffHeaders(diff string) string {
|
||||
lines := strings.SplitAfter(diff, "\n")
|
||||
for i, line := range lines {
|
||||
if strings.HasPrefix(utils.Decolorise(line), "@@ ") {
|
||||
return strings.Join(lines[i+1:], "")
|
||||
}
|
||||
}
|
||||
|
||||
return diff
|
||||
}
|
||||
|
||||
type DiffToolCmdOptions struct {
|
||||
// The path to show a diff for. Pass "." for the entire repo.
|
||||
Filepath string
|
||||
|
|
|
|||
126
pkg/commands/git_commands/diff_test.go
Normal file
126
pkg/commands/git_commands/diff_test.go
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
package git_commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRenderedTextDiff(t *testing.T) {
|
||||
var args []string
|
||||
var dir string
|
||||
var beforeContent, afterContent string
|
||||
|
||||
// The two texts are diffed as files in a temporary directory that is removed
|
||||
// again afterwards, so read them here, while they are still there.
|
||||
runner := oscommands.NewFakeRunner(t).ExpectFunc("text diff",
|
||||
func(cmdObj *oscommands.CmdObj) bool {
|
||||
args = cmdObj.GetCmd().Args
|
||||
dir = args[2] // git -C <dir>
|
||||
before, _ := os.ReadFile(filepath.Join(dir, "old message"))
|
||||
after, _ := os.ReadFile(filepath.Join(dir, "new message"))
|
||||
beforeContent, afterContent = string(before), string(after)
|
||||
return true
|
||||
},
|
||||
"\x1b[1mdiff --git old message new message\x1b[m\n"+
|
||||
"\x1b[1mindex 9ebe4a6..ec3885a 100644\x1b[m\n"+
|
||||
"\x1b[1m--- old message\x1b[m\n"+
|
||||
"\x1b[1m+++ new message\x1b[m\n"+
|
||||
"\x1b[36m@@ -1,3 +1,3 @@\x1b[m\n"+
|
||||
"-Fix the widget\n"+
|
||||
"+Fix the widget on startup\n",
|
||||
// --no-index implies --exit-code
|
||||
errors.New("exit status 1"))
|
||||
|
||||
instance := buildDiffCommands(commonDeps{runner: runner})
|
||||
|
||||
output, err := instance.RenderedTextDiff(
|
||||
NamedText{Name: "old message", Content: "Fix the widget\n"},
|
||||
NamedText{Name: "new message", Content: "Fix the widget on startup\n"},
|
||||
80, 24)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "-Fix the widget\n+Fix the widget on startup\n", output)
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"git", "-C", dir, "diff", "--no-ext-diff", "--unified=3", "--find-renames=50%",
|
||||
"--no-index", "--no-prefix", "--color=always", "--", "old message", "new message",
|
||||
}, args)
|
||||
assert.Equal(t, "Fix the widget\n", beforeContent)
|
||||
assert.Equal(t, "Fix the widget on startup\n", afterContent)
|
||||
|
||||
assert.NoDirExists(t, dir)
|
||||
}
|
||||
|
||||
func TestRenderedTextDiffPassesOnTheRenderersGitArgs(t *testing.T) {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{Type: "rawGit", Args: []string{"--color-words"}},
|
||||
}
|
||||
|
||||
var args []string
|
||||
runner := oscommands.NewFakeRunner(t).ExpectFunc("text diff",
|
||||
func(cmdObj *oscommands.CmdObj) bool {
|
||||
args = cmdObj.GetCmd().Args
|
||||
return true
|
||||
}, "", nil)
|
||||
|
||||
instance := buildDiffCommands(commonDeps{runner: runner, userConfig: userConfig})
|
||||
|
||||
_, err := instance.RenderedTextDiff(
|
||||
NamedText{Name: "old message", Content: "one"},
|
||||
NamedText{Name: "new message", Content: "two"},
|
||||
80, 24)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, args, "--color-words")
|
||||
}
|
||||
|
||||
func TestStripDiffHeaders(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
diff string
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "colored diff",
|
||||
diff: "\x1b[1mdiff --git old message new message\x1b[m\n" +
|
||||
"\x1b[1mindex 9ebe4a6..ec3885a 100644\x1b[m\n" +
|
||||
"\x1b[1m--- old message\x1b[m\n" +
|
||||
"\x1b[1m+++ new message\x1b[m\n" +
|
||||
"\x1b[36m@@ -1,3 +1,3 @@\x1b[m\n" +
|
||||
"one \x1b[32mtwo\x1b[m\n",
|
||||
expectedOutput: "one \x1b[32mtwo\x1b[m\n",
|
||||
},
|
||||
{
|
||||
name: "uncolored diff with several hunks",
|
||||
diff: "diff --git old message new message\n" +
|
||||
"index 9ebe4a6..ec3885a 100644\n" +
|
||||
"--- old message\n" +
|
||||
"+++ new message\n" +
|
||||
"@@ -1,3 +1,3 @@\n" +
|
||||
"one two\n" +
|
||||
"@@ -20,3 +20,3 @@\n" +
|
||||
"three four\n",
|
||||
expectedOutput: "one two\n" +
|
||||
"@@ -20,3 +20,3 @@\n" +
|
||||
"three four\n",
|
||||
},
|
||||
{
|
||||
name: "empty diff",
|
||||
diff: "",
|
||||
expectedOutput: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.Equal(t, s.expectedOutput, stripDiffHeaders(s.diff))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,10 @@ package oscommands
|
|||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// Pty is the master side of a pseudo-terminal running a subprocess. The
|
||||
|
|
@ -32,3 +36,51 @@ type StartedPty struct {
|
|||
// Implemented per-platform in pty_unix.go / pty_windows.go.
|
||||
//
|
||||
// func StartPty(cmd *exec.Cmd, cols, rows uint16) (StartedPty, error)
|
||||
|
||||
// RunInPtyWithOutput runs cmd in a pseudo-terminal of the given size and returns
|
||||
// everything it wrote. Commands that render for a terminal need one to render at
|
||||
// all: git only pipes its output through a pager when it thinks it is talking to
|
||||
// a terminal, and the pager itself commonly decides whether to use color the same
|
||||
// way.
|
||||
func RunInPtyWithOutput(cmd *exec.Cmd, cols uint16, rows uint16) (string, error) {
|
||||
startedPty, err := StartPty(cmd, cols, rows)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer startedPty.Pty.Close()
|
||||
|
||||
// Reading from the master side of a pty fails as soon as the child has closed
|
||||
// the other side, so an error here only tells us that the command is done.
|
||||
// What it wrote before that is what we came for.
|
||||
output, _ := io.ReadAll(startedPty.Pty)
|
||||
|
||||
// A terminal ends each line with a carriage return, which is of no use to a
|
||||
// caller that treats the output as text.
|
||||
return strings.ReplaceAll(string(output), "\r\n", "\n"), startedPty.Wait()
|
||||
}
|
||||
|
||||
// SetDumbTerminalEnv tells a command that runs in a pty that we're in a very
|
||||
// simple terminal that they should not expect to have much capabilities.
|
||||
// Moving the cursor, clearing the screen, or querying for colors are among such
|
||||
// "advanced" capabilities.
|
||||
// Context: https://github.com/jesseduffield/lazygit/issues/3419
|
||||
func SetDumbTerminalEnv(cmd *exec.Cmd) {
|
||||
cmd.Env = append(removeExistingTermEnvVars(cmd.Env), "TERM=dumb")
|
||||
}
|
||||
|
||||
func removeExistingTermEnvVars(env []string) []string {
|
||||
return lo.Filter(env, func(envVar string, _ int) bool {
|
||||
return !isTermEnvVar(envVar)
|
||||
})
|
||||
}
|
||||
|
||||
// Terminals set a variety of different environment variables
|
||||
// to identify themselves to processes. This list should catch the most common among them.
|
||||
func isTermEnvVar(envVar string) bool {
|
||||
return strings.HasPrefix(envVar, "TERM=") ||
|
||||
strings.HasPrefix(envVar, "TERM_PROGRAM=") ||
|
||||
strings.HasPrefix(envVar, "TERM_PROGRAM_VERSION=") ||
|
||||
strings.HasPrefix(envVar, "TERMINAL_EMULATOR=") ||
|
||||
strings.HasPrefix(envVar, "TERMINAL_NAME=") ||
|
||||
strings.HasPrefix(envVar, "TERMINAL_VERSION_")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -108,6 +109,18 @@ func (self *DiffRendererConfigManager) GetRawGitArgs() []string {
|
|||
return currentDiffRendererConfig.Args
|
||||
}
|
||||
|
||||
// Signature is what identifies the current diff renderer, so that something we
|
||||
// remembered about what it produced can be dropped once it no longer describes
|
||||
// the renderer we have. The width a command is asked for is no part of its
|
||||
// identity, so a fixed one is used.
|
||||
func (self *DiffRendererConfigManager) Signature() string {
|
||||
return fmt.Sprintf("%d\x00%s\x00%s\x00%s",
|
||||
self.diffRendererIndex,
|
||||
self.GetExternalDiffCommand(3),
|
||||
self.GetStdinFilterCommand(0),
|
||||
strings.Join(self.GetRawGitArgs(), "\x00"))
|
||||
}
|
||||
|
||||
func (self *DiffRendererConfigManager) CycleDiffRenderers() {
|
||||
self.diffRendererIndex = (self.diffRendererIndex + 1) % len(self.getUserConfig().Git.DiffRenderers)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
|
|
@ -10,6 +11,7 @@ import (
|
|||
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
|
|
@ -18,12 +20,19 @@ type DiffHelper struct {
|
|||
// diffLineHelper says how a diff for the main view is to be produced, which depends
|
||||
// on whether the focused main view could act on what a diff renderer would make of it.
|
||||
diffLineHelper *DiffLineHelper
|
||||
|
||||
// Diffs of the messages of "amend!" commits, keyed by everything that
|
||||
// shapes them: the two commits, and the diff renderer and width they were
|
||||
// rendered by. An empty diff means that the commit doesn't change the
|
||||
// message. Only accessed on the UI thread, while rendering the main view.
|
||||
commitMessageDiffs map[string]string
|
||||
}
|
||||
|
||||
func NewDiffHelper(c *HelperCommon, diffLineHelper *DiffLineHelper) *DiffHelper {
|
||||
return &DiffHelper{
|
||||
c: c,
|
||||
diffLineHelper: diffLineHelper,
|
||||
c: c,
|
||||
diffLineHelper: diffLineHelper,
|
||||
commitMessageDiffs: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +65,13 @@ func (self *DiffHelper) DiffArgs() []string {
|
|||
// and the refRange for a range selection. If the refRange is nil (meaning that
|
||||
// either there's no range, or it can't be diffed for some reason), then we want
|
||||
// to fall back to rendering the diff for the single commit.
|
||||
func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(commit *models.Commit, refRange *types.RefRange) types.UpdateTask {
|
||||
// In addition, we need to pass the list of all commits; this is needed for
|
||||
// showing the commit message diff for "amend!" commits.
|
||||
func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(
|
||||
commits []*models.Commit,
|
||||
commit *models.Commit,
|
||||
refRange *types.RefRange,
|
||||
) types.UpdateTask {
|
||||
mode := self.diffLineHelper.MainViewDiffMode()
|
||||
|
||||
if refRange != nil {
|
||||
|
|
@ -84,7 +99,81 @@ func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(commit *models.Comm
|
|||
}
|
||||
|
||||
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash(), self.FilterPathsForCommit(commit), mode)
|
||||
return types.NewMainViewDiffTask(cmdObj.GetCmd(), mode)
|
||||
return types.NewMainViewDiffTaskWithPrefix(cmdObj.GetCmd(), self.commitMessageDiffPrefix(commits, commit), mode)
|
||||
}
|
||||
|
||||
// For an "amend!" commit, returns a diff of the commit message it sets against
|
||||
// the message it replaces, to be shown above the commit's own diff. Returns an
|
||||
// empty string for any other commit, and for an "amend!" commit that only
|
||||
// changes the contents of the commit it applies to.
|
||||
func (self *DiffHelper) commitMessageDiffPrefix(commits []*models.Commit, commit *models.Commit) string {
|
||||
previousCommit, ok := findCommitWithPreviousMessage(commits, commit)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
width, height := self.c.Contexts().Normal.GetView().InnerSize()
|
||||
|
||||
diff := self.commitMessageDiff(commit, previousCommit, width, height)
|
||||
if diff == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
header := style.FgYellow.Sprintf("%s\n", utils.ResolvePlaceholderString(
|
||||
self.c.Tr.CommitMessageChanges,
|
||||
map[string]string{"hash": previousCommit.ShortHash()},
|
||||
))
|
||||
rule := strings.Repeat("─", width) + "\n"
|
||||
|
||||
return header + diff + rule
|
||||
}
|
||||
|
||||
// The names the two messages are diffed under. A diff renderer shows them as the
|
||||
// names of the files being diffed, so they are what tells the reader which side
|
||||
// is which. They are not translated because they end up as file names, and git
|
||||
// mangles paths outside of ASCII when it states them in a diff.
|
||||
const (
|
||||
oldMessageName = "old message"
|
||||
newMessageName = "new message"
|
||||
)
|
||||
|
||||
func (self *DiffHelper) commitMessageDiff(
|
||||
commit *models.Commit, previousCommit *models.Commit, width int, height int,
|
||||
) string {
|
||||
// The diff renderer lays the diff out, and lays it out for the width it is
|
||||
// shown at, so both belong in the key along with the two messages.
|
||||
key := fmt.Sprintf("%s\x00%s\x00%d\x00%s",
|
||||
commit.Hash(),
|
||||
previousCommit.Hash(),
|
||||
width,
|
||||
self.c.State().GetDiffRendererConfigManager().Signature())
|
||||
if diff, ok := self.commitMessageDiffs[key]; ok {
|
||||
return diff
|
||||
}
|
||||
|
||||
messages, err := self.c.Git().Commit.GetCommitMessages([]string{previousCommit.Hash(), commit.Hash()})
|
||||
if err != nil {
|
||||
self.c.Log.Error(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
before := messageAfterAmending(messages[0])
|
||||
after := messageAfterAmending(messages[1])
|
||||
|
||||
diff := ""
|
||||
if before != after {
|
||||
diff, err = self.c.Git().Diff.RenderedTextDiff(
|
||||
git_commands.NamedText{Name: oldMessageName, Content: before},
|
||||
git_commands.NamedText{Name: newMessageName, Content: after},
|
||||
width, height)
|
||||
if err != nil {
|
||||
self.c.Log.Error(err)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
self.commitMessageDiffs[key] = diff
|
||||
return diff
|
||||
}
|
||||
|
||||
// PlainDiffBetweenRefs returns the diff of the given files between two refs as git
|
||||
|
|
|
|||
|
|
@ -409,3 +409,62 @@ func IsFixupCommit(subject string) (string, bool) {
|
|||
|
||||
return subject, false
|
||||
}
|
||||
|
||||
// Check whether the given subject line is the subject of an "amend!" commit,
|
||||
// i.e. of a commit that replaces the message of the commit it applies to, and
|
||||
// return the subject of that commit if so. Note that a commit with a subject
|
||||
// like "fixup! amend! Bla" is not an "amend!" commit; only the outermost
|
||||
// prefix decides what happens to the message.
|
||||
func isAmendCommit(subject string) (string, bool) {
|
||||
if !strings.HasPrefix(subject, "amend! ") {
|
||||
return subject, false
|
||||
}
|
||||
|
||||
return IsFixupCommit(subject)
|
||||
}
|
||||
|
||||
// For an "amend!" commit, find the commit that holds the message it replaces.
|
||||
// This is the nearest "amend!" commit below it that applies to the same commit,
|
||||
// or, if there is none, the commit it applies to itself. Returns false if the
|
||||
// given commit isn't an "amend!" commit, or if the commit it applies to isn't
|
||||
// in the given list.
|
||||
func findCommitWithPreviousMessage(commits []*models.Commit, commit *models.Commit) (*models.Commit, bool) {
|
||||
baseSubject, isAmend := isAmendCommit(commit.Name)
|
||||
if !isAmend {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
_, index, ok := lo.FindIndexOf(commits, func(c *models.Commit) bool {
|
||||
return c.Hash() == commit.Hash()
|
||||
})
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for _, previousCommit := range commits[index+1:] {
|
||||
if previousCommit.Name == baseSubject {
|
||||
return previousCommit, true
|
||||
}
|
||||
if subject, isAmend := isAmendCommit(previousCommit.Name); isAmend && subject == baseSubject {
|
||||
return previousCommit, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Return the message that the given commit leaves on the commit it applies to:
|
||||
// for an "amend!" commit this is its message without the "amend! <subject>"
|
||||
// line at the top, and for any other commit it is simply its own message.
|
||||
func messageAfterAmending(message string) string {
|
||||
subject, body, found := strings.Cut(message, "\n")
|
||||
if !found {
|
||||
return message
|
||||
}
|
||||
|
||||
if _, isAmend := isAmendCommit(subject); !isAmend {
|
||||
return message
|
||||
}
|
||||
|
||||
return strings.TrimLeft(body, "\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,6 +205,187 @@ func TestFixupHelper_IsFixupCommit(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFixupHelper_findCommitWithPreviousMessage(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
|
||||
type commitDesc struct {
|
||||
Hash string
|
||||
Name string
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
commits []commitDesc
|
||||
index int
|
||||
expectedHash string
|
||||
}{
|
||||
{
|
||||
name: "not an amend commit",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "Some feature"},
|
||||
},
|
||||
index: 0,
|
||||
expectedHash: "",
|
||||
},
|
||||
{
|
||||
name: "fixup commits don't change the message",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "fixup! Some feature"},
|
||||
{"def456", "Some feature"},
|
||||
},
|
||||
index: 0,
|
||||
expectedHash: "",
|
||||
},
|
||||
{
|
||||
name: "a fixup of an amend commit doesn't change the message either",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "fixup! amend! Some feature"},
|
||||
{"def456", "amend! Some feature"},
|
||||
{"ghi789", "Some feature"},
|
||||
},
|
||||
index: 0,
|
||||
expectedHash: "",
|
||||
},
|
||||
{
|
||||
name: "base commit right below the amend commit",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "amend! Some feature"},
|
||||
{"def456", "Some feature"},
|
||||
},
|
||||
index: 0,
|
||||
expectedHash: "def456",
|
||||
},
|
||||
{
|
||||
name: "base commit further down the list",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "amend! Some feature"},
|
||||
{"def456", "Unrelated commit"},
|
||||
{"ghi789", "Some feature"},
|
||||
},
|
||||
index: 0,
|
||||
expectedHash: "ghi789",
|
||||
},
|
||||
{
|
||||
name: "amend commit in the middle of the list",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "Unrelated commit"},
|
||||
{"def456", "amend! Some feature"},
|
||||
{"ghi789", "Some feature"},
|
||||
},
|
||||
index: 1,
|
||||
expectedHash: "ghi789",
|
||||
},
|
||||
{
|
||||
name: "the nearest earlier amend commit holds the message",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "amend! Some feature"},
|
||||
{"def456", "amend! Some feature"},
|
||||
{"ghi789", "Some feature"},
|
||||
},
|
||||
index: 0,
|
||||
expectedHash: "def456",
|
||||
},
|
||||
{
|
||||
name: "fixup and squash commits in between are skipped",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "amend! Some feature"},
|
||||
{"def456", "fixup! Some feature"},
|
||||
{"ghi789", "squash! Some feature"},
|
||||
{"jkl012", "amend! Some feature"},
|
||||
{"mno345", "Some feature"},
|
||||
},
|
||||
index: 0,
|
||||
expectedHash: "jkl012",
|
||||
},
|
||||
{
|
||||
name: "amend commit with several prefixes applies to the innermost subject",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "amend! amend! Some feature"},
|
||||
{"def456", "amend! Some feature"},
|
||||
{"ghi789", "Some feature"},
|
||||
},
|
||||
index: 0,
|
||||
expectedHash: "def456",
|
||||
},
|
||||
{
|
||||
name: "base commit is not in the list",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "amend! Some feature"},
|
||||
{"def456", "Unrelated commit"},
|
||||
},
|
||||
index: 0,
|
||||
expectedHash: "",
|
||||
},
|
||||
{
|
||||
name: "base commit is above the amend commit",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "Some feature"},
|
||||
{"def456", "amend! Some feature"},
|
||||
},
|
||||
index: 1,
|
||||
expectedHash: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
commits := lo.Map(s.commits, func(desc commitDesc, _ int) *models.Commit {
|
||||
return models.NewCommit(hashPool, models.NewCommitOpts{Hash: desc.Hash, Name: desc.Name})
|
||||
})
|
||||
|
||||
result, ok := findCommitWithPreviousMessage(commits, commits[s.index])
|
||||
|
||||
if s.expectedHash == "" {
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, result)
|
||||
} else {
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, s.expectedHash, result.Hash())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixupHelper_messageAfterAmending(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
message string
|
||||
expectedMessage string
|
||||
}{
|
||||
{
|
||||
name: "subject only",
|
||||
message: "Some feature",
|
||||
expectedMessage: "Some feature",
|
||||
},
|
||||
{
|
||||
name: "subject and body",
|
||||
message: "Some feature\n\nSome description",
|
||||
expectedMessage: "Some feature\n\nSome description",
|
||||
},
|
||||
{
|
||||
name: "amend commit",
|
||||
message: "amend! Some feature\n\nA better subject\n\nSome description",
|
||||
expectedMessage: "A better subject\n\nSome description",
|
||||
},
|
||||
{
|
||||
name: "amend commit without a replacement message",
|
||||
message: "amend! Some feature",
|
||||
expectedMessage: "amend! Some feature",
|
||||
},
|
||||
{
|
||||
name: "fixup commit",
|
||||
message: "fixup! amend! Some feature\n\nSome description",
|
||||
expectedMessage: "fixup! amend! Some feature\n\nSome description",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.Equal(t, s.expectedMessage, messageAfterAmending(s.message))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixupHelper_removeFixupCommits(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
|
||||
|
|
|
|||
|
|
@ -706,7 +706,8 @@ func (self *LocalCommitsController) GetOnRenderToMain() func() {
|
|||
self.c.Tr.ExecCommandHere + "\n\n" + commit.Name)
|
||||
} else {
|
||||
refRange := self.context().GetSelectedRefRangeForDiffFiles()
|
||||
task = self.c.Helpers().Diff.GetUpdateTaskForRenderingCommitsDiff(commit, refRange)
|
||||
task = self.c.Helpers().Diff.GetUpdateTaskForRenderingCommitsDiff(
|
||||
self.c.Model().Commits, commit, refRange)
|
||||
}
|
||||
|
||||
self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ func (self *SubCommitsController) GetOnRenderToMain() func() {
|
|||
task = types.NewRenderStringTask("No commits")
|
||||
} else {
|
||||
refRange := self.context().GetSelectedRefRangeForDiffFiles()
|
||||
task = self.c.Helpers().Diff.GetUpdateTaskForRenderingCommitsDiff(commit, refRange)
|
||||
task = self.c.Helpers().Diff.GetUpdateTaskForRenderingCommitsDiff(
|
||||
self.c.Model().SubCommits, commit, refRange)
|
||||
}
|
||||
|
||||
self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import (
|
|||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/tasks"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func (gui *Gui) desiredPtySize(view *gocui.View) (cols, rows uint16) {
|
||||
|
|
@ -104,12 +103,7 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
|
|||
width = view.InnerWidth()
|
||||
pager := gui.stateAccessor.GetDiffRendererConfigManager().GetStdinFilterCommand(width)
|
||||
|
||||
// This communicates to diff renderers that we're in a very simple
|
||||
// terminal that they should not expect to have much capabilities.
|
||||
// Moving the cursor, clearing the screen, or querying for colors are among such "advanced" capabilities.
|
||||
// Context: https://github.com/jesseduffield/lazygit/issues/3419
|
||||
cmd.Env = removeExistingTermEnvVars(cmd.Env)
|
||||
cmd.Env = append(cmd.Env, "TERM=dumb")
|
||||
oscommands.SetDumbTerminalEnv(cmd)
|
||||
|
||||
cmd.Env = append(cmd.Env, "GIT_PAGER="+pager)
|
||||
|
||||
|
|
@ -202,20 +196,3 @@ func withPtyGitConfig(args []string, goos string) []string {
|
|||
result = append(result, "-c", "diff.autoRefreshIndex=false")
|
||||
return append(result, args[1:]...)
|
||||
}
|
||||
|
||||
func removeExistingTermEnvVars(env []string) []string {
|
||||
return lo.Filter(env, func(envVar string, _ int) bool {
|
||||
return !isTermEnvVar(envVar)
|
||||
})
|
||||
}
|
||||
|
||||
// Terminals set a variety of different environment variables
|
||||
// to identify themselves to processes. This list should catch the most common among them.
|
||||
func isTermEnvVar(envVar string) bool {
|
||||
return strings.HasPrefix(envVar, "TERM=") ||
|
||||
strings.HasPrefix(envVar, "TERM_PROGRAM=") ||
|
||||
strings.HasPrefix(envVar, "TERM_PROGRAM_VERSION=") ||
|
||||
strings.HasPrefix(envVar, "TERMINAL_EMULATOR=") ||
|
||||
strings.HasPrefix(envVar, "TERMINAL_NAME=") ||
|
||||
strings.HasPrefix(envVar, "TERMINAL_VERSION_")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -693,6 +693,7 @@ type TranslationSet struct {
|
|||
OpenCommandLogMenuTooltip string
|
||||
ShowingGitDiff string
|
||||
ShowingDiffForRange string
|
||||
CommitMessageChanges string
|
||||
CommitDiff string
|
||||
CopyCommitHashToClipboard string
|
||||
CommitHash string
|
||||
|
|
@ -1846,6 +1847,7 @@ func EnglishTranslationSet() *TranslationSet {
|
|||
OpenCommandLogMenuTooltip: "View options for the command log e.g. show/hide the command log and focus the command log.",
|
||||
ShowingGitDiff: "Showing output for:",
|
||||
ShowingDiffForRange: "Showing diff for range",
|
||||
CommitMessageChanges: "Commit message changes compared to {{.hash}}:",
|
||||
CommitDiff: "Commit diff",
|
||||
CopyCommitHashToClipboard: "Copy abbreviated commit hash to clipboard",
|
||||
CommitHash: "Commit hash",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var ShowAmendCommitMessageDiff = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Show a diff of the commit message when selecting an amend! commit",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
EmptyCommitWithBody("Fix the widget",
|
||||
"The frobnicator was not initialised properly at all.").
|
||||
EmptyCommitWithBody("amend! Fix the widget",
|
||||
"Fix the widget on startup\n\nThe frobnicator was not initialised at all.").
|
||||
EmptyCommitWithBody("amend! Fix the widget",
|
||||
"Fix the widget on startup\n\nThe frobnicator was not properly initialised at all.")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
// git's default colors for added and removed lines
|
||||
green := "#008000"
|
||||
red := "#800000"
|
||||
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("amend! Fix the widget").IsSelected(),
|
||||
Contains("amend! Fix the widget"),
|
||||
Contains("Fix the widget"),
|
||||
)
|
||||
|
||||
// The message of the topmost amend! commit is compared with the message
|
||||
// of the amend! commit below it, not with the one of the commit they
|
||||
// both apply to.
|
||||
t.Views().Main().
|
||||
TopLines(
|
||||
Contains("Commit message changes compared to"),
|
||||
Equals(" Fix the widget on startup"),
|
||||
Equals(" "),
|
||||
Equals("-The frobnicator was not initialised at all."),
|
||||
Equals("+The frobnicator was not properly initialised at all."),
|
||||
Contains("───"),
|
||||
).
|
||||
ContainsColoredText(green, "+The frobnicator was not properly initialised at all.").
|
||||
ContainsColoredText(red, "-The frobnicator was not initialised at all.")
|
||||
|
||||
t.Views().Commits().
|
||||
SelectNextItem()
|
||||
|
||||
// The other amend! commit is compared with the commit it applies to.
|
||||
t.Views().Main().
|
||||
TopLines(
|
||||
Contains("Commit message changes compared to"),
|
||||
Equals("-Fix the widget"),
|
||||
Equals("+Fix the widget on startup"),
|
||||
Equals(" "),
|
||||
Equals("-The frobnicator was not initialised properly at all."),
|
||||
Equals("+The frobnicator was not initialised at all."),
|
||||
Contains("───"),
|
||||
)
|
||||
|
||||
t.Views().Commits().
|
||||
SelectNextItem()
|
||||
|
||||
// The commit that they both apply to gets no such header.
|
||||
t.Views().Main().
|
||||
TopLines(
|
||||
Contains("commit "),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var ShowAmendCommitMessageDiffWithRenderer = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Show the commit message diff of an amend! commit through a diff renderer",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
// cat does nothing to the diff, but it is a diff renderer as far as
|
||||
// lazygit is concerned, so the diff takes the same route through it as
|
||||
// it would for a real one.
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{{Command: "cat"}}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
EmptyCommitWithBody("Fix the widget",
|
||||
"The frobnicator was not initialised.").
|
||||
EmptyCommitWithBody("amend! Fix the widget",
|
||||
"Fix the widget on startup\n\nThe frobnicator was not initialised.")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("amend! Fix the widget").IsSelected(),
|
||||
Contains("Fix the widget"),
|
||||
)
|
||||
|
||||
// A diff renderer states the two sides of the diff in its own way, so
|
||||
// unlike for git's own diff, the header naming them is kept.
|
||||
t.Views().Main().
|
||||
TopLines(
|
||||
Contains("Commit message changes compared to"),
|
||||
Equals("diff --git old message new message"),
|
||||
).
|
||||
ContainsLines(
|
||||
Contains("--- old message"),
|
||||
Contains("+++ new message"),
|
||||
Contains("@@"),
|
||||
Equals("-Fix the widget"),
|
||||
Equals("+Fix the widget on startup"),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -158,6 +158,8 @@ var tests = []*components.IntegrationTest{
|
|||
commit.Search,
|
||||
commit.SetAuthor,
|
||||
commit.SetAuthorRange,
|
||||
commit.ShowAmendCommitMessageDiff,
|
||||
commit.ShowAmendCommitMessageDiffWithRenderer,
|
||||
commit.StageRangeOfLines,
|
||||
commit.Staged,
|
||||
commit.StagedWithoutHooks,
|
||||
|
|
|
|||
Loading…
Reference in a new issue