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