From 91541a8324252a597836c7b95ebe711207522c14 Mon Sep 17 00:00:00 2001 From: RobiNino Date: Sun, 26 Jan 2025 21:19:58 +0200 Subject: [PATCH 1/2] Generify git log parsing --- artifactory/commands/buildinfo/addgit.go | 185 ++------------- artifactory/commands/buildinfo/addgit_test.go | 16 +- artifactory/utils/vcs.go | 218 ++++++++++++++++++ 3 files changed, 245 insertions(+), 174 deletions(-) create mode 100644 artifactory/utils/vcs.go diff --git a/artifactory/commands/buildinfo/addgit.go b/artifactory/commands/buildinfo/addgit.go index ae7aeef0b..1ed5a21c8 100644 --- a/artifactory/commands/buildinfo/addgit.go +++ b/artifactory/commands/buildinfo/addgit.go @@ -2,24 +2,18 @@ package buildinfo import ( "errors" - "io" - "os" - "os/exec" + "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "strconv" buildinfo "github.com/jfrog/build-info-go/entities" gofrogcmd "github.com/jfrog/gofrog/io" - "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/common/build" "github.com/jfrog/jfrog-cli-core/v2/common/project" utilsconfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-client-go/artifactory/services" - artclientutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/forPelevin/gomoji" "github.com/jfrog/jfrog-client-go/utils/errorutils" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/spf13/viper" ) @@ -29,6 +23,7 @@ const ( ConfigIssuesPrefix = "issues." ConfigParseValueError = "Failed parsing %s from configuration file: %s" MissingConfigurationError = "Configuration file must contain: %s" + gitParsingPrettyFormat = "format:%s" ) type BuildAddGitCommand struct { @@ -84,15 +79,9 @@ func (config *BuildAddGitCommand) Run() error { } // Find .git if it wasn't provided in the command. - if config.dotGitPath == "" { - var exists bool - config.dotGitPath, exists, err = fileutils.FindUpstream(".git", fileutils.Any) - if err != nil { - return err - } - if !exists { - return errorutils.CheckErrorf("Could not find .git") - } + config.dotGitPath, err = utils.GetDotGit(config.dotGitPath) + if err != nil { + return err } // Collect URL, branch and revision into GitManager. @@ -105,7 +94,7 @@ func (config *BuildAddGitCommand) Run() error { // Collect issues if required. var issues []buildinfo.AffectedIssue if config.configFilePath != "" { - issues, err = config.collectBuildIssues(gitManager.GetUrl()) + issues, err = config.collectBuildIssues() if err != nil { return err } @@ -163,77 +152,31 @@ func (config *BuildAddGitCommand) CommandName() string { return "rt_build_add_git" } -func (config *BuildAddGitCommand) collectBuildIssues(vcsUrl string) ([]buildinfo.AffectedIssue, error) { +func (config *BuildAddGitCommand) collectBuildIssues() ([]buildinfo.AffectedIssue, error) { log.Info("Collecting build issues from VCS...") - // Check that git exists in path. - _, err := exec.LookPath("git") - if err != nil { - return nil, errorutils.CheckError(err) - } - // Initialize issues-configuration. config.issuesConfig = new(IssuesConfiguration) // Create config's IssuesConfigurations from the provided spec file. - err = config.createIssuesConfigs() + err := config.createIssuesConfigs() if err != nil { return nil, err } - // Get latest build's VCS revision from Artifactory. - lastVcsRevision, err := config.getLatestVcsRevision(vcsUrl) + var foundIssues []buildinfo.AffectedIssue + logRegExp, err := createLogRegExpHandler(config.issuesConfig, &foundIssues) if err != nil { return nil, err } // Run issues collection. - return config.DoCollect(config.issuesConfig, lastVcsRevision) -} - -func (config *BuildAddGitCommand) DoCollect(issuesConfig *IssuesConfiguration, lastVcsRevision string) (foundIssues []buildinfo.AffectedIssue, err error) { - logRegExp, err := createLogRegExpHandler(issuesConfig, &foundIssues) - if err != nil { - return - } - - errRegExp, err := createErrRegExpHandler(lastVcsRevision) + gitDetails := utils.GitParsingDetails{DotGitPath: config.dotGitPath, LogLimit: config.issuesConfig.LogLimit, PrettyFormat: gitParsingPrettyFormat} + err = utils.ParseGitLogsFromLastBuild(config.issuesConfig.ServerDetails, config.buildConfiguration, gitDetails, logRegExp) if err != nil { - return - } - - // Get log with limit, starting from the latest commit. - logCmd := &LogCmd{logLimit: issuesConfig.LogLimit, lastVcsRevision: lastVcsRevision} - - // Change working dir to where .git is. - wd, err := os.Getwd() - if errorutils.CheckError(err) != nil { - return - } - defer func() { - err = errors.Join(err, errorutils.CheckError(os.Chdir(wd))) - }() - err = os.Chdir(config.dotGitPath) - if errorutils.CheckError(err) != nil { - return - } - - // Run git command. - _, _, exitOk, err := gofrogcmd.RunCmdWithOutputParser(logCmd, false, logRegExp, errRegExp) - if errorutils.CheckError(err) != nil { - var revisionRangeError RevisionRangeError - if errors.As(err, &revisionRangeError) { - // Revision not found in range. Ignore and don't collect new issues. - log.Info(err.Error()) - return []buildinfo.AffectedIssue{}, nil - } - return - } - if !exitOk { - // May happen when trying to run git log for non-existing revision. - err = errorutils.CheckErrorf("failed executing git log command") + return nil, err } - return + return foundIssues, nil } // Creates a regexp handler to parse and fetch issues from the output of the git log command. @@ -267,35 +210,6 @@ func createLogRegExpHandler(issuesConfig *IssuesConfiguration, foundIssues *[]bu return &logRegExp, nil } -// Error to be thrown when revision could not be found in the git revision range. -type RevisionRangeError struct { - ErrorMsg string -} - -func (err RevisionRangeError) Error() string { - return err.ErrorMsg -} - -// Creates a regexp handler to handle the event of revision missing in the git revision range. -func createErrRegExpHandler(lastVcsRevision string) (*gofrogcmd.CmdOutputPattern, error) { - // Create regex pattern. - invalidRangeExp, err := clientutils.GetRegExp(`fatal: Invalid revision range [a-fA-F0-9]+\.\.`) - if err != nil { - return nil, err - } - - // Create handler with exec function. - errRegExp := gofrogcmd.CmdOutputPattern{ - RegExp: invalidRangeExp, - ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) { - // Revision could not be found in the revision range, probably due to a squash / revert. Ignore and don't collect new issues. - errMsg := "Revision: '" + lastVcsRevision + "' that was fetched from latest build info does not exist in the git revision range. No new issues are added." - return "", RevisionRangeError{ErrorMsg: errMsg} - }, - } - return &errRegExp, nil -} - func (config *BuildAddGitCommand) createIssuesConfigs() (err error) { // Read file's data. err = config.issuesConfig.populateIssuesConfigsFromSpec(config.configFilePath) @@ -323,50 +237,6 @@ func (config *BuildAddGitCommand) createIssuesConfigs() (err error) { return } -func (config *BuildAddGitCommand) getLatestVcsRevision(vcsUrl string) (string, error) { - // Get latest build's build-info from Artifactory - buildInfo, err := config.getLatestBuildInfo(config.issuesConfig) - if err != nil { - return "", err - } - - // Get previous VCS Revision from BuildInfo. - lastVcsRevision := "" - for _, vcs := range buildInfo.VcsList { - if vcs.Url == vcsUrl { - lastVcsRevision = vcs.Revision - break - } - } - - return lastVcsRevision, nil -} - -// Returns build info, or empty build info struct if not found. -func (config *BuildAddGitCommand) getLatestBuildInfo(issuesConfig *IssuesConfiguration) (*buildinfo.BuildInfo, error) { - // Create services manager to get build-info from Artifactory. - sm, err := utils.CreateServiceManager(issuesConfig.ServerDetails, -1, 0, false) - if err != nil { - return nil, err - } - - // Get latest build-info from Artifactory. - buildName, err := config.buildConfiguration.GetBuildName() - if err != nil { - return nil, err - } - buildInfoParams := services.BuildInfoParams{BuildName: buildName, BuildNumber: artclientutils.LatestBuildNumberKey} - publishedBuildInfo, found, err := sm.GetBuildInfo(buildInfoParams) - if err != nil { - return nil, err - } - if !found { - return &buildinfo.BuildInfo{}, nil - } - - return &publishedBuildInfo.BuildInfo, nil -} - func (ic *IssuesConfiguration) populateIssuesConfigsFromSpec(configFilePath string) (err error) { var vConfig *viper.Viper vConfig, err = project.ReadConfigFile(configFilePath, project.YAML) @@ -461,30 +331,3 @@ type IssuesConfiguration struct { AggregationStatus string ServerID string } - -type LogCmd struct { - logLimit int - lastVcsRevision string -} - -func (logCmd *LogCmd) GetCmd() *exec.Cmd { - var cmd []string - cmd = append(cmd, "git") - cmd = append(cmd, "log", "--pretty=format:%s", "-"+strconv.Itoa(logCmd.logLimit)) - if logCmd.lastVcsRevision != "" { - cmd = append(cmd, logCmd.lastVcsRevision+"..") - } - return exec.Command(cmd[0], cmd[1:]...) -} - -func (logCmd *LogCmd) GetEnv() map[string]string { - return map[string]string{} -} - -func (logCmd *LogCmd) GetStdWriter() io.WriteCloser { - return nil -} - -func (logCmd *LogCmd) GetErrWriter() io.WriteCloser { - return nil -} diff --git a/artifactory/commands/buildinfo/addgit_test.go b/artifactory/commands/buildinfo/addgit_test.go index 8e6d3ccb1..43b17cf01 100644 --- a/artifactory/commands/buildinfo/addgit_test.go +++ b/artifactory/commands/buildinfo/addgit_test.go @@ -1,6 +1,7 @@ package buildinfo import ( + "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "os" "path/filepath" "strconv" @@ -255,8 +256,15 @@ func TestAddGitDoCollect(t *testing.T) { dotGitPath: dotGitPath, } + gitDetails := utils.GitParsingDetails{DotGitPath: config.dotGitPath, LogLimit: config.issuesConfig.LogLimit, PrettyFormat: gitParsingPrettyFormat} + var issues []buildinfo.AffectedIssue + logRegExp, err := createLogRegExpHandler(config.issuesConfig, &issues) + if err != nil { + t.Error(err) + } + // Collect issues - issues, err := config.DoCollect(config.issuesConfig, "") + err = utils.ParseGitLogsFromLastVcsRevision(gitDetails, logRegExp, "") if err != nil { t.Error(err) } @@ -276,7 +284,8 @@ func TestAddGitDoCollect(t *testing.T) { baseDir, dotGitPath = tests.PrepareDotGitDir(t, originalFolder, filepath.Join("..", "testdata")) // Collect issues - we pass a revision, so only 2 of the 4 existing issues should be collected - issues, err = config.DoCollect(config.issuesConfig, "6198a6294722fdc75a570aac505784d2ec0d1818") + issues = []buildinfo.AffectedIssue{} + err = utils.ParseGitLogsFromLastVcsRevision(gitDetails, logRegExp, "6198a6294722fdc75a570aac505784d2ec0d1818") if err != nil { t.Error(err) } @@ -286,7 +295,8 @@ func TestAddGitDoCollect(t *testing.T) { } // Test collection with a made up revision - the command should not throw an error, and 0 issues should be returned. - issues, err = config.DoCollect(config.issuesConfig, "abcdefABCDEF1234567890123456789012345678") + issues = []buildinfo.AffectedIssue{} + err = utils.ParseGitLogsFromLastVcsRevision(gitDetails, logRegExp, "abcdefABCDEF1234567890123456789012345678") assert.NoError(t, err) assert.Empty(t, issues) diff --git a/artifactory/utils/vcs.go b/artifactory/utils/vcs.go new file mode 100644 index 000000000..242fbcc1b --- /dev/null +++ b/artifactory/utils/vcs.go @@ -0,0 +1,218 @@ +package utils + +import ( + "errors" + buildinfo "github.com/jfrog/build-info-go/entities" + gofrogcmd "github.com/jfrog/gofrog/io" + "github.com/jfrog/jfrog-cli-core/v2/common/build" + utilsconfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-client-go/artifactory/services" + artclientutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "io" + "os" + "os/exec" + "strconv" +) + +func getLatestVcsRevision(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, vcsUrl string) (string, error) { + // Get latest build's build-info from Artifactory + buildInfo, err := getLatestBuildInfo(serverDetails, buildConfiguration) + if err != nil { + return "", err + } + + // Get previous VCS Revision from BuildInfo. + lastVcsRevision := "" + for _, vcs := range buildInfo.VcsList { + if vcs.Url == vcsUrl { + lastVcsRevision = vcs.Revision + break + } + } + + return lastVcsRevision, nil +} + +// Returns build info, or empty build info struct if not found. +func getLatestBuildInfo(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration) (*buildinfo.BuildInfo, error) { + // Create services manager to get build-info from Artifactory. + sm, err := CreateServiceManager(serverDetails, -1, 0, false) + if err != nil { + return nil, err + } + + // Get latest build-info from Artifactory. + buildName, err := buildConfiguration.GetBuildName() + if err != nil { + return nil, err + } + buildInfoParams := services.BuildInfoParams{BuildName: buildName, BuildNumber: artclientutils.LatestBuildNumberKey} + publishedBuildInfo, found, err := sm.GetBuildInfo(buildInfoParams) + if err != nil { + return nil, err + } + if !found { + return &buildinfo.BuildInfo{}, nil + } + + return &publishedBuildInfo.BuildInfo, nil +} + +type GitParsingDetails struct { + LogLimit int + PrettyFormat string + // Optional + DotGitPath string +} + +// Parses git logs from the last build's VCS revision. +// Calls git log with a custom format, and parses each line of the output with regexp. logRegExp is used to parse the log lines. +func ParseGitLogsFromLastBuild(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, gitDetails GitParsingDetails, logRegExp *gofrogcmd.CmdOutputPattern) error { + // Check that git exists in path. + _, err := exec.LookPath("git") + if err != nil { + return errorutils.CheckError(err) + } + + gitDetails.DotGitPath, err = GetDotGit(gitDetails.DotGitPath) + if err != nil { + return err + } + + vcsUrl, err := getVcsUrl(gitDetails.DotGitPath) + if err != nil { + return err + } + + // Get latest build's VCS revision from Artifactory. + lastVcsRevision, err := getLatestVcsRevision(serverDetails, buildConfiguration, vcsUrl) + if err != nil { + return err + } + return ParseGitLogsFromLastVcsRevision(gitDetails, logRegExp, lastVcsRevision) +} + +func ParseGitLogsFromLastVcsRevision(gitDetails GitParsingDetails, logRegExp *gofrogcmd.CmdOutputPattern, lastVcsRevision string) error { + errRegExp, err := createErrRegExpHandler(lastVcsRevision) + if err != nil { + return err + } + + // Get log with limit, starting from the latest commit. + logCmd := &LogCmd{logLimit: gitDetails.LogLimit, lastVcsRevision: lastVcsRevision, prettyFormat: gitDetails.PrettyFormat} + + // Change working dir to where .git is. + wd, err := os.Getwd() + if errorutils.CheckError(err) != nil { + return err + } + defer func() { + err = errors.Join(err, errorutils.CheckError(os.Chdir(wd))) + }() + err = os.Chdir(gitDetails.DotGitPath) + if errorutils.CheckError(err) != nil { + return err + } + + // Run git command. + _, _, exitOk, err := gofrogcmd.RunCmdWithOutputParser(logCmd, false, logRegExp, errRegExp) + if errorutils.CheckError(err) != nil { + var revisionRangeError RevisionRangeError + if errors.As(err, &revisionRangeError) { + // Revision not found in range. Ignore and return. + log.Info(err.Error()) + return nil + } + return err + } + if !exitOk { + // May happen when trying to run git log for non-existing revision. + err = errorutils.CheckErrorf("failed executing git log command") + } + return err +} + +// Creates a regexp handler to handle the event of revision missing in the git revision range. +func createErrRegExpHandler(lastVcsRevision string) (*gofrogcmd.CmdOutputPattern, error) { + // Create regex pattern. + invalidRangeExp, err := clientutils.GetRegExp(`fatal: Invalid revision range [a-fA-F0-9]+\.\.`) + if err != nil { + return nil, err + } + + // Create handler with exec function. + errRegExp := gofrogcmd.CmdOutputPattern{ + RegExp: invalidRangeExp, + ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) { + // Revision could not be found in the revision range, probably due to a squash / revert. Ignore and return. + errMsg := "Revision: '" + lastVcsRevision + "' that was fetched from latest build info does not exist in the git revision range." + return "", RevisionRangeError{ErrorMsg: errMsg} + }, + } + return &errRegExp, nil +} + +// Looks for the .git directory in the current directory and its parents. +func GetDotGit(providedDotGitPath string) (string, error) { + if providedDotGitPath != "" { + return providedDotGitPath, nil + } + dotGitPath, exists, err := fileutils.FindUpstream(".git", fileutils.Any) + if err != nil { + return "", err + } + if !exists { + return "", errorutils.CheckErrorf("Could not find .git") + } + return dotGitPath, nil + +} + +func getVcsUrl(dotGitPath string) (string, error) { + gitManager := clientutils.NewGitManager(dotGitPath) + if err := gitManager.ReadConfig(); err != nil { + return "", err + } + return gitManager.GetUrl(), nil +} + +type LogCmd struct { + logLimit int + lastVcsRevision string + prettyFormat string +} + +func (logCmd *LogCmd) GetCmd() *exec.Cmd { + var cmd []string + cmd = append(cmd, "git") + cmd = append(cmd, "log", "--pretty="+logCmd.prettyFormat, "-"+strconv.Itoa(logCmd.logLimit)) + if logCmd.lastVcsRevision != "" { + cmd = append(cmd, logCmd.lastVcsRevision+"..") + } + return exec.Command(cmd[0], cmd[1:]...) +} + +func (logCmd *LogCmd) GetEnv() map[string]string { + return map[string]string{} +} + +func (logCmd *LogCmd) GetStdWriter() io.WriteCloser { + return nil +} + +func (logCmd *LogCmd) GetErrWriter() io.WriteCloser { + return nil +} + +// Error to be thrown when revision could not be found in the git revision range. +type RevisionRangeError struct { + ErrorMsg string +} + +func (err RevisionRangeError) Error() string { + return err.ErrorMsg +} From f77413cdc97ae6ff57b94d23129db429d6d7d608 Mon Sep 17 00:00:00 2001 From: RobiNino Date: Tue, 28 Jan 2025 18:23:23 +0200 Subject: [PATCH 2/2] Add a plain go log api --- artifactory/commands/buildinfo/addgit.go | 2 +- artifactory/commands/buildinfo/addgit_test.go | 6 +- artifactory/utils/vcs.go | 80 +++++++++++++------ artifactory/utils/vcs_test.go | 24 ++++++ 4 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 artifactory/utils/vcs_test.go diff --git a/artifactory/commands/buildinfo/addgit.go b/artifactory/commands/buildinfo/addgit.go index 1ed5a21c8..16596e3a2 100644 --- a/artifactory/commands/buildinfo/addgit.go +++ b/artifactory/commands/buildinfo/addgit.go @@ -172,7 +172,7 @@ func (config *BuildAddGitCommand) collectBuildIssues() ([]buildinfo.AffectedIssu // Run issues collection. gitDetails := utils.GitParsingDetails{DotGitPath: config.dotGitPath, LogLimit: config.issuesConfig.LogLimit, PrettyFormat: gitParsingPrettyFormat} - err = utils.ParseGitLogsFromLastBuild(config.issuesConfig.ServerDetails, config.buildConfiguration, gitDetails, logRegExp) + err = utils.ParseGitLogFromLastBuild(config.issuesConfig.ServerDetails, config.buildConfiguration, gitDetails, logRegExp) if err != nil { return nil, err } diff --git a/artifactory/commands/buildinfo/addgit_test.go b/artifactory/commands/buildinfo/addgit_test.go index 43b17cf01..8ee463beb 100644 --- a/artifactory/commands/buildinfo/addgit_test.go +++ b/artifactory/commands/buildinfo/addgit_test.go @@ -264,7 +264,7 @@ func TestAddGitDoCollect(t *testing.T) { } // Collect issues - err = utils.ParseGitLogsFromLastVcsRevision(gitDetails, logRegExp, "") + err = utils.ParseGitLogFromLastVcsRevision(gitDetails, logRegExp, "") if err != nil { t.Error(err) } @@ -285,7 +285,7 @@ func TestAddGitDoCollect(t *testing.T) { // Collect issues - we pass a revision, so only 2 of the 4 existing issues should be collected issues = []buildinfo.AffectedIssue{} - err = utils.ParseGitLogsFromLastVcsRevision(gitDetails, logRegExp, "6198a6294722fdc75a570aac505784d2ec0d1818") + err = utils.ParseGitLogFromLastVcsRevision(gitDetails, logRegExp, "6198a6294722fdc75a570aac505784d2ec0d1818") if err != nil { t.Error(err) } @@ -296,7 +296,7 @@ func TestAddGitDoCollect(t *testing.T) { // Test collection with a made up revision - the command should not throw an error, and 0 issues should be returned. issues = []buildinfo.AffectedIssue{} - err = utils.ParseGitLogsFromLastVcsRevision(gitDetails, logRegExp, "abcdefABCDEF1234567890123456789012345678") + err = utils.ParseGitLogFromLastVcsRevision(gitDetails, logRegExp, "abcdefABCDEF1234567890123456789012345678") assert.NoError(t, err) assert.Empty(t, issues) diff --git a/artifactory/utils/vcs.go b/artifactory/utils/vcs.go index 242fbcc1b..9b75b8447 100644 --- a/artifactory/utils/vcs.go +++ b/artifactory/utils/vcs.go @@ -18,7 +18,7 @@ import ( "strconv" ) -func getLatestVcsRevision(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, vcsUrl string) (string, error) { +func doGetLatestVcsRevision(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, vcsUrl string) (string, error) { // Get latest build's build-info from Artifactory buildInfo, err := getLatestBuildInfo(serverDetails, buildConfiguration) if err != nil { @@ -69,52 +69,74 @@ type GitParsingDetails struct { DotGitPath string } -// Parses git logs from the last build's VCS revision. +// Parses git commits from the last build's VCS revision. // Calls git log with a custom format, and parses each line of the output with regexp. logRegExp is used to parse the log lines. -func ParseGitLogsFromLastBuild(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, gitDetails GitParsingDetails, logRegExp *gofrogcmd.CmdOutputPattern) error { - // Check that git exists in path. - _, err := exec.LookPath("git") +func ParseGitLogFromLastBuild(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, gitDetails GitParsingDetails, logRegExp *gofrogcmd.CmdOutputPattern) error { + lastVcsRevision, err := getLatestVcsRevision(serverDetails, buildConfiguration, &gitDetails) if err != nil { - return errorutils.CheckError(err) + return err } + return ParseGitLogFromLastVcsRevision(gitDetails, logRegExp, lastVcsRevision) +} - gitDetails.DotGitPath, err = GetDotGit(gitDetails.DotGitPath) +// Returns git commits from the last build's VCS revision. +// Calls git log with a custom format, and returns the output as is. +func GetPlainGitLogFromLastBuild(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, gitDetails GitParsingDetails) (string, error) { + lastVcsRevision, err := getLatestVcsRevision(serverDetails, buildConfiguration, &gitDetails) if err != nil { - return err + return "", err } + return getPlainGitLogFromLastVcsRevision(gitDetails, lastVcsRevision) +} - vcsUrl, err := getVcsUrl(gitDetails.DotGitPath) +// Get latest build's VCS revision from Artifactory. +func getLatestVcsRevision(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, gitDetails *GitParsingDetails) (string, error) { + // Check that git exists in path. + _, err := exec.LookPath("git") if err != nil { - return err + return "", errorutils.CheckError(err) } - // Get latest build's VCS revision from Artifactory. - lastVcsRevision, err := getLatestVcsRevision(serverDetails, buildConfiguration, vcsUrl) + (*gitDetails).DotGitPath, err = GetDotGit(gitDetails.DotGitPath) if err != nil { - return err + return "", err } - return ParseGitLogsFromLastVcsRevision(gitDetails, logRegExp, lastVcsRevision) -} -func ParseGitLogsFromLastVcsRevision(gitDetails GitParsingDetails, logRegExp *gofrogcmd.CmdOutputPattern, lastVcsRevision string) error { - errRegExp, err := createErrRegExpHandler(lastVcsRevision) + vcsUrl, err := getVcsUrl(gitDetails.DotGitPath) if err != nil { - return err + return "", err } + // Get latest build's VCS revision from Artifactory. + return doGetLatestVcsRevision(serverDetails, buildConfiguration, vcsUrl) +} + +func prepareGitLogCommand(gitDetails GitParsingDetails, lastVcsRevision string) (logCmd *LogCmd, cleanupFunc func() error, err error) { // Get log with limit, starting from the latest commit. - logCmd := &LogCmd{logLimit: gitDetails.LogLimit, lastVcsRevision: lastVcsRevision, prettyFormat: gitDetails.PrettyFormat} + logCmd = &LogCmd{logLimit: gitDetails.LogLimit, lastVcsRevision: lastVcsRevision, prettyFormat: gitDetails.PrettyFormat} // Change working dir to where .git is. wd, err := os.Getwd() if errorutils.CheckError(err) != nil { - return err + return + } + cleanupFunc = func() error { + return errors.Join(err, errorutils.CheckError(os.Chdir(wd))) } + err = errorutils.CheckError(os.Chdir(gitDetails.DotGitPath)) + return +} + +func ParseGitLogFromLastVcsRevision(gitDetails GitParsingDetails, logRegExp *gofrogcmd.CmdOutputPattern, lastVcsRevision string) (err error) { + logCmd, cleanupFunc, err := prepareGitLogCommand(gitDetails, lastVcsRevision) defer func() { - err = errors.Join(err, errorutils.CheckError(os.Chdir(wd))) + if cleanupFunc != nil { + err = errors.Join(err, cleanupFunc()) + } }() - err = os.Chdir(gitDetails.DotGitPath) - if errorutils.CheckError(err) != nil { + + errRegExp, err := createErrRegExpHandler(lastVcsRevision) + if err != nil { return err } @@ -136,6 +158,18 @@ func ParseGitLogsFromLastVcsRevision(gitDetails GitParsingDetails, logRegExp *go return err } +func getPlainGitLogFromLastVcsRevision(gitDetails GitParsingDetails, lastVcsRevision string) (gitLog string, err error) { + logCmd, cleanupFunc, err := prepareGitLogCommand(gitDetails, lastVcsRevision) + defer func() { + if cleanupFunc != nil { + err = errors.Join(err, cleanupFunc()) + } + }() + + // Run git command. + return gofrogcmd.RunCmdOutput(logCmd) +} + // Creates a regexp handler to handle the event of revision missing in the git revision range. func createErrRegExpHandler(lastVcsRevision string) (*gofrogcmd.CmdOutputPattern, error) { // Create regex pattern. diff --git a/artifactory/utils/vcs_test.go b/artifactory/utils/vcs_test.go new file mode 100644 index 000000000..3e4907e48 --- /dev/null +++ b/artifactory/utils/vcs_test.go @@ -0,0 +1,24 @@ +package utils + +import ( + "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/stretchr/testify/assert" + "path/filepath" + "testing" +) + +// TODO complete +func TestGetPlainGitLogFromLastVcsRevision(t *testing.T) { + // Create git folder with files + originalFolder := "git_issues_.git_suffix" + baseDir, dotGitPath := tests.PrepareDotGitDir(t, originalFolder, filepath.Join("..", "..", "commands", "testdata")) + gitDetails := GitParsingDetails{DotGitPath: dotGitPath, LogLimit: 3, PrettyFormat: "fuller"} + + // Collect issues + gitLog, err := getPlainGitLogFromLastVcsRevision(gitDetails, "") + assert.NoError(t, err) + assert.NotEmpty(t, gitLog) + + // Clean git path + tests.RenamePath(dotGitPath, filepath.Join(baseDir, originalFolder), t) +}