Skip to content

Commit 3094512

Browse files
Rework build info and improve CLI output formatting
- Replace GitTreeState/GitCommit with BuildMode in build info - Use RFC 3339 date format, only show build time in dev builds - Group issues by category and code, show occurrence counts - Group vulnerabilities by identifier, collapse duplicate packages - Add ruled line severity headers and cleaner summary footer
1 parent 6a63cfc commit 3094512

7 files changed

Lines changed: 230 additions & 117 deletions

File tree

.github/workflows/build-and-deploy.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
run: |
6969
BINARY_NAME="${{ needs.resolve-env.outputs.binary_name }}"
7070
cd cmd/deepsource && go build -tags static_all \
71-
-ldflags "-X 'main.version=${{ needs.resolve-env.outputs.version }}' -X 'main.Date=$(date -u +%Y-%m-%d)' -X 'main.SentryDSN=${{ secrets.SENTRY_DSN }}' -X 'main.buildMode=${{ needs.resolve-env.outputs.environment }}'" \
71+
-ldflags "-X 'main.version=${{ needs.resolve-env.outputs.version }}' -X 'main.Date=$(date -u +%Y-%m-%dT%H:%M:%SZ)' -X 'main.SentryDSN=${{ secrets.SENTRY_DSN }}' -X 'main.buildMode=${{ needs.resolve-env.outputs.environment }}'" \
7272
-o "$BINARY_NAME" .
7373
7474
- name: Package
@@ -106,7 +106,7 @@ jobs:
106106
run: |
107107
BINARY_NAME="${{ needs.resolve-env.outputs.binary_name }}"
108108
cd cmd/deepsource && go build -tags static_all \
109-
-ldflags "-X 'main.version=${{ needs.resolve-env.outputs.version }}' -X 'main.Date=$(date -u +%Y-%m-%d)' -X 'main.SentryDSN=${{ secrets.SENTRY_DSN }}' -X 'main.buildMode=${{ needs.resolve-env.outputs.environment }}'" \
109+
-ldflags "-X 'main.version=${{ needs.resolve-env.outputs.version }}' -X 'main.Date=$(date -u +%Y-%m-%dT%H:%M:%SZ)' -X 'main.SentryDSN=${{ secrets.SENTRY_DSN }}' -X 'main.buildMode=${{ needs.resolve-env.outputs.environment }}'" \
110110
-o "$BINARY_NAME" .
111111
112112
- name: Codesign
@@ -196,7 +196,7 @@ jobs:
196196
run: |
197197
BINARY_NAME="${{ needs.resolve-env.outputs.binary_name }}"
198198
cd cmd/deepsource && go build -tags static_all \
199-
-ldflags "-X 'main.version=${{ needs.resolve-env.outputs.version }}' -X 'main.Date=$(date -u +%Y-%m-%d)' -X 'main.SentryDSN=${{ secrets.SENTRY_DSN }}' -X 'main.buildMode=${{ needs.resolve-env.outputs.environment }}'" \
199+
-ldflags "-X 'main.version=${{ needs.resolve-env.outputs.version }}' -X 'main.Date=$(date -u +%Y-%m-%dT%H:%M:%SZ)' -X 'main.SentryDSN=${{ secrets.SENTRY_DSN }}' -X 'main.buildMode=${{ needs.resolve-env.outputs.environment }}'" \
200200
-o "${BINARY_NAME}.exe" .
201201
202202
- name: Package

buildinfo/version.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,18 @@ type BuildInfo struct {
2121
Version string `json:"version,omitempty"`
2222
// Date is the build date.
2323
Date time.Time `json:"date,omitempty"`
24-
// gitTreeState is the state of the git tree.
25-
GitTreeState string `json:"git_tree_state,omitempty"`
26-
// GitCommit is the git sha1.
27-
GitCommit string `json:"git_commit,omitempty"`
24+
// BuildMode is "dev" or "prod".
25+
BuildMode string `json:"build_mode,omitempty"`
2826
}
2927

