Skip to content

Commit f84efb0

Browse files
committed
Address #227 fully.
1 parent 566da0b commit f84efb0

9 files changed

Lines changed: 361 additions & 41 deletions

File tree

cmd/report.go

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ func changerateCommit(commit *model.Commit, breakingConfig *whatChangedModel.Bre
3232
}
3333

3434
type historicalFlattenResult struct {
35-
Reports []*model.FlatReport
36-
SkippedCommits []string
35+
Reports []*model.FlatReport
36+
SkippedCommits []string
37+
SuccessfulComparisons int
3738
}
3839

3940
func changerateAndFlattenHistory(commits []*model.Commit, breakingConfig *whatChangedModel.BreakingRulesConfig) (*historicalFlattenResult, error) {
@@ -72,8 +73,9 @@ func changerateAndFlattenHistory(commits []*model.Commit, breakingConfig *whatCh
7273
fmt.Fprintf(os.Stderr, "warning: %d commits failed to render report data\n", len(renderErrors))
7374
}
7475
return &historicalFlattenResult{
75-
Reports: reports,
76-
SkippedCommits: skippedCommits,
76+
Reports: reports,
77+
SkippedCommits: skippedCommits,
78+
SuccessfulComparisons: processedComparables,
7779
}, nil
7880
}
7981

@@ -102,35 +104,7 @@ func runGitHistoryReport(gitPath, filePath string, opts summaryOpts, breakingCon
102104
if err != nil {
103105
return nil, err
104106
}
105-
if loaded == nil {
106-
return nil, nil
107-
}
108-
if len(loaded.Commits) == 0 && len(loaded.SkippedCommits) == 0 {
109-
return nil, nil
110-
}
111-
history, err := changerateAndFlattenHistory(loaded.Commits, breakingConfig)
112-
if err != nil {
113-
return nil, err
114-
}
115-
skippedCommits := mergeSkippedCommitHashes(loaded.SkippedCommits, history.SkippedCommits)
116-
if len(history.Reports) == 0 && len(skippedCommits) > 0 {
117-
return nil, fmt.Errorf("all %d candidate commits were skipped or failed to render", len(skippedCommits))
118-
}
119-
120-
report := &model.FlatHistoricalReport{
121-
GitRepoPath: gitPath,
122-
GitFilePath: filePath,
123-
Filename: path.Base(filePath),
124-
DateGenerated: time.Now().Format(time.RFC3339),
125-
Reports: history.Reports,
126-
}
127-
if len(skippedCommits) > 0 {
128-
report.MetaData = &model.HistoricalReportMetaData{
129-
Partial: true,
130-
SkippedCommits: skippedCommits,
131-
}
132-
}
133-
return report, nil
107+
return buildHistoricalReport(gitPath, filePath, loaded, breakingConfig)
134108
}
135109

