diff --git a/artifactory/commands/buildinfo/addgit.go b/artifactory/commands/buildinfo/addgit.go index ae7aeef0..4c06cda5 100644 --- a/artifactory/commands/buildinfo/addgit.go +++ b/artifactory/commands/buildinfo/addgit.go @@ -2,26 +2,18 @@ package buildinfo import ( "errors" - "io" - "os" - "os/exec" - "strconv" - + "github.com/forPelevin/gomoji" 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-artifactory/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" + "strconv" ) const ( @@ -29,6 +21,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 +77,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 +92,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 +150,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) + gitDetails := utils.GitLogDetails{DotGitPath: config.dotGitPath, LogLimit: config.issuesConfig.LogLimit, PrettyFormat: gitParsingPrettyFormat} + err = utils.ParseGitLogFromLastBuild(config.issuesConfig.ServerDetails, config.buildConfiguration, gitDetails, logRegExp) if err != nil { - return - } - - errRegExp, err := createErrRegExpHandler(lastVcsRevision) - 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 +208,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 +235,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 +329,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 8e6d3ccb..d1792acf 100644 --- a/artifactory/commands/buildinfo/addgit_test.go +++ b/artifactory/commands/buildinfo/addgit_test.go @@ -1,22 +1,21 @@ package buildinfo import ( + buildinfo "github.com/jfrog/build-info-go/entities" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils" + "github.com/jfrog/jfrog-cli-core/v2/common/build" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/utils/log" + "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + testsutils "github.com/jfrog/jfrog-client-go/utils/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "os" "path/filepath" "strconv" "strings" "testing" "time" - - buildinfo "github.com/jfrog/build-info-go/entities" - testsutils "github.com/jfrog/jfrog-client-go/utils/tests" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/jfrog/jfrog-cli-core/v2/common/build" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/utils/log" - "github.com/jfrog/jfrog-cli-core/v2/utils/tests" ) const ( @@ -255,13 +254,20 @@ func TestAddGitDoCollect(t *testing.T) { dotGitPath: dotGitPath, } + gitDetails := utils.GitLogDetails{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.ParseGitLogFromLastVcsRevision(gitDetails, logRegExp, "") if err != nil { t.Error(err) } if len(issues) != 2 { - // Error - should be empty + // Error - should find 2 issues t.Errorf("Issues list expected to have 2 issues, instead found %d issues: %v", len(issues), issues) } @@ -276,7 +282,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.ParseGitLogFromLastVcsRevision(gitDetails, logRegExp, "6198a6294722fdc75a570aac505784d2ec0d1818") if err != nil { t.Error(err) } @@ -286,7 +293,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.ParseGitLogFromLastVcsRevision(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 00000000..02ba0281 --- /dev/null +++ b/artifactory/utils/vcs.go @@ -0,0 +1,341 @@ +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/artifactory/utils" + "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" + "strings" +) + +const ( + revisionRangeErrPrefix = "fatal: Invalid revision range" +) + +// Gets the vcs revision from the latest build in Artifactory. +func getLatestVcsRevision(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, vcsUrl string) (string, error) { + buildInfo, err := getLatestBuildInfo(serverDetails, buildConfiguration) + if err != nil { + return "", err + } + + return getMatchingRevisionFromBuild(buildInfo, vcsUrl), nil +} + +// Gets the vcs revision from the build in position "previousBuildPos" in Artifactory. previousBuildPos = 0 is the latest build. +// previousBuildPos must be 0 or larger. +func getVcsFromPreviousBuild(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, vcsUrl string, previousBuildPos int) (string, error) { + buildInfo, err := getPreviousBuild(serverDetails, buildConfiguration, previousBuildPos) + if err != nil { + return "", err + } + + return getMatchingRevisionFromBuild(buildInfo, vcsUrl), nil +} + +// Returns the vcs revision that matches th provided vcs url. +func getMatchingRevisionFromBuild(buildInfo *buildinfo.BuildInfo, vcsUrl string) string { + lastVcsRevision := "" + for _, vcs := range buildInfo.VcsList { + if vcs.Url == vcsUrl { + lastVcsRevision = vcs.Revision + break + } + } + return lastVcsRevision +} + +// 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 := utils.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 +} + +// Returns the previous build in order provided by previousBuildPos. For previousBuildPos 0 the latest build is returned. +// If previousBuildPos is not 0 or above, a general error will be returned. +// If the build does not exist, or there are less previous build runs than requested, an empty build will be returned. +func getPreviousBuild(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, previousBuildPos int) (*buildinfo.BuildInfo, error) { + if previousBuildPos < 0 { + return nil, errorutils.CheckErrorf("invalid input for previous build position. Input must be a non negative number") + } + + // Create services manager to get build-info from Artifactory. + sm, err := utils.CreateServiceManager(serverDetails, -1, 0, false) + if err != nil { + return nil, err + } + + buildName, err := buildConfiguration.GetBuildName() + if err != nil { + return nil, err + } + projectKey := buildConfiguration.GetProject() + buildInfoParams := services.BuildInfoParams{BuildName: buildName, ProjectKey: projectKey} + + runs, found, err := sm.GetBuildRuns(buildInfoParams) + if err != nil { + return nil, err + } + // Return if build not found, or not enough build runs were returned to match the requested previous position. + if !found || len(runs.BuildsNumbers)-1 < previousBuildPos { + return &buildinfo.BuildInfo{}, nil + } + + // Get matching build number. Build numbers are returned sorted, from latest to oldest. + run := runs.BuildsNumbers[previousBuildPos] + buildInfoParams.BuildNumber = strings.TrimPrefix(run.Uri, "/") + + publishedBuildInfo, found, err := sm.GetBuildInfo(buildInfoParams) + if err != nil { + return nil, err + } + // If build was deleted between requests. + if !found { + return &buildinfo.BuildInfo{}, nil + } + + return &publishedBuildInfo.BuildInfo, nil +} + +type GitLogDetails struct { + LogLimit int + PrettyFormat string + // Optional + DotGitPath string +} + +// 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 ParseGitLogFromLastBuild(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, gitDetails GitLogDetails, logRegExp *gofrogcmd.CmdOutputPattern) error { + vcsUrl, err := validateGitAndGetVcsUrl(&gitDetails) + 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 ParseGitLogFromLastVcsRevision(gitDetails, logRegExp, lastVcsRevision) +} + +// Returns the git log output for the VCS revision for the previous build in position previousBuildPos. +// For previousBuildPos 0 the latest build is returned, for an input 1 the latest -1 is returned, etc. previousBuildPos must be 0 or above. +// Calls git log with a custom format, and returns the output as is. +// Return RevisionRangeError if revision isn't found (due to git history modification). +func GetPlainGitLogFromPreviousBuild(serverDetails *utilsconfig.ServerDetails, buildConfiguration *build.BuildConfiguration, gitDetails GitLogDetails, previousBuildPos int) (string, error) { + vcsUrl, err := validateGitAndGetVcsUrl(&gitDetails) + if err != nil { + return "", err + } + + lastVcsRevision, err := getVcsFromPreviousBuild(serverDetails, buildConfiguration, vcsUrl, previousBuildPos) + if err != nil { + return "", err + } + + return getPlainGitLogFromLastVcsRevision(gitDetails, lastVcsRevision) +} + +// Validates git is in path, and returns the VCS url by searching in the .git directory. +func validateGitAndGetVcsUrl(gitDetails *GitLogDetails) (string, 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 + } + + return getVcsUrl(gitDetails.DotGitPath) +} + +func prepareGitLogCommand(gitDetails GitLogDetails, 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} + + // Change working dir to where .git is. + wd, err := os.Getwd() + if errorutils.CheckError(err) != nil { + return + } + cleanupFunc = func() error { + return errors.Join(err, errorutils.CheckError(os.Chdir(wd))) + } + err = errorutils.CheckError(os.Chdir(gitDetails.DotGitPath)) + return +} + +// Parses git log line by line, using the parser provided in logRegExp. +// Git log is parsed from lastVcsRevision to HEAD. +func ParseGitLogFromLastVcsRevision(gitDetails GitLogDetails, logRegExp *gofrogcmd.CmdOutputPattern, lastVcsRevision string) (err error) { + logCmd, cleanupFunc, err := prepareGitLogCommand(gitDetails, lastVcsRevision) + defer func() { + if cleanupFunc != nil { + err = errors.Join(err, cleanupFunc()) + } + }() + + errRegExp, err := createErrRegExpHandler(lastVcsRevision) + if 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 +} + +// Runs git log from lastVcsRevision to HEAD, using the provided format, and returns the output as is. +// Return RevisionRangeError if revision isn't found. +func getPlainGitLogFromLastVcsRevision(gitDetails GitLogDetails, lastVcsRevision string) (gitLog string, err error) { + logCmd, cleanupFunc, err := prepareGitLogCommand(gitDetails, lastVcsRevision) + defer func() { + if cleanupFunc != nil { + err = errors.Join(err, cleanupFunc()) + } + }() + + stdOut, errorOut, _, err := gofrogcmd.RunCmdWithOutputParser(logCmd, false) + if errorutils.CheckError(err) != nil { + if strings.HasPrefix(strings.TrimSpace(errorOut), revisionRangeErrPrefix) { + return "", getRevisionRangeError(lastVcsRevision) + } + return "", err + } + return stdOut, nil +} + +// 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(revisionRangeErrPrefix + ` [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) { + return "", getRevisionRangeError(lastVcsRevision) + }, + } + return &errRegExp, nil +} + +func getRevisionRangeError(lastVcsRevision 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} +} + +// 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 + +} + +// Gets vcs url from the .git directory. +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 +} diff --git a/artifactory/utils/vcs_test.go b/artifactory/utils/vcs_test.go new file mode 100644 index 00000000..0fd943a0 --- /dev/null +++ b/artifactory/utils/vcs_test.go @@ -0,0 +1,33 @@ +package utils + +import ( + "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/stretchr/testify/assert" + "path/filepath" + "strings" + "testing" +) + +func TestGetPlainGitLogFromLastVcsRevision(t *testing.T) { + // Create git folder with files + originalFolder := "git_issues2_.git_suffix" + baseDir, dotGitPath := tests.PrepareDotGitDir(t, originalFolder, filepath.Join("..", "commands", "testdata")) + defer tests.RenamePath(dotGitPath, filepath.Join(baseDir, originalFolder), t) + + gitDetails := GitLogDetails{DotGitPath: dotGitPath, LogLimit: 3, PrettyFormat: "oneline"} + + // Expect all commits without providing a revision. + runGitLogAndCountCommits(t, gitDetails, "", 3) + // Expect only commits in range when providing a revision. + runGitLogAndCountCommits(t, gitDetails, "6198a6294722fdc75a570aac505784d2ec0d1818", 2) + // Expect an RevisionRangeError error when revision doesn't exist. + _, err := getPlainGitLogFromLastVcsRevision(gitDetails, "1111111111111111111111111111111111111111") + assert.ErrorAs(t, err, &RevisionRangeError{}) +} + +func runGitLogAndCountCommits(t *testing.T, gitDetails GitLogDetails, vcsRevision string, expectedCommits int) { + gitLog, err := getPlainGitLogFromLastVcsRevision(gitDetails, vcsRevision) + assert.NoError(t, err) + commits := strings.Split(strings.TrimSpace(gitLog), "\n") + assert.Len(t, commits, expectedCommits) +} diff --git a/go.mod b/go.mod index ec213ef2..e386e823 100644 --- a/go.mod +++ b/go.mod @@ -120,12 +120,8 @@ require ( sigs.k8s.io/yaml v1.4.0 // indirect ) -//replace github.com/jfrog/jfrog-cli-core/v2 => github.com/oshratZairi/jfrog-cli-core/v2 v2.56.9-0.20241127142944-b39d0cc8f1c1 -//replace github.com/jfrog/jfrog-cli-core/v2 => github.com/oshratZairi/jfrog-cli-core/v2 v2.31.1-0.20241211104546-3e12a85de116 - replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20250130104846-27e495de291e -replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20250126110945-81abbdde452f +replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20250203113746-2ba71dd0e694 -//replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20240811150357-12a9330a2d67 -//replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20240811142930-ab9715567376 +replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20250203111011-4ff16d3d42be diff --git a/go.sum b/go.sum index 5f1bc54c..ac43638b 100644 --- a/go.sum +++ b/go.sum @@ -107,14 +107,14 @@ github.com/jedib0t/go-pretty/v6 v6.6.5 h1:9PgMJOVBedpgYLI56jQRJYqngxYAAzfEUua+3N github.com/jedib0t/go-pretty/v6 v6.6.5/go.mod h1:Uq/HrbhuFty5WSVNfjpQQe47x16RwVGXIveNGEyGtHs= github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI= github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw= -github.com/jfrog/build-info-go v1.10.8 h1:8D4wtvKzLS1hzfDWtfH4OliZLtLCgL62tXCnGWDXuac= -github.com/jfrog/build-info-go v1.10.8/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE= +github.com/jfrog/build-info-go v1.8.9-0.20250203111011-4ff16d3d42be h1:sCn4prpANCdmYBAUEBed10qGJUjY8XUElfEM3Xi2OqE= +github.com/jfrog/build-info-go v1.8.9-0.20250203111011-4ff16d3d42be/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20250130104846-27e495de291e h1:cJJxXI45QLJsaCr5ChOTToCJpLoRTBGlXu/Z6DZB9jk= github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20250130104846-27e495de291e/go.mod h1:3vP0hv13zJYvhXlgKIXmWSN8ADvGUQgxVVCqcO8hOeM= -github.com/jfrog/jfrog-client-go v1.28.1-0.20250126110945-81abbdde452f h1:2IIy3XfvmEp5zJgakKZiyKGGeVyDsouwYmtD+4QiVd4= -github.com/jfrog/jfrog-client-go v1.28.1-0.20250126110945-81abbdde452f/go.mod h1:ohIfKpMBCQsE9kunrKQ1wvoExpqsPLaluRFO186B5EM= +github.com/jfrog/jfrog-client-go v1.28.1-0.20250203113746-2ba71dd0e694 h1:5IkN0F1G3dYcqjd9QEsEAJUzbLbyIh+3/uyisNUfGd8= +github.com/jfrog/jfrog-client-go v1.28.1-0.20250203113746-2ba71dd0e694/go.mod h1:cMSS8x1XZGT+ZktqMpr4wxwq5i7tgLV50apExryI5dk= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=