30-
// Set's the build info as a package global.
31-
func SetBuildInfo(version, dateStr, gitTreeState, gitCommit string) {
32-
date, _ := time.Parse("2006-01-02", dateStr)
28+
// SetBuildInfo sets the build info as a package global.
29+
func SetBuildInfo(version, dateStr, buildMode string) {
30+
date, _ := time.Parse("2006-01-02T15:04:05Z", dateStr)
3331

3432
buildInfo = &BuildInfo{
35-
Version: version,
36-
Date: date,
37-
GitTreeState: gitTreeState,
38-
GitCommit: gitCommit,
33+
Version: version,
34+
Date: date,
35+
BuildMode: buildMode,
3936
}
4037
}
4138

@@ -45,9 +42,9 @@ func GetBuildInfo() *BuildInfo {
4542
}
4643

4744
func (bi BuildInfo) String() string {
48-
if bi.Date.IsZero() {
49-
return fmt.Sprintf("DeepSource CLI version %s", bi.Version)
45+
s := fmt.Sprintf("Version: v%s", bi.Version)
46+
if bi.BuildMode == "dev" && !bi.Date.IsZero() {
47+
s += fmt.Sprintf("\nBuild Time: %s", bi.Date.Format(time.RFC3339))
5048
}
51-
dateStr := bi.Date.Format("2006-01-02")
52-
return fmt.Sprintf("DeepSource CLI version %s (%s)", bi.Version, dateStr)
49+
return s
5350
}

buildinfo/version_test.go

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,43 @@ import (
77
)
88

99
func TestBuildInfo_String(t *testing.T) {
10-
date, _ := time.Parse("2006-01-02", "2021-01-21")
10+
date, _ := time.Parse("2006-01-02T15:04:05Z", "2021-01-21T10:30:00Z")
1111

12-
type fields struct {
13-
Version string
14-
Date time.Time
15-
GitTreeState string
16-
GitCommit string
17-
}
1812
tests := []struct {
19-
name string
20-
fields fields
21-
want string
13+
name string
14+
version string
15+
date time.Time
16+
buildMode string
17+
want string
2218
}{
2319
{
24-
"must return the correct version string when date and version is available",
25-
fields{
26-
Version: "1.5.0",
27-
Date: date,
28-
},
29-
"DeepSource CLI version 1.5.0 (2021-01-21)",
20+
"dev build with date shows build time",
21+
"1.5.0",
22+
date,
23+
"dev",
24+
"Version: v1.5.0\nBuild Time: 2021-01-21T10:30:00Z",
25+
},
26+
{
27+
"prod build with date hides build time",
28+
"1.5.0",
29+
date,
30+
"prod",
31+
"Version: v1.5.0",
3032
},
3133
{
32-
"must return the correct version string when only version is available",
33-
fields{
34-
Version: "1.5.0",
35-
},
36-
"DeepSource CLI version 1.5.0",
34+
"version only",
35+
"1.5.0",
36+
time.Time{},
37+
"",
38+
"Version: v1.5.0",
3739
},
3840
}
3941
for _, tt := range tests {
4042
t.Run(tt.name, func(t *testing.T) {
4143
bi := BuildInfo{
42-
Version: tt.fields.Version,
43-
Date: tt.fields.Date,
44-
GitTreeState: tt.fields.GitTreeState,
45-
GitCommit: tt.fields.GitCommit,
44+
Version: tt.version,
45+
Date: tt.date,
46+
BuildMode: tt.buildMode,
4647
}
4748
if got := bi.String(); got != tt.want {
4849
t.Errorf("BuildInfo.String() = %v, want %v", got, tt.want)
@@ -52,31 +53,30 @@ func TestBuildInfo_String(t *testing.T) {
5253
}
5354

5455
func TestSetBuildInfo(t *testing.T) {
55-
date, _ := time.Parse("2006-01-02", "2021-01-21")
56+
date, _ := time.Parse("2006-01-02T15:04:05Z", "2021-01-21T10:30:00Z")
5657

5758
want := &BuildInfo{
58-
Version: "1.0.0",
59-
Date: date,
59+
Version: "1.0.0",
60+
Date: date,
61+
BuildMode: "dev",
6062
}
6163

62-
type args struct {
63-
version string
64-
dateStr string
65-
gitTreeState string
66-
gitCommit string
67-
}
6864
tests := []struct {
69-
name string
70-
args args
65+
name string
66+
version string
67+
dateStr string
68+
buildMode string
7169
}{
7270
{
7371
"must set the buildInfo package global",
74-
args{version: "1.0.0", dateStr: "2021-01-21"},
72+
"1.0.0",
73+
"2021-01-21T10:30:00Z",
74+
"dev",
7575
},
7676
}
7777
for _, tt := range tests {
7878
t.Run(tt.name, func(t *testing.T) {
79-
SetBuildInfo(tt.args.version, tt.args.dateStr, tt.args.gitTreeState, tt.args.gitCommit)
79+
SetBuildInfo(tt.version, tt.dateStr, tt.buildMode)
8080
})
8181
if !reflect.DeepEqual(buildInfo, want) {
8282
t.Errorf("buildInfo = %v, want %v", buildInfo, want)

cmd/deepsource/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func main() {
6666
}
6767
}()
6868

69-
v.SetBuildInfo(version, Date, "", "")
69+
v.SetBuildInfo(version, Date, buildMode)
7070

7171
if err := command.Execute(); err != nil {
7272
var cliErr *clierrors.CLIError

command/issues/issues.go

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func NewCmdIssuesWithDeps(deps *cmddeps.Deps) *cobra.Command {
102102
cmd.Flags().StringVar(&opts.OutputFile, "output-file", "", "Write output to a file instead of stdout")
103103

104104
// --verbose, -v flag
105-
cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show issue code, analyzer, and description")
105+
cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show issue description")
106106

107107
// Scoping flags
108108
cmd.Flags().StringVar(&opts.CommitOid, "commit", "", "Scope to a specific analysis run by commit OID")
@@ -312,6 +312,68 @@ func matchesPathFilters(path string, filters []string) bool {
312312

313313
// --- Human output ---
314314

315+
type codeGroup struct {
316+
issue issues.Issue
317+
locations []issues.Location
318+
}
319+
320+
type categoryGroup struct {
321+
category string
322+
codes []codeGroup
323+
total int
324+
}
325+
326+
// groupByCategoryAndCode groups issues first by category (preserving first-seen order),
327+
// then by issue code within each category.
328+
func groupByCategoryAndCode(list []issues.Issue) []categoryGroup {
329+
catOrder := []string{}
330+
catMap := map[string]*categoryGroup{}
331+
332+
for _, issue := range list {
333+
cat := issue.IssueCategory
334+
cg, catExists := catMap[cat]
335+
if !catExists {
336+
catOrder = append(catOrder, cat)
337+
cg = &categoryGroup{category: cat}
338+
catMap[cat] = cg
339+
}
340+
341+
found := false
342+
for i := range cg.codes {
343+
if cg.codes[i].issue.IssueCode == issue.IssueCode {
344+
cg.codes[i].locations = append(cg.codes[i].locations, issue.Location)
345+
found = true
346+
break
347+
}
348+
}
349+
if !found {
350+
cg.codes = append(cg.codes, codeGroup{
351+
issue: issue,
352+
locations: []issues.Location{issue.Location},
353+
})
354+
}
355+
cg.total++
356+
}
357+
358+
result := make([]categoryGroup, 0, len(catOrder))
359+
for _, cat := range catOrder {
360+
result = append(result, *catMap[cat])
361+
}
362+
return result
363+
}
364+
365+
const ruledLineWidth = 55
366+
367+
func severityRuledHeader(sev string, count int) string {
368+
label := fmt.Sprintf("── %s (%d) ", humanizeSeverity(sev), count)
369+
pad := ruledLineWidth - len(label)
370+
if pad < 3 {
371+
pad = 3
372+
}
373+
line := label + strings.Repeat("─", pad)
374+
return colorSeverity(sev, line)
375+
}
376+
315377
func (opts *IssuesOptions) outputHuman() error {
316378
if len(opts.issues) == 0 {
317379
if opts.hasFilters() {
@@ -332,44 +394,56 @@ func (opts *IssuesOptions) outputHuman() error {
332394
}
333395

334396
for _, sev := range order {
335-
group, ok := groups[sev]
336-
if !ok || len(group) == 0 {
397+
sevGroup, ok := groups[sev]
398+
if !ok || len(sevGroup) == 0 {
337399
continue
338400
}
339401

340-
header := fmt.Sprintf("%s (%d)", humanizeSeverity(sev), len(group))
341-
fmt.Println(colorSeverity(sev, header))
402+
fmt.Println(severityRuledHeader(sev, len(sevGroup)))
342403
fmt.Println()
343404

344-
for _, issue := range group {
345-
location := formatLocation(issue, cwd)
346-
category := humanizeCategory(issue.IssueCategory)
347-
fmt.Printf(" %s: %s (%s)\n", category, issue.IssueText, location)
405+
catGroups := groupByCategoryAndCode(sevGroup)
406+
for _, cg := range catGroups {
407+
fmt.Printf(" %s (%d)\n\n", humanizeCategory(cg.category), cg.total)
408+
409+
for _, ig := range cg.codes {
410+
fmt.Printf(" %s %s\n", pterm.Bold.Sprint("✗"), pterm.Bold.Sprint(ig.issue.IssueText))
411+
412+
analyzer := analyzerDisplayName(ig.issue.Analyzer)
413+
meta := fmt.Sprintf("%s · %s", ig.issue.IssueCode, analyzer)
414+
if len(ig.locations) > 1 {
415+
meta += fmt.Sprintf(" (%d occurrences)", len(ig.locations))
416+
}
417+
fmt.Printf(" %s\n", pterm.Gray(meta))
348418

349-
if opts.Verbose {
350-
analyzer := analyzerDisplayName(issue.Analyzer)
351-
fmt.Printf(" %s · %s\n", issue.IssueCode, analyzer)
352-
if issue.Description != "" {
353-
fmt.Printf(" %s\n", issue.Description)
419+
for _, loc := range ig.locations {
420+
fmt.Printf(" %s\n", pterm.Gray(formatLocationFromParts(loc, cwd)))
354421
}
422+
423+
if opts.Verbose && ig.issue.Description != "" {
424+
desc := ig.issue.Description
425+
if idx := strings.IndexByte(desc, '\n'); idx != -1 {
426+
desc = desc[:idx]
427+
}
428+
fmt.Printf(" %s\n", pterm.Gray(desc))
429+
}
430+
355431
fmt.Println()
356432
}
357433
}
358-
359-
if !opts.Verbose {
360-
fmt.Println()
361-
}
362434
}
363435

364-
fmt.Printf("Showing %d issue(s) in %s", len(opts.issues), opts.repoSlug)
436+
fmt.Println(strings.Repeat("─", ruledLineWidth))
437+
438+
scope := "default branch"
365439
switch {
366440
case opts.CommitOid != "":
367-
fmt.Printf(" from commit %s\n", opts.CommitOid)
441+
scope = "commit " + opts.CommitOid
368442
case opts.PRNumber > 0:
369-
fmt.Printf(" from PR #%d\n", opts.PRNumber)
370-
default:
371-
fmt.Println(" from default branch")
443+
scope = fmt.Sprintf("PR #%d", opts.PRNumber)
372444
}
445+
fmt.Printf("%d issues · %s · %s\n", len(opts.issues), opts.repoSlug, scope)
446+
373447
return nil
374448
}
375449

@@ -569,14 +643,19 @@ func formatSeverity(severity string) string {
569643
}
570644
}
571645

572-
func formatLocation(issue issues.Issue, cwd string) string {
573-
filePath := issue.Location.Path
646+
func formatLocationFromParts(loc issues.Location, cwd string) string {
647+
filePath := loc.Path
574648
if cwd != "" && strings.HasPrefix(filePath, cwd) {
575649
filePath = strings.TrimPrefix(filePath, cwd+"/")
576650
}
577-
if issue.Location.Position.BeginLine == issue.Location.Position.EndLine {
578-
return fmt.Sprintf("%s:%d", filePath, issue.Location.Position.BeginLine)
651+
if loc.Position.BeginLine == loc.Position.EndLine {
652+
return fmt.Sprintf("%s:%d", filePath, loc.Position.BeginLine)
579653
}
580-
return fmt.Sprintf("%s:%d-%d", filePath, issue.Location.Position.BeginLine, issue.Location.Position.EndLine)
654+
return fmt.Sprintf("%s:%d-%d", filePath, loc.Position.BeginLine, loc.Position.EndLine)
655+
}
656+
657+
658+
func formatLocation(issue issues.Issue, cwd string) string {
659+
return formatLocationFromParts(issue.Location, cwd)
581660
}
582661

command/root.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package command
22

33
import (
44
"context"
5-
"fmt"
65

76
"github.com/deepsourcelabs/cli/buildinfo"
87
"github.com/deepsourcelabs/cli/command/auth"
@@ -28,7 +27,7 @@ func NewCmdRoot() *cobra.Command {
2827
info := buildinfo.GetBuildInfo()
2928
if info != nil {
3029
cmd.Version = info.Version
31-
cmd.SetVersionTemplate(fmt.Sprintf("DeepSource CLI %s (%s)\n", info.Version, info.GitCommit))
30+
cmd.SetVersionTemplate(info.String() + "\n")
3231
}
3332

3433
// Disable default completion command

0 commit comments

Comments
 (0)