Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs-master/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ gui:
# If true, show commit hashes alongside branch names in the branches view.
showBranchCommitHash: false

# If true, show GPG signature verification status for each commit in the
# commits view. This can slow down commit loading as it requires GPG
# verification.
showGpgSigningStatus: false

# Whether to show the divergence from the base branch in the branches view.
# One of: 'none' | 'onlyArrow' | 'arrowAndNumber'
showDivergenceFromBaseBranch: none
Expand Down
54 changes: 42 additions & 12 deletions pkg/commands/git_commands/commit_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
defer wg.Done()

var realCommits []*models.Commit
showGpg := self.UserConfig().Gui.ShowGpgSigningStatus
realCommits, logErr = loadCommits(self.getLogCmd(opts), opts.FilterPath, func(line string) (*models.Commit, bool) {
return self.extractCommitFromLine(opts.HashPool, line, opts.RefToShowDivergenceFrom != ""), false
return self.extractCommitFromLine(opts.HashPool, line, opts.RefToShowDivergenceFrom != "", showGpg), false
})
if logErr == nil {
commits = append(commits, realCommits...)
Expand Down Expand Up @@ -189,12 +190,16 @@ func (self *CommitLoader) MergeRebasingCommits(hashPool *utils.StringPool, commi
// then puts them into a commit object
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line string, showDivergence bool) *models.Commit {
split := strings.SplitN(line, "\x00", 8)
func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line string, showDivergence bool, showGpg bool) *models.Commit {
numFields := 8
if showGpg {
numFields = 9
}
split := strings.SplitN(line, "\x00", numFields)

// Ensure we have the minimum required fields (at least 7 for basic functionality)
if len(split) < 7 {
self.Log.Warnf("Malformed git log line: expected at least 7 fields, got %d. Line: %s", len(split), line)
minFields := 7
if len(split) < minFields {
self.Log.Warnf("Malformed git log line: expected at least %d fields, got %d. Line: %s", minFields, len(split), line)
return nil
}

Expand All @@ -209,10 +214,18 @@ func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line
}
extraInfo := strings.TrimSpace(split[6])

// message (and the \x00 before it) might not be present if extraInfo is extremely long
gpgStatus := ""
messageIdx := 7
if showGpg {
if len(split) > 7 {
gpgStatus = split[7]
}
messageIdx = 8
}

message := ""
if len(split) > 7 {
message = split[7]
if len(split) > messageIdx {
message = split[messageIdx]
}

var tags []string
Expand Down Expand Up @@ -248,6 +261,7 @@ func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line
AuthorEmail: authorEmail,
Parents: parents,
Divergence: divergence,
GpgStatus: gpgStatus,
})
}

Expand Down Expand Up @@ -285,10 +299,16 @@ func (self *CommitLoader) getHydratedTodoCommits(hashPool *utils.StringPool, tod

// note that we're not filtering these as we do non-rebasing commits just because
// I suspect that will cause some damage
showGpg := self.UserConfig().Gui.ShowGpgSigningStatus
format := prettyFormat
if showGpg {
format = prettyFormatWithGpg
}

cmdObj := self.cmd.New(
NewGitCmd("show").
Config("log.showSignature=false").
Arg("--no-patch", "--oneline", "--abbrev=20", prettyFormat).
Arg("--no-patch", "--oneline", "--abbrev=20", format).
Arg(commitHashes...).
ToArgv(),
).DontLog()
Expand All @@ -298,7 +318,10 @@ func (self *CommitLoader) getHydratedTodoCommits(hashPool *utils.StringPool, tod
if line == "" || line[0] != '+' {
return false, nil
}
commit := self.extractCommitFromLine(hashPool, line[1:], false)
commit := self.extractCommitFromLine(hashPool, line[1:], false, showGpg)
if commit == nil {
return false, nil
}
fullCommits[commit.Hash()] = commit
return false, nil
})
Expand Down Expand Up @@ -586,12 +609,18 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj {
refSpec += "..." + opts.RefToShowDivergenceFrom
}

showGpg := self.UserConfig().Gui.ShowGpgSigningStatus
format := prettyFormat
if showGpg {
format = prettyFormatWithGpg
}

cmdArgs := NewGitCmd("log").
Arg(refSpec).
ArgIf(gitLogOrder != "default", "--"+gitLogOrder).
ArgIf(opts.All, "--all").
Arg("--oneline").
Arg(prettyFormat).
Arg(format).
Arg("--abbrev=40").
ArgIf(opts.FilterAuthor != "", "--author="+opts.FilterAuthor).
ArgIf(opts.Limit, "-300").
Expand All @@ -606,3 +635,4 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj {
}

