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 }