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 }