const prettyFormat = `--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s`
const prettyFormatWithGpg = `--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%G?%x00%s`
85 changes: 84 additions & 1 deletion pkg/commands/git_commands/commit_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ var commitsOutput = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18db

var singleCommitOutput = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|b21997d6b4cbdf84b149|>|HEAD -> better-tests|better typing for rebase mode`, "|", "\x00")

var singleCommitOutputWithGpg = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|b21997d6b4cbdf84b149|>|HEAD -> better-tests|G|better typing for rebase mode`, "|", "\x00")

func TestGetCommits(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
expectedCommitOpts []models.NewCommitOpts
expectedError error
logOrder string
showGpg bool
opts GetCommitsOptions
mainBranches []string
}
Expand Down Expand Up @@ -61,6 +64,30 @@ func TestGetCommits(t *testing.T) {
expectedCommitOpts: []models.NewCommitOpts{},
expectedError: nil,
},
{
testName: "should return commits with gpg status when enabled",
logOrder: "topo-order",
showGpg: true,
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: &models.Branch{Name: "mybranch"}, IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"rev-list", "refs/heads/mybranch", "^mybranch@{u}"}, "0eea75e8c631fba6b58135697835d58ba4c18dbc\n", nil).
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%G?%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutputWithGpg, nil),

expectedCommitOpts: []models.NewCommitOpts{{
Hash: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
Status: models.StatusUnpushed,
Action: models.ActionNone,
Tags: nil,
ExtraInfo: "(HEAD -> better-tests)",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640826609,
Parents: []string{"b21997d6b4cbdf84b149"},
GpgStatus: "G",
}},
expectedError: nil,
},
{
testName: "should return commits if they are present",
logOrder: "topo-order",
Expand Down Expand Up @@ -300,6 +327,7 @@ func TestGetCommits(t *testing.T) {
t.Run(scenario.testName, func(t *testing.T) {
common := common.NewDummyCommon()
common.UserConfig().Git.Log.Order = scenario.logOrder
common.UserConfig().Gui.ShowGpgSigningStatus = scenario.showGpg
cmd := oscommands.NewDummyCmdObjBuilder(scenario.runner)

builder := &CommitLoader{
Expand Down Expand Up @@ -333,6 +361,42 @@ func TestGetCommits(t *testing.T) {
}
}

func TestCommitLoader_getHydratedTodoCommitsWithGpgStatus(t *testing.T) {
common := common.NewDummyCommon()
common.UserConfig().Gui.ShowGpgSigningStatus = true
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", "log.showSignature=false", "show", "--no-patch", "--oneline", "--abbrev=20", prettyFormatWithGpg, "0eea75e8c631fba6b58135697835d58ba4c18dbc"}, singleCommitOutputWithGpg, nil)

hashPool := &utils.StringPool{}
loader := &CommitLoader{
Common: common,
cmd: oscommands.NewDummyCmdObjBuilder(runner),
}

todoCommits := []*models.Commit{models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Status: models.StatusRebasing,
Action: todo.Pick,
})}

commits, err := loader.getHydratedTodoCommits(hashPool, todoCommits, false)

assert.NoError(t, err)
assert.Equal(t, []*models.Commit{models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
Status: models.StatusRebasing,
Action: todo.Pick,
ExtraInfo: "(HEAD -> better-tests)",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640826609,
Parents: []string{"b21997d6b4cbdf84b149"},
GpgStatus: "G",
})}, commits)
runner.CheckForMissingCalls()
}

func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
hashPool := &utils.StringPool{}

