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

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