diff --git a/internal/cli/command_releases.go b/internal/cli/command_releases.go index 5f9efe9..bc4222b 100644 --- a/internal/cli/command_releases.go +++ b/internal/cli/command_releases.go @@ -145,13 +145,14 @@ func (c *CliContextWrapper) Option() (*core.Option, error) { } return &core.Option{ - Since: c.Since(), - Until: c.Until(), - IgnorePattern: ignorePattern, - FixCommitPattern: fixCommitPattern, - StartTimerFunc: c.StartTimer, - StopTimerFunc: c.StopTimer, - DebuglnFunc: c.Debugln, + Since: c.Since(), + Until: c.Until(), + IgnorePattern: ignorePattern, + IsLocalRepository: c.context.String("repository") == "", + FixCommitPattern: fixCommitPattern, + StartTimerFunc: c.StartTimer, + StopTimerFunc: c.StopTimer, + DebuglnFunc: c.Debugln, }, nil } diff --git a/internal/core/query_releases.go b/internal/core/query_releases.go index 192a461..22961f2 100644 --- a/internal/core/query_releases.go +++ b/internal/core/query_releases.go @@ -1,8 +1,12 @@ package core import ( + "bufio" + "bytes" "fmt" + "os/exec" "regexp" + "strconv" "strings" "time" @@ -21,12 +25,13 @@ type Option struct { // inclucive Since time.Time `json:"since"` // inclucive - Until time.Time `json:"until"` - IgnorePattern *regexp.Regexp `json:"-"` - FixCommitPattern *regexp.Regexp `json:"-"` - StartTimerFunc func(string) `json:"-"` - StopTimerFunc func(string) `json:"-"` - DebuglnFunc func(...any) `json:"-"` + Until time.Time `json:"until"` + IgnorePattern *regexp.Regexp `json:"-"` + FixCommitPattern *regexp.Regexp `json:"-"` + IsLocalRepository bool `json:"-"` + StartTimerFunc func(string) `json:"-"` + StopTimerFunc func(string) `json:"-"` + DebuglnFunc func(...any) `json:"-"` } func (r *Release) String() string { @@ -114,23 +119,11 @@ func QueryReleases(repository *git.Repository, option *Option) []*Release { nextSuccessReleaseIndex = len(releases) } - var lastCommit *object.Commit - var preReleaseCommit *object.Commit - if i < len(sources)-1 { - preReleaseCommit = sources[i+1].commit - } - restoresPreRelease := false - err := traverseCommits(repository, preReleaseCommit, source.commit, func(c *object.Commit) error { - if option.isFixedCommit(c.Message) { - restoresPreRelease = true - } - lastCommit = c - return nil - }) - isRestored = restoresPreRelease leadTimeForChanges := time.Duration(0) - if err == nil && lastCommit != nil { - leadTimeForChanges = source.commit.Committer.When.Sub(lastCommit.Committer.When) + if option != nil && option.IsLocalRepository { + isRestored, leadTimeForChanges = getIsRestoredAndLeadTimeForChangesByLocalGit(sources, i, option) + } else { + isRestored, leadTimeForChanges = getIsRestoredAndLeadTimeForChangesByGoGit(sources, i, option, repository) } option.StopTimer(timerKeyReleaseMetrics) @@ -145,3 +138,79 @@ func QueryReleases(repository *git.Repository, option *Option) []*Release { } return releases } + +// getIsRestoredAndLeadTimeForChangesByLocalGit gets isRestored and leadTimeForChanges by using local git command. +// Local git command is about 10 times faster than go-git. +func getIsRestoredAndLeadTimeForChangesByLocalGit( + sources []ReleaseSource, + i int, + option *Option, +) (isRestored bool, leadTimeForChanges time.Duration) { + source := sources[i] + since := "1900-01-01" + if i < len(sources)-1 { + preReleaseCommit := sources[i+1].commit + since = preReleaseCommit.Committer.When.Format("2006-01-02") + } + restoresPreRelease := false + output, cmdErr := exec.Command("git", "log", + "--since", since, + `--format="%ct %s"`, + "--date-order", + source.commit.Hash.String(), + ).Output() + lastLine := "" + if cmdErr == nil { + scanner := bufio.NewScanner(bytes.NewReader(output)) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + line := scanner.Text() + if option.isFixedCommit(line) { + restoresPreRelease = true + } + lastLine = line + } + } + isRestored = restoresPreRelease + leadTimeForChanges = time.Duration(0) + if cmdErr == nil && lastLine != "" { + unixtimeString := strings.Split(lastLine, " ")[0] + unixtimeInt, err := strconv.ParseInt(unixtimeString, 10, 64) + if err == nil { + lastCommitWhen := time.Unix(unixtimeInt, 0) + leadTimeForChanges = source.commit.Committer.When.Sub(lastCommitWhen) + } + } + return isRestored, leadTimeForChanges +} + +// getIsRestoredAndLeadTimeForChangesByGoGit gets isRestored and leadTimeForChanges by using go-git. +// go-git is slow but it can use in-memory repository. +// When repository is specified by url, repository is in-memory so that go-git is used. +func getIsRestoredAndLeadTimeForChangesByGoGit( + sources []ReleaseSource, + i int, + option *Option, + repository *git.Repository, +) (isRestored bool, leadTimeForChanges time.Duration) { + source := sources[i] + var lastCommit *object.Commit + var preReleaseCommit *object.Commit + if i < len(sources)-1 { + preReleaseCommit = sources[i+1].commit + } + restoresPreRelease := false + err := traverseCommits(repository, preReleaseCommit, source.commit, func(c *object.Commit) error { + if option.isFixedCommit(c.Message) { + restoresPreRelease = true + } + lastCommit = c + return nil + }) + isRestored = restoresPreRelease + leadTimeForChanges = time.Duration(0) + if err == nil && lastCommit != nil { + leadTimeForChanges = source.commit.Committer.When.Sub(lastCommit.Committer.When) + } + return isRestored, leadTimeForChanges +} diff --git a/internal/core/query_releases_test.go b/internal/core/query_releases_test.go index 6961062..d770a56 100644 --- a/internal/core/query_releases_test.go +++ b/internal/core/query_releases_test.go @@ -85,30 +85,7 @@ func TestQueryReleasesShouldReturnReleasesWithSpecifiedTimeRange(t *testing.T) { tag5_2_0 := &Release{Tag: "v5.2.0", Date: time.Date(2020, 10, 9, 11, 49, 30, 0, time.FixedZone("+0200", 2*60*60)), Result: ReleaseResult{IsSuccess: true}} expectedTags := []*Release{tag5_2_0, tag5_1_0, tag5_0_0} - if len(releases) != len(expectedTags) { - t.Errorf("releases does not have expected tag num. expected: %v. actual: %v", len(expectedTags), len(releases)) - return - } - - unmatchedRelease := make([]int, 0) - for i, actual := range releases { - expected := expectedTags[i] - if actual.Equal(expected) { - continue - } - unmatchedRelease = append(unmatchedRelease, i) - } - - if len(unmatchedRelease) == 0 { - return - } - - for i := range unmatchedRelease { - actual := releases[i] - expected := expectedTags[i] - t.Logf("releases[%d] = %s. expected: %v", i, actual, expected) - } - t.Errorf("releases does not have specified") + assertReleasesAreEqual(t, expectedTags, releases) } func TestQueryReleasesShouldHaveReleaseResult(t *testing.T) { @@ -145,30 +122,7 @@ func TestQueryReleasesShouldHaveReleaseResult(t *testing.T) { } expectedTags := []*Release{tag2_1_2, tag2_1_1, tag2_1_0} - if len(releases) != len(expectedTags) { - t.Errorf("releases does not have expected tag num. expected: %v. actual: %v", len(expectedTags), len(releases)) - return - } - - unmatchedRelease := make([]int, 0) - for i, actual := range releases { - expected := expectedTags[i] - if actual.Equal(expected) { - continue - } - unmatchedRelease = append(unmatchedRelease, i) - } - - if len(unmatchedRelease) == 0 { - return - } - - for i := range unmatchedRelease { - actual := releases[i] - expected := expectedTags[i] - t.Logf("releases[%d] = %s. expected: %v", i, actual, expected) - } - t.Errorf("releases does not have specified") + assertReleasesAreEqual(t, expectedTags, releases) } func TestQueryReleasesShouldReturnReleasesWithIgnorePattern(t *testing.T) { @@ -194,6 +148,17 @@ func TestQueryReleasesShouldReturnReleasesWithIgnorePattern(t *testing.T) { t.Errorf("releases does not have specified") } +func TestQueryReleasesShouldReturnSameReleasesRepositoryIsLocalOrNot(t *testing.T) { + fourKeysRepository, _ := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{}) + releasesOfLocalRepository := QueryReleases(fourKeysRepository, &Option{ + IsLocalRepository: true, + }) + releasesOfNotLocalRepository := QueryReleases(fourKeysRepository, &Option{ + IsLocalRepository: false, + }) + assertReleasesAreEqual(t, releasesOfLocalRepository, releasesOfNotLocalRepository) +} + func parseDurationOrZero(str string) time.Duration { d, err := time.ParseDuration(str) if err != nil { @@ -209,3 +174,30 @@ func parseDurationOrNil(str string) *time.Duration { } return &d } + +func assertReleasesAreEqual(t *testing.T, releasesExpected []*Release, releasesActual []*Release) { + if len(releasesActual) != len(releasesExpected) { + t.Errorf("releases does not have same length. expected: %v. actual: %v", len(releasesExpected), len(releasesActual)) + return + } + + unmatchedRelease := make([]int, 0) + for i, actual := range releasesActual { + expected := releasesExpected[i] + if actual.Equal(expected) { + continue + } + unmatchedRelease = append(unmatchedRelease, i) + } + + if len(unmatchedRelease) == 0 { + return + } + + for i := range unmatchedRelease { + actual := releasesActual[i] + expected := releasesExpected[i] + t.Logf("releases[%d] = %s. expected: %v", i, actual, expected) + } + t.Errorf("releases does not have specified") +}