This commit is contained in:
Harsh Abasaheb Chavan 2026-08-26 19:03:47 -04:00 committed by GitHub
commit 83b9c1bb15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 181 additions and 14 deletions

View file

@ -288,6 +288,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

View file

@ -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...)
@ -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
}
@ -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
@ -248,6 +261,7 @@ func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line
AuthorEmail: authorEmail,
Parents: parents,
Divergence: divergence,
GpgStatus: gpgStatus,
})
}
@ -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()
@ -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
})
@ -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").
@ -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`

View file

@ -27,6 +27,8 @@ 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
@ -34,6 +36,7 @@ func TestGetCommits(t *testing.T) {
expectedCommitOpts []models.NewCommitOpts
expectedError error
logOrder string
showGpg bool
opts GetCommitsOptions
mainBranches []string
}
@ -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",
@ -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{
@ -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{}
@ -605,6 +669,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) {
testName string
line string
showDivergence bool
showGpg bool
expectedCommit *models.Commit
}{
{
@ -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: "",
@ -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 {

View file

@ -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 {
@ -77,6 +81,7 @@ type NewCommitOpts struct {
UnixTimestamp int64
Divergence Divergence
Parents []string
GpgStatus string
}
func NewCommit(hashPool *utils.StringPool, opts NewCommitOpts) *Commit {
@ -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,
}
}

View file

@ -184,6 +184,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"`
@ -914,6 +916,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
CommitAuthorLongLength: 17,
CommitHashLength: 8,
ShowBranchCommitHash: false,
ShowGpgSigningStatus: false,
ShowDivergenceFromBaseBranch: "none",
CommandLogSize: 8,
SplitDiff: "auto",

View file

@ -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 != "" {
@ -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,

View file

@ -39,6 +39,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
selectedCommitHashPtr *string
startIdx int
endIdx int
showGpg bool
showGraph bool
bisectInfo *git_commands.BisectInfo
expected string
@ -89,6 +90,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{
@ -543,6 +562,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) })

View file

@ -822,6 +822,11 @@
"description": "If true, show commit hashes alongside branch names in the branches view.",
"default": false
},
"showGpgSigningStatus": {
"type": "boolean",
"description": "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.",
"default": false
},
"showDivergenceFromBaseBranch": {
"type": "string",
"enum": [