Expand Down Expand Up @@ -605,6 +669,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) {
testName string
line string
showDivergence bool
showGpg bool
expectedCommit *models.Commit
}{
{
Expand Down Expand Up @@ -759,6 +824,24 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) {
Divergence: models.DivergenceLeft,
}),
},
{
testName: "valid line with gpg status",
line: "hash\x00timestamp\x00author\x00email\x00parents\x00<\x00extraInfo\x00G\x00message",
showDivergence: true,
showGpg: true,
expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "hash",
Name: "message",
Tags: nil,
ExtraInfo: "(extraInfo)",
UnixTimestamp: 0,
AuthorName: "author",
AuthorEmail: "email",
Parents: []string{"parents"},
Divergence: models.DivergenceLeft,
GpgStatus: "G",
}),
},
{
testName: "empty line",
line: "",
Expand All @@ -785,7 +868,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) {

for _, scenario := range scenarios {
t.Run(scenario.testName, func(t *testing.T) {
result := loader.extractCommitFromLine(hashPool, scenario.line, scenario.showDivergence)
result := loader.extractCommitFromLine(hashPool, scenario.line, scenario.showDivergence, scenario.showGpg)
if scenario.expectedCommit == nil {
assert.Nil(t, result)
} else {
Expand Down
6 changes: 6 additions & 0 deletions pkg/commands/models/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ type Commit struct {
Action todo.TodoCommand
ActionFlag string // e.g. "-C" for fixup -C
Divergence Divergence // set to DivergenceNone unless we are showing the divergence view

// GPG signature status: "G" good, "B" bad, "U" unknown validity,
// "X" expired, "Y" expired key, "R" revoked, "E" missing key, "N" none
GpgStatus string
}

type NewCommitOpts struct {
Expand All @@ -77,6 +81,7 @@ type NewCommitOpts struct {
UnixTimestamp int64
Divergence Divergence
Parents []string
GpgStatus string
}

func NewCommit(hashPool *utils.StringPool, opts NewCommitOpts) *Commit {
Expand All @@ -93,6 +98,7 @@ func NewCommit(hashPool *utils.StringPool, opts NewCommitOpts) *Commit {
UnixTimestamp: opts.UnixTimestamp,
Divergence: opts.Divergence,
parents: lo.Map(opts.Parents, func(s string, _ int) *string { return hashPool.Add(s) }),
GpgStatus: opts.GpgStatus,
}
}

Expand Down
3 changes: 3 additions & 0 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ type GuiConfig struct {
CommitHashLength int `yaml:"commitHashLength" jsonschema:"minimum=0"`
// If true, show commit hashes alongside branch names in the branches view.
ShowBranchCommitHash bool `yaml:"showBranchCommitHash"`
// If true, show GPG signature verification status for each commit in the commits view. This can slow down commit loading as it requires GPG verification.
ShowGpgSigningStatus bool `yaml:"showGpgSigningStatus"`
// Whether to show the divergence from the base branch in the branches view.
// One of: 'none' | 'onlyArrow' | 'arrowAndNumber'
ShowDivergenceFromBaseBranch string `yaml:"showDivergenceFromBaseBranch" jsonschema:"enum=none,enum=onlyArrow,enum=arrowAndNumber"`
Expand Down Expand Up @@ -820,6 +822,7 @@ func GetDefaultConfig() *UserConfig {
CommitAuthorLongLength: 17,
CommitHashLength: 8,
ShowBranchCommitHash: false,
ShowGpgSigningStatus: false,
ShowDivergenceFromBaseBranch: "none",
CommandLogSize: 8,
SplitDiff: "auto",
Expand Down
17 changes: 16 additions & 1 deletion pkg/gui/presentation/commits.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,20 @@ func displayCommit(
actionString = actionColorMap(commit.Action, commit.Status).Sprint(actionStr)
}

gpgString := ""
if common.UserConfig().Gui.ShowGpgSigningStatus {
switch commit.GpgStatus {
case "G", "U":
gpgString = style.FgGreen.Sprint("✓")
case "B", "R", "X", "Y":
gpgString = style.FgRed.Sprint("✗")
case "E":
gpgString = style.FgYellow.Sprint("~")
default:
gpgString = style.FgBlue.Sprint("-")
}
}

tagString := ""
if fullDescription {
if commit.ExtraInfo != "" {
Expand Down Expand Up @@ -439,11 +453,12 @@ func displayCommit(
}
author := authors.AuthorWithLength(commit.AuthorName, authorLength)

cols := make([]string, 0, 7)
cols := make([]string, 0, 8)
cols = append(
cols,
divergenceString,
hashString,
gpgString,
bisectString,
descriptionString,
actionString,
Expand Down
20 changes: 20 additions & 0 deletions pkg/gui/presentation/commits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
selectedCommitHashPtr *string
startIdx int
endIdx int
showGpg bool
showGraph bool
bisectInfo *git_commands.BisectInfo
expected string
Expand Down Expand Up @@ -90,6 +91,24 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
hash2 commit2
`),
},
{
testName: "show gpg status when enabled",
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1", GpgStatus: "G"},
{Name: "commit2", Hash: "hash2", GpgStatus: "N"},
},
startIdx: 0,
endIdx: 2,
showGpg: true,
showGraph: false,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
hash1 ✓ commit1
hash2 - commit2
`),
},
{
testName: "show local branch head, except the current branch, main branches, or merged branches",
commitOpts: []models.NewCommitOpts{
Expand Down Expand Up @@ -546,6 +565,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
if !focusing || s.focus {
t.Run(s.testName, func(t *testing.T) {
hashPool := &utils.StringPool{}
common.UserConfig().Gui.ShowGpgSigningStatus = s.showGpg

commits := lo.Map(s.commitOpts,
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
Expand Down
Loading