136110
func runGithubHistoryReport(rawURL string, opts summaryOpts, breakingConfig *whatChangedModel.BreakingRulesConfig) (*model.FlatHistoricalReport, error) {
@@ -147,23 +121,30 @@ func runGithubHistoryReport(rawURL string, opts summaryOpts, breakingConfig *wha
147121
if err != nil {
148122
return nil, err
149123
}
124+
return buildHistoricalReport(fmt.Sprintf("%s/%s", user, repo), filePath, loaded, breakingConfig)
125+
}
126+
127+
func buildHistoricalReport(repoPath, filePath string, loaded *loadedHistoryResult,
128+
breakingConfig *whatChangedModel.BreakingRulesConfig,
129+
) (*model.FlatHistoricalReport, error) {
150130
if loaded == nil {
151131
return nil, nil
152132
}
153133
if len(loaded.Commits) == 0 && len(loaded.SkippedCommits) == 0 {
154134
return nil, nil
155135
}
136+
156137
history, err := changerateAndFlattenHistory(loaded.Commits, breakingConfig)
157138
if err != nil {
158139
return nil, err
159140
}
160141
skippedCommits := mergeSkippedCommitHashes(loaded.SkippedCommits, history.SkippedCommits)
161-
if len(history.Reports) == 0 && len(skippedCommits) > 0 {
142+
if len(history.Reports) == 0 && history.SuccessfulComparisons == 0 && len(skippedCommits) > 0 {
162143
return nil, fmt.Errorf("all %d candidate commits were skipped or failed to render", len(skippedCommits))
163144
}
164145

165146
report := &model.FlatHistoricalReport{
166-
GitRepoPath: fmt.Sprintf("%s/%s", user, repo),
147+
GitRepoPath: repoPath,
167148
GitFilePath: filePath,
168149
Filename: path.Base(filePath),
169150
DateGenerated: time.Now().Format(time.RFC3339),

cmd/report_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,42 @@ func TestRunGitHistoryReport_BasePathUsesRevisionScopedSiblingRefs(t *testing.T)
324324
assert.Contains(t, content, `"path":"$.paths['/thing'].get.responses['200'].content['application/json'].schema"`)
325325
}
326326

327+
func TestRunGitHistoryReport_LimitOneComparesAgainstParent(t *testing.T) {
328+
repoDir := createGitSpecRepoForFile(t, "openapi.yaml")
329+
330+
report, err := runGitHistoryReport(repoDir, "openapi.yaml", summaryOpts{
331+
base: repoDir,
332+
noColor: true,
333+
limit: 1,
334+
limitTime: -1,
335+
}, nil)
336+
337+
require.NoError(t, err)
338+
require.NotNil(t, report)
339+
require.Len(t, report.Reports, 1)
340+
assert.Equal(t, gitOutputInDir(t, repoDir, "rev-parse", "--short", "HEAD"), report.Reports[0].Commit.Hash)
341+
assert.NotEmpty(t, report.Reports[0].Changes)
342+
}
343+
344+
func TestRunGitHistoryReport_BaseCommitUsesExcludedParentAsBaseline(t *testing.T) {
345+
repoDir := createGitSpecRepoForFile(t, "openapi.yaml")
346+
baseCommit := gitOutputInDir(t, repoDir, "rev-parse", "--short", "HEAD~2")
347+
348+
report, err := runGitHistoryReport(repoDir, "openapi.yaml", summaryOpts{
349+
base: repoDir,
350+
baseCommit: baseCommit,
351+
noColor: true,
352+
limitTime: -1,
353+
}, nil)
354+
355+
require.NoError(t, err)
356+
require.NotNil(t, report)
357+
require.Len(t, report.Reports, 2)
358+
assert.Equal(t, gitOutputInDir(t, repoDir, "rev-parse", "--short", "HEAD"), report.Reports[0].Commit.Hash)
359+
assert.Equal(t, gitOutputInDir(t, repoDir, "rev-parse", "--short", "HEAD~1"), report.Reports[1].Commit.Hash)
360+
assert.NotEmpty(t, report.Reports[1].Changes)
361+
}
362+
327363
func TestRunGitHistoryReport_PartialHistoryIncludesMetaData(t *testing.T) {
328364
fixture := testutil.CreateInvalidHistoryGitSpecRepo(t)
329365

@@ -343,6 +379,23 @@ func TestRunGitHistoryReport_PartialHistoryIncludesMetaData(t *testing.T) {
343379
assert.Contains(t, string(encoded), fixture.InvalidHash)
344380
}
345381

382+
func TestRunGitHistoryReport_PartialHistoryWithoutChangesReturnsEmptyPartialReport(t *testing.T) {
383+
fixture := testutil.CreateRevertedHistoryGitSpecRepo(t)
384+
385+
report, err := runGitHistoryReport(fixture.RepoDir, fixture.FileName, summaryOpts{
386+
base: fixture.RepoDir,
387+
noColor: true,
388+
limitTime: -1,
389+
}, nil)
390+
391+
require.NoError(t, err)
392+
require.NotNil(t, report)
393+
assert.Empty(t, report.Reports)
394+
require.NotNil(t, report.MetaData)
395+
assert.True(t, report.MetaData.Partial)
396+
assert.Equal(t, []string{fixture.InvalidHash}, report.MetaData.SkippedCommits)
397+
}
398+
346399
func TestRunGitHistoryReport_AllInvalidHistoryFails(t *testing.T) {
347400
repoDir := t.TempDir()
348401
runGitInDir(t, repoDir, "init")
@@ -412,6 +465,44 @@ paths:
412465
assert.Equal(t, []string{"bbb222"}, report.MetaData.SkippedCommits)
413466
}
414467

468+
func TestRunGithubHistoryReport_PartialHistoryWithoutChangesReturnsEmptyPartialReport(t *testing.T) {
469+
originalProcess := processGithubRepoDetailed
470+
t.Cleanup(func() {
471+
processGithubRepoDetailed = originalProcess
472+
})
473+
474+
processGithubRepoDetailed = func(username, repo, filePath string,
475+
progressChan chan *model.ProgressUpdate, errorChan chan model.ProgressError,
476+
opts git.HistoryOptions, breakingConfig *whatChangedModel.BreakingRulesConfig,
477+
) (*git.HistoryBuildResult, []error) {
478+
return &git.HistoryBuildResult{
479+
Commits: []*model.Commit{
480+
mustMakeDoctorOnlyCommitFromSpecs(t, "ccc333", `openapi: 3.0.3
481+
info:
482+
title: test
483+
version: "1.0.0"
484+
paths: {}
485+
`, `openapi: 3.0.3
486+
info:
487+
title: test
488+
version: "1.0.0"
489+
paths: {}
490+
`),
491+
},
492+
SkippedCommits: []string{"bbb222"},
493+
}, nil
494+
}
495+
496+
report, err := runGithubHistoryReport("https://github.com/oai/openapi-specification/blob/main/examples/v3.0/petstore.yaml", summaryOpts{}, nil)
497+
498+
require.NoError(t, err)
499+
require.NotNil(t, report)
500+
assert.Empty(t, report.Reports)
501+
require.NotNil(t, report.MetaData)
502+
assert.True(t, report.MetaData.Partial)
503+
assert.Equal(t, []string{"bbb222"}, report.MetaData.SkippedCommits)
504+
}
505+
415506
func TestReportCommand_GitRefUsesLeftRightMode(t *testing.T) {
416507
wd, err := os.Getwd()
417508
require.NoError(t, err)

cmd/summary_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,3 +957,22 @@ func TestNewSummaryCommand_NoComparableHistoryPrintsPriorVersionMessage(t *testi
957957

958958
assert.Contains(t, output, "The file has no prior version to compare against")
959959
}
960+
961+
func TestSummaryCommand_LimitOneStillComparesAgainstParent(t *testing.T) {
962+
repoDir := createGitSpecRepoForFile(t, "openapi.yaml")
963+
964+
cmd := testRootCmd(GetSummaryCommand(),
965+
"--no-logo", "--no-color",
966+
"--limit", "1",
967+
"--base", repoDir,
968+
repoDir, "openapi.yaml",
969+
)
970+
output := captureStdout(t, func() {
971+
require.NoError(t, cmd.Execute())
972+
})
973+
974+
assert.NotContains(t, output, noPriorVersionMessage)
975+
assert.NotContains(t, output, noChangesFoundMessage)
976+
assert.Contains(t, output, "Document Element")
977+
assert.Contains(t, output, "paths")
978+
}

cmd/test_helpers_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"os/exec"
1010
"path/filepath"
11+
"strings"
1112
"testing"
1213
"time"
1314

@@ -318,3 +319,13 @@ func runGitInDir(t *testing.T, dir string, args ...string) {
318319
out, err := cmd.CombinedOutput()
319320
require.NoError(t, err, "git %v failed: %s", args, string(out))
320321
}
322+
323+
func gitOutputInDir(t *testing.T, dir string, args ...string) string {
324+
t.Helper()
325+
326+
cmd := exec.Command("git", args...)
327+
cmd.Dir = dir
328+
out, err := cmd.CombinedOutput()
329+
require.NoError(t, err, "git %v failed: %s", args, string(out))
330+
return strings.TrimSpace(string(out))
331+
}

git/read_local.go

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,12 +280,30 @@ func buildCommitChangelogDetailed(commitHistory []*model.Commit,
280280
}
281281

282282
if previousComparable == nil {
283-
if c == len(commitHistory)-1 && commit.RepoDirectory != "" {
284-
model.SendProgressWarning("building models",
285-
fmt.Sprintf("Commit %s is the first version of '%s' — no prior version to compare against, skipping",
286-
commit.Hash, commit.FilePath), progressChan)
283+
baseline, baselineErr := resolvePriorComparableBaseline(commit, newDoc, revisionContext, docConfig)
284+
if baselineErr != nil {
285+
model.SendFatalError("building models", fmt.Sprintf("unable to configure original document '%s': %s", commit.FilePath, baselineErr.Error()), errorChan)
286+
changeErrors = append(changeErrors, baselineErr)
287+
return nil, changeErrors
288+
}
289+
if baseline == nil {
290+
if c == len(commitHistory)-1 && commit.RepoDirectory != "" {
291+
model.SendProgressWarning("building models",
292+
fmt.Sprintf("Commit %s is the first version of '%s' — no prior version to compare against, skipping",
293+
commit.Hash, commit.FilePath), progressChan)
294+
}
295+
cleaned = append(cleaned, commit)
296+
previousComparable = commit
297+
continue
298+
}
299+
300+
comparableCount++
301+
commit.OldData = baseline.Data
302+
commit.OldDocument = baseline.Document
303+
commit.Changes = baseline.Changes
304+
if commit.Changes != nil || opts.KeepComparable {
305+
cleaned = append(cleaned, commit)
287306
}
288-
cleaned = append(cleaned, commit)
289307
previousComparable = commit
290308
continue
291309
}
@@ -325,6 +343,47 @@ func buildCommitChangelogDetailed(commitHistory []*model.Commit,
325343
}, changeErrors
326344
}
327345

346+
type priorComparableBaseline struct {
347+
Data []byte
348+
Document libopenapi.Document
349+
Changes *whatChangedModel.DocumentChanges
350+
}
351+
352+
func resolvePriorComparableBaseline(commit *model.Commit, newDoc libopenapi.Document,
353+
revisionContext *RevisionDocumentContext, docConfig *datamodel.DocumentConfiguration,
354+
) (*priorComparableBaseline, error) {
355+
if commit == nil || commit.RepoDirectory == "" || newDoc == nil {
356+
return nil, nil
357+
}
358+
359+
for revision := fmt.Sprintf("%s~1", commit.Hash); ; revision = fmt.Sprintf("%s~1", revision) {
360+
oldBits, err := readFile(commit.RepoDirectory, revision, commit.FilePath)
361+
if err != nil {
362+
return nil, nil
363+
}
364+
365+
oldDocConfig, err := BuildRevisionDocumentConfiguration(revisionContext, revision, docConfig)
366+
if err != nil {
367+
return nil, err
368+
}
369+
oldDoc, err := libopenapi.NewDocumentWithConfiguration(oldBits, oldDocConfig)
370+
if err != nil {
371+
continue
372+
}
373+
374+
changes, err := libopenapi.CompareDocuments(oldDoc, newDoc)
375+
if err != nil {
376+
continue
377+
}
378+
379+
return &priorComparableBaseline{
380+
Data: oldBits,
381+
Document: oldDoc,
382+
Changes: changes,
383+
}, nil
384+
}
385+
}
386+
328387
func warnSkippedCommit(commit *model.Commit, reason string, progressChan chan *model.ProgressUpdate, skippedCommits *[]string, seen map[string]struct{}) {
329388
if commit == nil {
330389
return

0 commit comments

Comments
 (0)