github.com/jfrog/jfrog-cli-core@v1.12.1/artifactory/commands/buildinfo/addgit.go (about)

     1  package buildinfo
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	gofrogcmd "github.com/jfrog/gofrog/io"
     7  	"github.com/jfrog/jfrog-cli-core/artifactory/utils"
     8  	utilsconfig "github.com/jfrog/jfrog-cli-core/utils/config"
     9  	"github.com/jfrog/jfrog-client-go/artifactory/buildinfo"
    10  	"github.com/jfrog/jfrog-client-go/artifactory/services"
    11  	clientutils "github.com/jfrog/jfrog-client-go/utils"
    12  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    13  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    14  	"github.com/jfrog/jfrog-client-go/utils/log"
    15  	"github.com/spf13/viper"
    16  	"io"
    17  	"os"
    18  	"os/exec"
    19  	"strconv"
    20  )
    21  
    22  const (
    23  	GitLogLimit               = 100
    24  	ConfigIssuesPrefix        = "issues."
    25  	ConfigParseValueError     = "Failed parsing %s from configuration file: %s"
    26  	MissingConfigurationError = "Configuration file must contain: %s"
    27  )
    28  
    29  type BuildAddGitCommand struct {
    30  	buildConfiguration *utils.BuildConfiguration
    31  	dotGitPath         string
    32  	configFilePath     string
    33  	serverId           string
    34  	issuesConfig       *IssuesConfiguration
    35  }
    36  
    37  func NewBuildAddGitCommand() *BuildAddGitCommand {
    38  	return &BuildAddGitCommand{}
    39  }
    40  
    41  func (config *BuildAddGitCommand) SetIssuesConfig(issuesConfig *IssuesConfiguration) *BuildAddGitCommand {
    42  	config.issuesConfig = issuesConfig
    43  	return config
    44  }
    45  
    46  func (config *BuildAddGitCommand) SetConfigFilePath(configFilePath string) *BuildAddGitCommand {
    47  	config.configFilePath = configFilePath
    48  	return config
    49  }
    50  
    51  func (config *BuildAddGitCommand) SetDotGitPath(dotGitPath string) *BuildAddGitCommand {
    52  	config.dotGitPath = dotGitPath
    53  	return config
    54  }
    55  
    56  func (config *BuildAddGitCommand) SetBuildConfiguration(buildConfiguration *utils.BuildConfiguration) *BuildAddGitCommand {
    57  	config.buildConfiguration = buildConfiguration
    58  	return config
    59  }
    60  
    61  func (config *BuildAddGitCommand) SetServerId(serverId string) *BuildAddGitCommand {
    62  	config.serverId = serverId
    63  	return config
    64  }
    65  
    66  func (config *BuildAddGitCommand) Run() error {
    67  	log.Info("Reading the git branch, revision and remote URL and adding them to the build-info.")
    68  	err := utils.SaveBuildGeneralDetails(config.buildConfiguration.BuildName, config.buildConfiguration.BuildNumber, config.buildConfiguration.Project)
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	// Find .git if it wasn't provided in the command.
    74  	if config.dotGitPath == "" {
    75  		var exists bool
    76  		config.dotGitPath, exists, err = fileutils.FindUpstream(".git", fileutils.Any)
    77  		if err != nil {
    78  			return err
    79  		}
    80  		if !exists {
    81  			return errorutils.CheckError(errors.New("Could not find .git"))
    82  		}
    83  	}
    84  
    85  	// Collect URL, branch and revision into GitManager.
    86  	gitManager := clientutils.NewGitManager(config.dotGitPath)
    87  	err = gitManager.ReadConfig()
    88  	if err != nil {
    89  		return err
    90  	}
    91  
    92  	// Collect issues if required.
    93  	var issues []buildinfo.AffectedIssue
    94  	if config.configFilePath != "" {
    95  		issues, err = config.collectBuildIssues(gitManager.GetUrl())
    96  		if err != nil {
    97  			return err
    98  		}
    99  	}
   100  
   101  	// Populate partials with VCS info.
   102  	populateFunc := func(partial *buildinfo.Partial) {
   103  		partial.VcsList = append(partial.VcsList, buildinfo.Vcs{
   104  			Url:      gitManager.GetUrl(),
   105  			Revision: gitManager.GetRevision(),
   106  			Branch:   gitManager.GetBranch(),
   107  			Message:  gitManager.GetMessage(),
   108  		})
   109  
   110  		if config.configFilePath != "" {
   111  			partial.Issues = &buildinfo.Issues{
   112  				Tracker:                &buildinfo.Tracker{Name: config.issuesConfig.TrackerName, Version: ""},
   113  				AggregateBuildIssues:   config.issuesConfig.Aggregate,
   114  				AggregationBuildStatus: config.issuesConfig.AggregationStatus,
   115  				AffectedIssues:         issues,
   116  			}
   117  		}
   118  	}
   119  	err = utils.SavePartialBuildInfo(config.buildConfiguration.BuildName, config.buildConfiguration.BuildNumber, config.buildConfiguration.Project, populateFunc)
   120  	if err != nil {
   121  		return err
   122  	}
   123  
   124  	// Done.
   125  	log.Debug("Collected VCS details for", config.buildConfiguration.BuildName+"/"+config.buildConfiguration.BuildNumber+".")
   126  	return nil
   127  }
   128  
   129  // Priorities for selecting server:
   130  // 1. 'server-id' flag.
   131  // 2. 'serverID' in config file.
   132  // 3. Default server.
   133  func (config *BuildAddGitCommand) ServerDetails() (*utilsconfig.ServerDetails, error) {
   134  	var serverId string
   135  	if config.serverId != "" {
   136  		serverId = config.serverId
   137  	} else if config.configFilePath != "" {
   138  		// Get the server ID from the conf file.
   139  		var vConfig *viper.Viper
   140  		vConfig, err := utils.ReadConfigFile(config.configFilePath, utils.YAML)
   141  		if err != nil {
   142  			return nil, err
   143  		}
   144  		serverId = vConfig.GetString(ConfigIssuesPrefix + "serverID")
   145  	}
   146  	return utilsconfig.GetSpecificConfig(serverId, true, false)
   147  }
   148  
   149  func (config *BuildAddGitCommand) CommandName() string {
   150  	return "rt_build_add_git"
   151  }
   152  
   153  func (config *BuildAddGitCommand) collectBuildIssues(vcsUrl string) ([]buildinfo.AffectedIssue, error) {
   154  	log.Info("Collecting build issues from VCS...")
   155  
   156  	// Check that git exists in path.
   157  	_, err := exec.LookPath("git")
   158  	if err != nil {
   159  		return nil, errorutils.CheckError(err)
   160  	}
   161  
   162  	// Initialize issues-configuration.
   163  	config.issuesConfig = new(IssuesConfiguration)
   164  
   165  	// Create config's IssuesConfigurations from the provided spec file.
   166  	err = config.createIssuesConfigs()
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	// Get latest build's VCS revision from Artifactory.
   172  	lastVcsRevision, err := config.getLatestVcsRevision(vcsUrl)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	// Run issues collection.
   178  	return config.DoCollect(config.issuesConfig, lastVcsRevision)
   179  }
   180  
   181  func (config *BuildAddGitCommand) DoCollect(issuesConfig *IssuesConfiguration, lastVcsRevision string) ([]buildinfo.AffectedIssue, error) {
   182  	var foundIssues []buildinfo.AffectedIssue
   183  	logRegExp, err := createLogRegExpHandler(issuesConfig, &foundIssues)
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  
   188  	errRegExp, err := createErrRegExpHandler(lastVcsRevision)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	// Get log with limit, starting from the latest commit.
   194  	logCmd := &LogCmd{logLimit: issuesConfig.LogLimit, lastVcsRevision: lastVcsRevision}
   195  
   196  	// Change working dir to where .git is.
   197  	wd, err := os.Getwd()
   198  	if errorutils.CheckError(err) != nil {
   199  		return nil, err
   200  	}
   201  	defer os.Chdir(wd)
   202  	err = os.Chdir(config.dotGitPath)
   203  	if errorutils.CheckError(err) != nil {
   204  		return nil, err
   205  	}
   206  
   207  	// Run git command.
   208  	_, _, exitOk, err := gofrogcmd.RunCmdWithOutputParser(logCmd, false, logRegExp, errRegExp)
   209  	if err != nil {
   210  		if _, ok := err.(RevisionRangeError); ok {
   211  			// Revision not found in range. Ignore and don't collect new issues.
   212  			log.Info(err.Error())
   213  			return []buildinfo.AffectedIssue{}, nil
   214  		}
   215  		return nil, errorutils.CheckError(err)
   216  	}
   217  	if !exitOk {
   218  		// May happen when trying to run git log for non-existing revision.
   219  		return nil, errorutils.CheckError(errors.New("failed executing git log command"))
   220  	}
   221  
   222  	// Return found issues.
   223  	return foundIssues, nil
   224  }
   225  
   226  // Creates a regexp handler to parse and fetch issues from the the output of the git log command.
   227  func createLogRegExpHandler(issuesConfig *IssuesConfiguration, foundIssues *[]buildinfo.AffectedIssue) (*gofrogcmd.CmdOutputPattern, error) {
   228  	// Create regex pattern.
   229  	issueRegexp, err := clientutils.GetRegExp(issuesConfig.Regexp)
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  
   234  	// Create handler with exec function.
   235  	logRegExp := gofrogcmd.CmdOutputPattern{
   236  		RegExp: issueRegexp,
   237  		ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) {
   238  			// Reached here - means no error occurred.
   239  
   240  			// Check for out of bound results.
   241  			if len(pattern.MatchedResults)-1 < issuesConfig.KeyGroupIndex || len(pattern.MatchedResults)-1 < issuesConfig.SummaryGroupIndex {
   242  				return "", errors.New("unexpected result while parsing issues from git log. Make sure that the regular expression used to find issues, includes two capturing groups, for the issue ID and the summary")
   243  			}
   244  			// Create found Affected Issue.
   245  			foundIssue := buildinfo.AffectedIssue{Key: pattern.MatchedResults[issuesConfig.KeyGroupIndex], Summary: pattern.MatchedResults[issuesConfig.SummaryGroupIndex], Aggregated: false}
   246  			if issuesConfig.TrackerUrl != "" {
   247  				foundIssue.Url = issuesConfig.TrackerUrl + pattern.MatchedResults[issuesConfig.KeyGroupIndex]
   248  			}
   249  			*foundIssues = append(*foundIssues, foundIssue)
   250  			log.Debug("Found issue: " + pattern.MatchedResults[issuesConfig.KeyGroupIndex])
   251  			return "", nil
   252  		},
   253  	}
   254  	return &logRegExp, nil
   255  }
   256  
   257  // Error to be thrown when revision could not be found in the git revision range.
   258  type RevisionRangeError struct {
   259  	ErrorMsg string
   260  }
   261  
   262  func (err RevisionRangeError) Error() string {
   263  	return err.ErrorMsg
   264  }
   265  
   266  // Creates a regexp handler to handle the event of revision missing in the git revision range.
   267  func createErrRegExpHandler(lastVcsRevision string) (*gofrogcmd.CmdOutputPattern, error) {
   268  	// Create regex pattern.
   269  	invalidRangeExp, err := clientutils.GetRegExp(`fatal: Invalid revision range [a-fA-F0-9]+\.\.`)
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  
   274  	// Create handler with exec function.
   275  	errRegExp := gofrogcmd.CmdOutputPattern{
   276  		RegExp: invalidRangeExp,
   277  		ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) {
   278  			// Revision could not be found in the revision range, probably due to a squash / revert. Ignore and don't collect new issues.
   279  			errMsg := "Revision: '" + lastVcsRevision + "' that was fetched from latest build info does not exist in the git revision range. No new issues are added."
   280  			return "", RevisionRangeError{ErrorMsg: errMsg}
   281  		},
   282  	}
   283  	return &errRegExp, nil
   284  }
   285  
   286  func (config *BuildAddGitCommand) createIssuesConfigs() (err error) {
   287  	// Read file's data.
   288  	err = config.issuesConfig.populateIssuesConfigsFromSpec(config.configFilePath)
   289  	if err != nil {
   290  		return
   291  	}
   292  
   293  	// Use 'server-id' flag if provided.
   294  	if config.serverId != "" {
   295  		config.issuesConfig.ServerID = config.serverId
   296  	}
   297  
   298  	// Build ServerDetails from provided serverID.
   299  	err = config.issuesConfig.setServerDetails()
   300  	if err != nil {
   301  		return
   302  	}
   303  
   304  	// Add '/' suffix to URL if required.
   305  	if config.issuesConfig.TrackerUrl != "" {
   306  		// Url should end with '/'
   307  		config.issuesConfig.TrackerUrl = clientutils.AddTrailingSlashIfNeeded(config.issuesConfig.TrackerUrl)
   308  	}
   309  
   310  	return
   311  }
   312  
   313  func (config *BuildAddGitCommand) getLatestVcsRevision(vcsUrl string) (string, error) {
   314  	// Get latest build's build-info from Artifactory
   315  	buildInfo, err := config.getLatestBuildInfo(config.issuesConfig)
   316  	if err != nil {
   317  		return "", err
   318  	}
   319  
   320  	// Get previous VCS Revision from BuildInfo.
   321  	lastVcsRevision := ""
   322  	for _, vcs := range buildInfo.VcsList {
   323  		if vcs.Url == vcsUrl {
   324  			lastVcsRevision = vcs.Revision
   325  			break
   326  		}
   327  	}
   328  
   329  	return lastVcsRevision, nil
   330  }
   331  
   332  // Returns build info, or empty build info struct if not found.
   333  func (config *BuildAddGitCommand) getLatestBuildInfo(issuesConfig *IssuesConfiguration) (*buildinfo.BuildInfo, error) {
   334  	// Create services manager to get build-info from Artifactory.
   335  	sm, err := utils.CreateServiceManager(issuesConfig.ServerDetails, -1, false)
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  
   340  	// Get latest build-info from Artifactory.
   341  	buildInfoParams := services.BuildInfoParams{BuildName: config.buildConfiguration.BuildName, BuildNumber: "LATEST"}
   342  	publishedBuildInfo, found, err := sm.GetBuildInfo(buildInfoParams)
   343  	if err != nil {
   344  		return nil, err
   345  	}
   346  	if !found {
   347  		return &buildinfo.BuildInfo{}, nil
   348  	}
   349  
   350  	return &publishedBuildInfo.BuildInfo, nil
   351  }
   352  
   353  func (ic *IssuesConfiguration) populateIssuesConfigsFromSpec(configFilePath string) (err error) {
   354  	var vConfig *viper.Viper
   355  	vConfig, err = utils.ReadConfigFile(configFilePath, utils.YAML)
   356  	if err != nil {
   357  		return err
   358  	}
   359  
   360  	// Validate that the config contains issues.
   361  	if !vConfig.IsSet("issues") {
   362  		return errorutils.CheckError(errors.New(fmt.Sprintf(MissingConfigurationError, "issues")))
   363  	}
   364  
   365  	// Get server-id.
   366  	if vConfig.IsSet(ConfigIssuesPrefix + "serverID") {
   367  		ic.ServerID = vConfig.GetString(ConfigIssuesPrefix + "serverID")
   368  	}
   369  
   370  	// Set log limit.
   371  	ic.LogLimit = GitLogLimit
   372  
   373  	// Get tracker data
   374  	if !vConfig.IsSet(ConfigIssuesPrefix + "trackerName") {
   375  		return errorutils.CheckError(errors.New(fmt.Sprintf(MissingConfigurationError, ConfigIssuesPrefix+"trackerName")))
   376  	}
   377  	ic.TrackerName = vConfig.GetString(ConfigIssuesPrefix + "trackerName")
   378  
   379  	// Get issues pattern
   380  	if !vConfig.IsSet(ConfigIssuesPrefix + "regexp") {
   381  		return errorutils.CheckError(errors.New(fmt.Sprintf(MissingConfigurationError, ConfigIssuesPrefix+"regexp")))
   382  	}
   383  	ic.Regexp = vConfig.GetString(ConfigIssuesPrefix + "regexp")
   384  
   385  	// Get issues base url
   386  	if vConfig.IsSet(ConfigIssuesPrefix + "trackerUrl") {
   387  		ic.TrackerUrl = vConfig.GetString(ConfigIssuesPrefix + "trackerUrl")
   388  	}
   389  
   390  	// Get issues key group index
   391  	if !vConfig.IsSet(ConfigIssuesPrefix + "keyGroupIndex") {
   392  		return errorutils.CheckError(errors.New(fmt.Sprintf(MissingConfigurationError, ConfigIssuesPrefix+"keyGroupIndex")))
   393  	}
   394  	ic.KeyGroupIndex, err = strconv.Atoi(vConfig.GetString(ConfigIssuesPrefix + "keyGroupIndex"))
   395  	if err != nil {
   396  		return errorutils.CheckError(errors.New(fmt.Sprintf(ConfigParseValueError, ConfigIssuesPrefix+"keyGroupIndex", err.Error())))
   397  	}
   398  
   399  	// Get issues summary group index
   400  	if !vConfig.IsSet(ConfigIssuesPrefix + "summaryGroupIndex") {
   401  		return errorutils.CheckError(errors.New(fmt.Sprintf(MissingConfigurationError, ConfigIssuesPrefix+"summaryGroupIndex")))
   402  	}
   403  	ic.SummaryGroupIndex, err = strconv.Atoi(vConfig.GetString(ConfigIssuesPrefix + "summaryGroupIndex"))
   404  	if err != nil {
   405  		return errorutils.CheckError(errors.New(fmt.Sprintf(ConfigParseValueError, ConfigIssuesPrefix+"summaryGroupIndex", err.Error())))
   406  	}
   407  
   408  	// Get aggregation aggregate
   409  	ic.Aggregate = false
   410  	if vConfig.IsSet(ConfigIssuesPrefix + "aggregate") {
   411  		ic.Aggregate, err = strconv.ParseBool(vConfig.GetString(ConfigIssuesPrefix + "aggregate"))
   412  		if err != nil {
   413  			return errorutils.CheckError(errors.New(fmt.Sprintf(ConfigParseValueError, ConfigIssuesPrefix+"aggregate", err.Error())))
   414  		}
   415  	}
   416  
   417  	// Get aggregation status
   418  	if vConfig.IsSet(ConfigIssuesPrefix + "aggregationStatus") {
   419  		ic.AggregationStatus = vConfig.GetString(ConfigIssuesPrefix + "aggregationStatus")
   420  	}
   421  
   422  	return nil
   423  }
   424  
   425  func (ic *IssuesConfiguration) setServerDetails() error {
   426  	// If no server-id provided, use default server.
   427  	serverDetails, err := utilsconfig.GetSpecificConfig(ic.ServerID, true, false)
   428  	if err != nil {
   429  		return err
   430  	}
   431  	ic.ServerDetails = serverDetails
   432  	return nil
   433  }
   434  
   435  type IssuesConfiguration struct {
   436  	ServerDetails     *utilsconfig.ServerDetails
   437  	Regexp            string
   438  	LogLimit          int
   439  	TrackerUrl        string
   440  	TrackerName       string
   441  	KeyGroupIndex     int
   442  	SummaryGroupIndex int
   443  	Aggregate         bool
   444  	AggregationStatus string
   445  	ServerID          string
   446  }
   447  
   448  type LogCmd struct {
   449  	logLimit        int
   450  	lastVcsRevision string
   451  }
   452  
   453  func (logCmd *LogCmd) GetCmd() *exec.Cmd {
   454  	var cmd []string
   455  	cmd = append(cmd, "git")
   456  	cmd = append(cmd, "log", "--pretty=format:%s", "-"+strconv.Itoa(logCmd.logLimit))
   457  	if logCmd.lastVcsRevision != "" {
   458  		cmd = append(cmd, logCmd.lastVcsRevision+"..")
   459  	}
   460  	return exec.Command(cmd[0], cmd[1:]...)
   461  }
   462  
   463  func (logCmd *LogCmd) GetEnv() map[string]string {
   464  	return map[string]string{}
   465  }
   466  
   467  func (logCmd *LogCmd) GetStdWriter() io.WriteCloser {
   468  	return nil
   469  }
   470  
   471  func (logCmd *LogCmd) GetErrWriter() io.WriteCloser {
   472  	return nil
   473  }