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 }