package cmd import ( "bytes" "encoding/xml" "fmt" "html" "io/ioutil" "net/http" "net/http/cookiejar" "net/url" "regexp" "strconv" "strings" "github.com/SAP/jenkins-library/pkg/command" piperhttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/telemetry" "github.com/pkg/errors" ) var atcFailure, aUnitFailure bool func gctsExecuteABAPQualityChecks(config gctsExecuteABAPQualityChecksOptions, telemetryData *telemetry.CustomData) { // for command execution use Command c := command.Command{} // reroute command output to logging framework c.Stdout(log.Writer()) c.Stderr(log.Writer()) httpClient := &piperhttp.Client{} // error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end err := rungctsExecuteABAPQualityChecks(&config, httpClient) if err != nil { log.Entry().WithError(err).Fatal("step execution failed") } if aUnitFailure || atcFailure { log.Entry().Fatal("step execution failed") } } func rungctsExecuteABAPQualityChecks(config *gctsExecuteABAPQualityChecksOptions, httpClient piperhttp.Sender) error { const localChangedObjects = "localchangedobjects" const remoteChangedObjects = "remotechangedobjects" const localChangedPackages = "localchangedpackages" const remoteChangedPackages = "remotechangedpackages" const repository = "repository" const packages = "packages" cookieJar, cookieErr := cookiejar.New(nil) if cookieErr != nil { return errors.Wrap(cookieErr, "creating a cookie jar failed") } maxRetries := -1 clientOptions := piperhttp.ClientOptions{ CookieJar: cookieJar, Username: config.Username, Password: config.Password, MaxRetries: maxRetries, TransportSkipVerification: config.SkipSSLVerification, } httpClient.SetOptions(clientOptions) log.Entry().Infof("start of gctsExecuteABAPQualityChecks step with configuration values: %v", config) var objects []repoObject var err error log.Entry().Info("scope:", config.Scope) switch strings.ToLower(config.Scope) { case localChangedObjects: objects, err = getLocalObjects(config, httpClient) case remoteChangedObjects: objects, err = getRemoteObjects(config, httpClient) case localChangedPackages: objects, err = getLocalPackages(config, httpClient) case remoteChangedPackages: objects, err = getRemotePackages(config, httpClient) case repository: objects, err = getRepositoryObjects(config, httpClient) case packages: objects, err = getPackages(config, httpClient) default: log.Entry().Info("the specified scope does not exists, the default one will be used:" + repository) objects, err = getRepositoryObjects(config, httpClient) } if err != nil { log.Entry().WithError(err).Fatal("failure in get objects") } if objects == nil { log.Entry().Warning("no object delta was found, therefore the step execution will stop") return nil } log.Entry().Infof("objects to be checked:") for _, object := range objects { log.Entry().Info(object.Type, " ", object.Object) } if config.AUnitTest { // wrapper for execution of AUnit Test err := executeAUnitTest(config, httpClient, objects) if err != nil { log.Entry().WithError(err) } if aUnitFailure { log.Entry().Error("unit test(s) has/have failed! Check " + config.AUnitResultsFileName + " for more information! If you have enabled Warnings-Next-Generation Plugin, you can see the issues there!") } else { log.Entry().Info("AUnit test run completed successfully. If there are any results from the run, the results are saved in " + config.AUnitResultsFileName) } } if config.AtcCheck { // wrapper for execution of ATCChecks err = executeATCCheck(config, httpClient, objects) if err != nil { log.Entry().WithError(err).Fatal("execute ATC Check failed") } if atcFailure { log.Entry().Error(" ATC issue(s) found! Check " + config.AtcResultsFileName + " for more information! If you have enabled Warnings-Next-Generation Plugin, you can see the issues there!") } else { log.Entry().Info("ATCCheck test run completed successfully. If there are any results from the run, the results are saved in " + config.AtcResultsFileName) } } return nil } func getLocalObjects(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) ([]repoObject, error) { var localObjects []repoObject var localObject repoObject log.Entry().Info("get local changed objects started") if config.Commit == "" { return []repoObject{}, errors.Errorf("For scope: localChangedObjects you need to specify a commit") } history, err := getHistory(config, client) if err != nil { return []repoObject{}, errors.Wrap(err, "get local changed objects failed") } if len(history.Result) == 0 { return []repoObject{}, errors.Wrap(err, "no activities (from commit - to commit) were found") } fromCommit := history.Result[0].FromCommit log.Entry().Info("from Commit: ", fromCommit) toCommit := history.Result[0].ToCommit log.Entry().Info("to Commit: ", toCommit) // object delta between FromCommit and ToCommit retrieved from Activities Tab in gCTS resp, err := getObjectDifference(config, fromCommit, toCommit, client) if err != nil { return []repoObject{}, errors.Wrap(err, "get local changed objects failed") } for _, object := range resp.Objects { localObject.Object = object.Name localObject.Type = object.Type localObjects = append(localObjects, localObject) } log.Entry().Info("get local changed objects finished") return localObjects, nil } func getRemoteObjects(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) ([]repoObject, error) { var remoteObjects []repoObject var remoteObject repoObject var currentRemoteCommit string log.Entry().Info("get remote changed objects started") if config.Commit == "" { return []repoObject{}, errors.Errorf("For scope: remoteChangedObjects you need to specify a commit") } commitList, err := getCommitList(config, client) if err != nil { return []repoObject{}, errors.Wrap(err, "get remote changed objects failed") } for i, commit := range commitList.Commits { if commit.ID == config.Commit { currentRemoteCommit = commitList.Commits[i+1].ID break } } if currentRemoteCommit == "" { return []repoObject{}, errors.New("current remote commit was not found") } log.Entry().Info("current commit in the remote repository: ", currentRemoteCommit) // object delta between the commit that triggered the pipeline and the current commit in the remote repository resp, err := getObjectDifference(config, currentRemoteCommit, config.Commit, client) if err != nil { return []repoObject{}, errors.Wrap(err, "get remote changed objects failed") } for _, object := range resp.Objects { remoteObject.Object = object.Name remoteObject.Type = object.Type remoteObjects = append(remoteObjects, remoteObject) } log.Entry().Info("get remote changed objects finished") return remoteObjects, nil } func getLocalPackages(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) ([]repoObject, error) { var localPackages []repoObject var localPackage repoObject log.Entry().Info("get local changed packages started") if config.Commit == "" { return []repoObject{}, errors.Errorf("For scope: localChangedPackages you need to specify a commit") } history, err := getHistory(config, client) if err != nil { return []repoObject{}, errors.Wrap(err, "get local changed objects failed") } if len(history.Result) == 0 { return []repoObject{}, errors.Wrap(err, "no activities (from commit - to commit) were found") } fromCommit := history.Result[0].FromCommit log.Entry().Info("from Commit: ", fromCommit) toCommit := history.Result[0].ToCommit log.Entry().Info("to Commit: ", toCommit) // object delta between FromCommit and ToCommit retrieved from Activities Tab in gCTS resp, err := getObjectDifference(config, fromCommit, config.Commit, client) if err != nil { return []repoObject{}, errors.Wrap(err, "get local changed packages failed") } myPackages := map[string]bool{} // objects are resolved into packages(DEVC) for _, object := range resp.Objects { objInfo, err := getObjectInfo(config, client, object.Name, object.Type) if err != nil { return []repoObject{}, errors.Wrap(err, "get local changed packages failed") } if myPackages[objInfo.Devclass] { } else { myPackages[objInfo.Devclass] = true localPackage.Object = objInfo.Devclass localPackage.Type = "DEVC" localPackages = append(localPackages, localPackage) } } log.Entry().Info("get local changed packages finished") return localPackages, nil } func getRemotePackages(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) ([]repoObject, error) { var remotePackages []repoObject var remotePackage repoObject var currentRemoteCommit string log.Entry().Info("get remote changed packages started") if config.Commit == "" { return []repoObject{}, errors.Errorf("For scope: remoteChangedPackages you need to specify a commit") } commitList, err := getCommitList(config, client) if err != nil { return []repoObject{}, errors.Wrap(err, "get remote changed packages failed") } for i, commit := range commitList.Commits { if commit.ID == config.Commit { currentRemoteCommit = commitList.Commits[i+1].ID break } } if currentRemoteCommit == "" { return []repoObject{}, errors.Wrap(err, "current remote commit was not found") } log.Entry().Info("current commit in the remote repository: ", currentRemoteCommit) //object delta between the commit that triggered the pipeline and the current commit in the remote repository resp, err := getObjectDifference(config, currentRemoteCommit, config.Commit, client) if err != nil { return []repoObject{}, errors.Wrap(err, "get remote changed packages failed") } myPackages := map[string]bool{} // objects are resolved into packages(DEVC) for _, object := range resp.Objects { objInfo, err := getObjectInfo(config, client, object.Name, object.Type) if err != nil { return []repoObject{}, errors.Wrap(err, "get remote changed packages failed") } if myPackages[objInfo.Devclass] { } else { myPackages[objInfo.Devclass] = true remotePackage.Object = objInfo.Devclass remotePackage.Type = "DEVC" remotePackages = append(remotePackages, remotePackage) } } log.Entry().Info("get remote changed packages finished") return remotePackages, nil } func getRepositoryObjects(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) ([]repoObject, error) { log.Entry().Info("get repository objects started") var repoResp repoObjectResponse url := config.Host + "/sap/bc/cts_abapvcs/repository/" + config.Repository + "/objects?sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return nil, urlErr } resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) defer func() { if resp != nil && resp.Body != nil { resp.Body.Close() } }() if httpErr != nil { return []repoObject{}, errors.Wrap(httpErr, "could not get repository objects") } else if resp == nil { return []repoObject{}, errors.New("could not get repository objects: did not retrieve a HTTP response") } parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &repoResp) if parsingErr != nil { return []repoObject{}, errors.Errorf("%v", parsingErr) } var repositoryObjects []repoObject // remove object type DEVC, because it is already included in scope packages // also if you run ATC Checks for DEVC together with other object types, ATC checks will run only for DEVC for _, object := range repoResp.Objects { if object.Type != "DEVC" { repositoryObjects = append(repositoryObjects, object) } } log.Entry().Info("get repository objects finished") // all objects that are part of the local repository return repositoryObjects, nil } func getPackages(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) ([]repoObject, error) { var packages []repoObject log.Entry().Info("get packages started") var repoResp repoObjectResponse url := config.Host + "/sap/bc/cts_abapvcs/repository/" + config.Repository + "/objects?sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return nil, urlErr } resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) defer func() { if resp != nil && resp.Body != nil { resp.Body.Close() } }() if httpErr != nil { return []repoObject{}, errors.Wrap(httpErr, "get packages failed: could not get repository objects") } else if resp == nil { return []repoObject{}, errors.New("get packages failed: could not get repository objects: did not retrieve a HTTP response") } parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &repoResp) if parsingErr != nil { return []repoObject{}, errors.Errorf("%v", parsingErr) } // chose only DEVC from repository objects for _, object := range repoResp.Objects { if object.Type == "DEVC" { packages = append(packages, object) } } log.Entry().Info("get packages finished") return packages, nil } func discoverServer(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) (*http.Header, error) { url := config.Host + "/sap/bc/adt/core/discovery?sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return nil, urlErr } header := make(http.Header) header.Add("Accept", "application/atomsvc+xml") header.Add("x-csrf-token", "fetch") header.Add("saml2", "disabled") disc, httpErr := client.SendRequest("GET", url, nil, header, nil) defer func() { if disc != nil && disc.Body != nil { disc.Body.Close() } }() if httpErr != nil { return nil, errors.Wrap(httpErr, "discovery of the ABAP server failed") } else if disc == nil || disc.Header == nil { return nil, errors.New("discovery of the ABAP server failed: did not retrieve a HTTP response") } return &disc.Header, nil } func executeAUnitTest(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender, objects []repoObject) error { log.Entry().Info("execute ABAP Unit Test started") var innerXml string var result runResult for _, object := range objects { switch object.Type { case "CLAS": innerXml = innerXml + `` case "DEVC": innerXml = innerXml + `` } } var xmlBody = []byte(` ` + innerXml + ` `) resp, err := runAUnitTest(config, client, xmlBody) if err != nil { return errors.Wrap(err, "execute of Aunit test has failed") } parsingErr := piperhttp.ParseHTTPResponseBodyXML(resp, &result) if parsingErr != nil { log.Entry().Warning(parsingErr) return nil } parsedRes, err := parseUnitResult(config, client, &result) if err != nil { log.Entry().Warning(err) return nil } log.Entry().Info("execute ABAP Unit Test finished.", parsedRes.Text) return nil } func runAUnitTest(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender, xml []byte) (response *http.Response, err error) { log.Entry().Info("run ABAP Unit Test started") url := config.Host + "/sap/bc/adt/abapunit/testruns?sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return nil, urlErr } discHeader, discError := discoverServer(config, client) if discError != nil { return response, errors.Wrap(discError, "run of unit tests failed") } if discHeader.Get("X-Csrf-Token") == "" { return response, errors.Errorf("could not retrieve x-csrf-token from server") } header := make(http.Header) header.Add("x-csrf-token", discHeader.Get("X-Csrf-Token")) header.Add("Accept", "application/xml") header.Add("Content-Type", "application/vnd.sap.adt.abapunit.testruns.result.v1+xml") response, httpErr := client.SendRequest("POST", url, bytes.NewBuffer(xml), header, nil) if httpErr != nil { return response, errors.Wrap(httpErr, "run of unit tests failed") } else if response == nil { return response, errors.New("run of unit tests failed: did not retrieve a HTTP response") } log.Entry().Info("run ABAP Unit Test finished") return response, nil } func parseUnitResult(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender, aUnitRunResult *runResult) (parsedResult checkstyle, err error) { log.Entry().Info("parse ABAP Unit Result started") var fileName string var aUnitFile file var aUnitError checkstyleError parsedResult.Version = "1.0" for _, program := range aUnitRunResult.Program { objectType := program.Type[0:4] objectName := program.Name //syntax error in unit test or class if program.Alerts.Alert.HasSyntaxErrors == "true" { aUnitFailure = true aUnitError.Source = objectName aUnitError.Severity = "error" log.Entry().Info("severity: ", aUnitError.Severity) aUnitError.Message = html.UnescapeString(program.Alerts.Alert.Title + " " + program.Alerts.Alert.Details.Detail.AttrText) log.Entry().Info("message: ", aUnitError.Message) aUnitError.Line, err = findLine(config, client, program.Alerts.Alert.Stack.StackEntry.URI, objectName, objectType) log.Entry().Error("line: ", aUnitError.Line) if err != nil { return parsedResult, errors.Wrap(err, "parse AUnit Result failed") } fileName, err = getFileName(config, client, program.Alerts.Alert.Stack.StackEntry.URI, objectName) log.Entry().Error("file path: ", aUnitError.Line) if err != nil { return parsedResult, errors.Wrap(err, "parse AUnit Result failed") } aUnitFile.Error = append(aUnitFile.Error, aUnitError) aUnitError = checkstyleError{} log.Entry().Error("there is a syntax error", aUnitFile) } for _, testClass := range program.TestClasses.TestClass { for _, testMethod := range testClass.TestMethods.TestMethod { aUnitError.Source = testClass.Name + "/" + testMethod.Name // unit test failure if len(testMethod.Alerts.Alert) > 0 { for _, testalert := range testMethod.Alerts.Alert { switch testalert.Severity { case "fatal": log.Entry().Error("unit test " + aUnitError.Source + " has failed with severity fatal") aUnitFailure = true aUnitError.Severity = "error" case "critical": log.Entry().Error("unit test " + aUnitError.Source + " has failed with severity critical") aUnitFailure = true aUnitError.Severity = "error" case "tolerable": log.Entry().Warning("unit test " + aUnitError.Source + " has failed with severity warning") aUnitError.Severity = "warning" default: aUnitError.Severity = "info" } //unit test message is spread in different elements for _, detail := range testalert.Details.Detail { aUnitError.Message = aUnitError.Message + " " + detail.AttrText for _, subdetail := range detail.Details.Detail { aUnitError.Message = html.UnescapeString(aUnitError.Message + " " + subdetail.AttrText) log.Entry().Info("message: ", aUnitError.Message) } } aUnitError.Line, err = findLine(config, client, testalert.Stack.StackEntry.URI, objectName, objectType) log.Entry().Info("line: ", aUnitError.Line) if err != nil { log.Entry().Warning(err) } } aUnitFile.Error = append(aUnitFile.Error, aUnitError) aUnitError = checkstyleError{} } else { log.Entry().Info("unit test:", aUnitError.Source, "- was successful") } } fileName, err = getFileName(config, client, testClass.URI, objectName) if err != nil { return parsedResult, errors.Wrap(err, "parse AUnit Result failed") } } aUnitFile.Name, err = constructPath(config, client, fileName, objectName, objectType) log.Entry().Error("file path: ", aUnitFile.Name) if err != nil { return parsedResult, errors.Wrap(err, "parse AUnit Result failed") } parsedResult.File = append(parsedResult.File, aUnitFile) aUnitFile = file{} } body, _ := xml.Marshal(parsedResult) writeErr := ioutil.WriteFile(config.AUnitResultsFileName, body, 0644) if writeErr != nil { log.Entry().Error("file AUnitResults.xml could not be created") return parsedResult, fmt.Errorf("handling unit test results failed: %w", writeErr) } log.Entry().Info("parse ABAP Unit Result finished") return parsedResult, nil } func executeATCCheck(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender, objects []repoObject) (error error) { log.Entry().Info("execute ATC Check started") var innerXml string var result worklist for _, object := range objects { switch object.Type { case "CLAS": innerXml = innerXml + `` case "INTF": innerXml = innerXml + `` case "DEVC": innerXml = innerXml + `` case "FUGR": innerXml = innerXml + `` case "TABL": innerXml = innerXml + `` case "DTEL": innerXml = innerXml + `` case "DOMA": innerXml = innerXml + `` case "MSAG": innerXml = innerXml + `` case "PROG": innerXml = innerXml + `` default: log.Entry().Warning("object Type " + object.Type + " is not supported!") } } var xmlBody = []byte(` ` + innerXml + ` `) worklist, err := getWorklist(config, client) if err != nil { return errors.Wrap(err, "execution of ATC Checks failed") } err = startATCRun(config, client, xmlBody, worklist) if err != nil { return errors.Wrap(err, "execution of ATC Checks failed") } resp, err := getATCRun(config, client, worklist) if err != nil { return errors.Wrap(err, "execution of ATC Checks failed") } parsingErr := piperhttp.ParseHTTPResponseBodyXML(resp, &result) if parsingErr != nil { log.Entry().Warning(parsingErr) return nil } atcRes, err := parseATCCheckResult(config, client, &result) if err != nil { log.Entry().Error(err) return errors.Wrap(err, "execution of ATC Checks failed") } log.Entry().Info("execute ATC Checks finished.", atcRes.Text) return nil } func startATCRun(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender, xml []byte, worklistID string) (err error) { log.Entry().Info("ATC Run started") discHeader, discError := discoverServer(config, client) if discError != nil { return errors.Wrap(discError, "start of ATC run failed") } if discHeader.Get("X-Csrf-Token") == "" { return errors.Errorf("could not retrieve x-csrf-token from server") } header := make(http.Header) header.Add("x-csrf-token", discHeader.Get("X-Csrf-Token")) header.Add("Accept", "application/xml") url := config.Host + "/sap/bc/adt/atc/runs?worklistId=" + worklistID + "&sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return urlErr } resp, httpErr := client.SendRequest("POST", url, bytes.NewBuffer(xml), header, nil) defer func() { if resp != nil && resp.Body != nil { resp.Body.Close() } }() if httpErr != nil { return errors.Wrap(httpErr, "start of ATC run failed") } else if resp == nil { return errors.New("start of ATC run failed: did not retrieve a HTTP response") } log.Entry().Info("ATC Run finished") return nil } func getATCRun(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender, worklistID string) (response *http.Response, err error) { log.Entry().Info("get ATC Run Results started") header := make(http.Header) url := config.Host + "/sap/bc/adt/atc/worklists/" + worklistID + "?sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return nil, urlErr } header.Add("Accept", "application/atc.worklist.v1+xml") resp, httpErr := client.SendRequest("GET", url, nil, header, nil) if httpErr != nil { return response, errors.Wrap(httpErr, "get ATC run failed") } else if resp == nil { return response, errors.New("get ATC run failed: did not retrieve a HTTP response") } log.Entry().Info("get ATC Run Results finished") return resp, nil } func getWorklist(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) (worklistID string, error error) { url := config.Host + "/sap/bc/adt/atc/worklists?checkVariant=" + config.AtcVariant + "&sap-client=" + config.Client discHeader, discError := discoverServer(config, client) url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return worklistID, urlErr } if discError != nil { return worklistID, errors.Wrap(discError, "get worklist failed") } if discHeader.Get("X-Csrf-Token") == "" { return worklistID, errors.Errorf("could not retrieve x-csrf-token from server") } header := make(http.Header) header.Add("x-csrf-token", discHeader.Get("X-Csrf-Token")) header.Add("Accept", "*/*") resp, httpErr := client.SendRequest("POST", url, nil, header, nil) defer func() { if resp != nil && resp.Body != nil { resp.Body.Close() } }() if httpErr != nil { return worklistID, errors.Wrap(httpErr, "get worklist failed") } else if resp == nil { return worklistID, errors.New("get worklist failed: did not retrieve a HTTP response") } location := resp.Header["Location"][0] locationSlice := strings.Split(location, "/") worklistID = locationSlice[len(locationSlice)-1] log.Entry().Info("worklist id for ATC check: ", worklistID) return worklistID, nil } func parseATCCheckResult(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender, response *worklist) (atcResults checkstyle, error error) { log.Entry().Info("parse ATC Check Result started") var atcFile file var subObject string var aTCUnitError checkstyleError atcResults.Version = "1.0" for _, object := range response.Objects.Object { objectType := object.Type objectName := object.Name for _, atcworklist := range object.Findings.Finding { log.Entry().Info("there is atc finding for object type: ", objectType+" object name: "+objectName) path, err := url.PathUnescape(atcworklist.Location) if err != nil { return atcResults, errors.Wrap(err, "conversion of ATC check results to CheckStyle has failed") } if len(atcworklist.Atcfinding) > 0 { priority, err := strconv.Atoi(atcworklist.Priority) if err != nil { return atcResults, errors.Wrap(err, "conversion of ATC check results to CheckStyle has failed") } switch priority { case 1: atcFailure = true aTCUnitError.Severity = "error" log.Entry().Error("atc issue with priority: 1 ") case 2: atcFailure = true aTCUnitError.Severity = "error" log.Entry().Error("atc issue with priority: 2 ") case 3: aTCUnitError.Severity = "warning" log.Entry().Warning("atc issue with priority: 3 ") default: aTCUnitError.Severity = "info" log.Entry().Info("atc issue with low priority ") } log.Entry().Error("severity: ", aTCUnitError.Severity) if aTCUnitError.Line == "" { aTCUnitError.Line, err = findLine(config, client, path, objectName, objectType) log.Entry().Info("line: ", aTCUnitError.Line) if err != nil { log.Entry().Info(path) log.Entry().Warning(err) } } if subObject != "" { aTCUnitError.Source = objectName + "/" + strings.ToUpper(subObject) } else { aTCUnitError.Source = objectName } aTCUnitError.Message = html.UnescapeString(atcworklist.CheckTitle + " " + atcworklist.MessageTitle) log.Entry().Info("message: ", aTCUnitError.Message) atcFile.Error = append(atcFile.Error, aTCUnitError) aTCUnitError = checkstyleError{} } if atcFile.Error[0].Message != "" { fileName, err := getFileName(config, client, path, objectName) if err != nil { return atcResults, errors.Wrap(err, "conversion of ATC check results to CheckStyle has failed") } atcFile.Name, err = constructPath(config, client, fileName, objectName, objectType) log.Entry().Info("file path: ", atcFile.Name) if err != nil { return atcResults, errors.Wrap(err, "conversion of ATC check results to CheckStyle has failed") } atcResults.File = append(atcResults.File, atcFile) atcFile = file{} } } } atcBody, _ := xml.Marshal(atcResults) writeErr := ioutil.WriteFile(config.AtcResultsFileName, atcBody, 0644) if writeErr != nil { log.Entry().Error("ATCResults.xml could not be created") return atcResults, fmt.Errorf("handling atc results failed: %w", writeErr) } log.Entry().Info("parsing ATC check results to CheckStyle has finished.") return atcResults, writeErr } func constructPath(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender, fileName string, objectName string, objectType string) (filePath string, error error) { targetDir, err := getTargetDir(config, client) if err != nil { return filePath, errors.Wrap(err, "path could not be constructed") } filePath = config.Workspace + "/" + targetDir + "/objects/" + strings.ToUpper(objectType) + "/" + strings.ToUpper(objectName) + "/" + fileName return filePath, nil } func findLine(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender, path string, objectName string, objectType string) (line string, error error) { regexLine := regexp.MustCompile(`.start=\d*`) regexMethod := regexp.MustCompile(`.name=[a-zA-Z0-9_-]*;`) readableSource, err := checkReadableSource(config, client) if err != nil { return line, errors.Wrap(err, "could not find line in source code") } fileName, err := getFileName(config, client, path, objectName) if err != nil { return line, err } filePath, err := constructPath(config, client, fileName, objectName, objectType) if err != nil { return line, errors.Wrap(err, objectType+"/"+objectName+"could not find line in source code") } var absLine int if readableSource { // the error line that we get from UnitTest Run or ATC Check is not aligned for the readable source, we need to calculated it rawfile, err := ioutil.ReadFile(filePath) if err != nil { return line, errors.Wrapf(err, "could not find object in the workspace of your CI/CD tool ") } file := string(rawfile) splittedfile := strings.Split(file, "\n") // CLAS/OSO - is unique identifier for protection section in CLAS if strings.Contains(path, "CLAS/OSO") { for l, line := range splittedfile { if strings.Contains(line, "protected section.") { absLine = l break } } // CLAS/OM - is unique identifier for method section in CLAS } else if strings.Contains(path, "CLAS/OM") { methodName := regexMethod.FindString(path) if methodName != "" { methodName = methodName[len(`.name=`) : len(methodName)-1] } for line, linecontent := range splittedfile { if strings.Contains(linecontent, "method"+" "+methodName) { absLine = line break } } // CLAS/OSI - is unique identifier for private section in CLAS } else if strings.Contains(path, "CLAS/OSI") { for line, linecontent := range splittedfile { if strings.Contains(linecontent, "private section.") { absLine = line break } } } errLine := regexLine.FindString(path) if errLine != "" { errLine, err := strconv.Atoi(errLine[len(`.start=`):]) if err == nil { line = strconv.Itoa(absLine + errLine) } } } else { // classic format errLine := regexLine.FindString(path) if errLine != "" { line = errLine[len(`.start=`):] } } return line, nil } func getFileName(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender, path string, objName string) (fileName string, error error) { readableSource, err := checkReadableSource(config, client) if err != nil { return fileName, errors.Wrap(err, "get file name has failed") } path, err = url.PathUnescape(path) var fileExtension string fileExtensionLength := 30 - len(objName) for i := 0; i < fileExtensionLength; i++ { fileExtension += "=" } if err != nil { return fileName, errors.Wrap(err, "get file name has failed") } // INTERFACES regexInterface := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/interfaces\/\w*`) intf := regexInterface.FindString(path) if intf != "" && fileName == "" { if readableSource { fileName = strings.ToLower(objName) + ".intf.abap" } else { fileName = "REPS " + strings.ToUpper(objName) + fileExtension + "IU.abap" } } // CLASSES DEFINITIONS regexClasDef := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/includes\/definitions\/`) clasDef := regexClasDef.FindString(path) if clasDef != "" && fileName == "" { if readableSource { fileName = strings.ToLower(objName) + ".clas.definitions.abap" } else { fileName = "CINC " + objName + fileExtension + "CCDEF.abap" } } // CLASSES IMPLEMENTATIONS regexClasImpl := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/includes\/implementations\/`) clasImpl := regexClasImpl.FindString(path) if clasImpl != "" && fileName == "" { if readableSource { fileName = strings.ToLower(objName) + ".clas.implementations.abap" } else { fileName = "CINC " + objName + fileExtension + "CCIMP.abap" } } // CLASSES MACROS regexClasMacro := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/includes\/macros\/`) clasMacro := regexClasMacro.FindString(path) if clasMacro != "" && fileName == "" { if readableSource { fileName = strings.ToLower(objName) + ".clas.macros.abap" } else { fileName = "CINC " + objName + fileExtension + "CCMAC.abap" } } // TEST CLASSES regexTestClass := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*#?\/?\w*\/?testclass`) testClass := regexTestClass.FindString(path) if testClass != "" && fileName == "" { if readableSource { fileName = strings.ToLower(objName) + ".clas.testclasses.abap" } else { fileName = "CINC " + objName + fileExtension + "CCAU.abap" } } // CLASS PROTECTED regexClasProtected := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/source\/main#type=CLAS\/OSO`) classProtected := regexClasProtected.FindString(path) if classProtected != "" && fileName == "" { if readableSource { fileName = strings.ToLower(objName) + ".clas.abap" } else { fileName = "CPRO " + objName + ".abap" } } // CLASS PRIVATE regexClasPrivate := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/source\/main#type=CLAS\/OSI`) classPrivate := regexClasPrivate.FindString(path) if classPrivate != "" && fileName == "" { if readableSource { fileName = strings.ToLower(objName) + ".clas.abap" } else { fileName = "CPRI " + objName + ".abap" } } // CLASS METHOD regexClasMethod := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/source\/main#type=CLAS\/OM`) classMethod := regexClasMethod.FindString(path) if classMethod != "" && fileName == "" { if readableSource { fileName = strings.ToLower(objName) + ".clas.abap" } else { regexmethodName := regexp.MustCompile(`name=\w*`) methodName := regexmethodName.FindString(path) fileName = "METH " + methodName[len(`name=`):] + ".abap" } } // CLASS PUBLIC regexClasPublic := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/\w*\/source\/main#start`) classPublic := regexClasPublic.FindString(path) if classPublic != "" && fileName == "" { if readableSource { fileName = strings.ToLower(objName) + ".clas.abap" } else { fileName = "CPUB " + objName + ".abap" } } // FUNCTION INCLUDE regexFuncIncl := regexp.MustCompile(`\/sap\/bc\/adt\/functions\/groups\/\w*\/includes/\w*`) funcIncl := regexFuncIncl.FindString(path) if funcIncl != "" && fileName == "" { regexSubObj := regexp.MustCompile(`includes\/\w*`) subObject := regexSubObj.FindString(path) subObject = subObject[len(`includes/`):] if readableSource { fileName = strings.ToLower(objName) + ".fugr." + strings.ToLower(subObject) + ".reps.abap" } else { fileName = "REPS " + strings.ToUpper(subObject) + ".abap" } } // FUNCTION GROUP regexFuncGr := regexp.MustCompile(`\/sap\/bc\/adt\/functions\/groups\/\w*\/source\/main`) funcGr := regexFuncGr.FindString(path) if funcGr != "" && fileName == "" { if readableSource { fileName = strings.ToLower(objName) + ".fugr.sapl" + strings.ToLower(objName) + ".reps.abap" } else { fileName = "REPS SAPL" + objName + ".abap" } } // FUNCTION MODULE regexFuncMod := regexp.MustCompile(`\/sap\/bc\/adt\/functions\/groups\/\w*\/fmodules/\w*`) funcMod := regexFuncMod.FindString(path) if funcMod != "" && fileName == "" { regexSubObj := regexp.MustCompile(`includes\/\w*`) subObject := regexSubObj.FindString(path) subObject = subObject[len(`includes/`):] if readableSource { fileName = strings.ToLower(subObject) + ".func.abap" } else { fileName = "FUNC " + subObject + ".abap" } } // CLAS regexClas := regexp.MustCompile(`\/sap\/bc\/adt\/oo\/classes\/` + strings.ToLower(objName)) clas := regexClas.FindString(path) if clas != "" && fileName == "" { if readableSource { fileName = strings.ToLower(objName) + ".clas.abap" } else { fileName = "CPUB " + objName + ".abap" } } // PROGRAM regexProg := regexp.MustCompile(`\/sap\/bc\/adt\/programs\/programs\/` + strings.ToLower(objName)) prog := regexProg.FindString(path) if prog != "" && fileName == "" { fileName = "REPS " + objName + ".abap" } // TABLES regexTab := regexp.MustCompile(`\/sap\/bc\/adt\/ddic\/tables\/` + strings.ToLower(objName)) tab := regexTab.FindString(path) if tab != "" && fileName == "" { fileName = "TABL " + objName + ".asx.json" } return fileName, nil } func getTargetDir(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) (string, error) { var targetDir string repository, err := getRepo(config, client) if err != nil { return targetDir, err } for _, config := range repository.Result.Config { if config.Key == "VCS_TARGET_DIR" { targetDir = config.Value } } return targetDir, nil } func checkReadableSource(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) (readableSource bool, error error) { repoLayout, err := getRepositoryLayout(config, client) if err != nil { return readableSource, errors.Wrap(err, "could not check readable source format") } if repoLayout.Layout.ReadableSource == "true" || repoLayout.Layout.ReadableSource == "only" || repoLayout.Layout.ReadableSource == "all" { readableSource = true } else { readableSource = false } return readableSource, nil } func getRepo(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) (repositoryResponse, error) { var repositoryResp repositoryResponse url := config.Host + "/sap/bc/cts_abapvcs/repository/" + config.Repository + "?sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return repositoryResp, urlErr } resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) defer func() { if resp != nil && resp.Body != nil { resp.Body.Close() } }() if httpErr != nil { return repositoryResponse{}, errors.Wrap(httpErr, "could not get repository") } else if resp == nil { return repositoryResponse{}, errors.New("could not get repository: did not retrieve a HTTP response") } parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &repositoryResp) if parsingErr != nil { return repositoryResponse{}, errors.Errorf("%v", parsingErr) } return repositoryResp, nil } func getRepositoryLayout(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) (layoutResponse, error) { var repoLayoutResponse layoutResponse url := config.Host + "/sap/bc/cts_abapvcs/repository/" + config.Repository + "/layout?sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return repoLayoutResponse, urlErr } resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) defer func() { if resp != nil && resp.Body != nil { resp.Body.Close() } }() if httpErr != nil { return layoutResponse{}, errors.Wrap(httpErr, "could not get repository layout") } else if resp == nil { return layoutResponse{}, errors.New("could not get repository layout: did not retrieve a HTTP response") } parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &repoLayoutResponse) if parsingErr != nil { return layoutResponse{}, errors.Errorf("%v", parsingErr) } return repoLayoutResponse, nil } func getCommitList(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) (commitResponse, error) { var commitResp commitResponse url := config.Host + "/sap/bc/cts_abapvcs/repository/" + config.Repository + "/getCommit?sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return commitResp, urlErr } resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) defer func() { if resp != nil && resp.Body != nil { resp.Body.Close() } }() if httpErr != nil { return commitResponse{}, errors.Wrap(httpErr, "get repository history failed") } else if resp == nil { return commitResponse{}, errors.New("get repository history failed: did not retrieve a HTTP response") } parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &commitResp) if parsingErr != nil { return commitResponse{}, errors.Errorf("%v", parsingErr) } return commitResp, nil } func getObjectDifference(config *gctsExecuteABAPQualityChecksOptions, fromCommit string, toCommit string, client piperhttp.Sender) (objectsResponse, error) { var objectResponse objectsResponse url := config.Host + "/sap/bc/cts_abapvcs/repository/" + config.Repository + "/compareCommits?fromCommit=" + fromCommit + "&toCommit=" + toCommit + "&sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return objectResponse, urlErr } resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) defer func() { if resp != nil && resp.Body != nil { resp.Body.Close() } }() if httpErr != nil { return objectsResponse{}, errors.Wrap(httpErr, "get object difference failed") } else if resp == nil { return objectsResponse{}, errors.New("get object difference failed: did not retrieve a HTTP response") } parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &objectResponse) if parsingErr != nil { return objectsResponse{}, errors.Errorf("%v", parsingErr) } log.Entry().Info("get object differences: ", objectResponse.Objects) return objectResponse, nil } func getObjectInfo(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender, objectName string, objectType string) (objectInfo, error) { var objectMetInfoResponse objectInfo url := config.Host + "/sap/bc/cts_abapvcs/objects/" + objectType + "/" + objectName + "?sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return objectMetInfoResponse, urlErr } resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) defer func() { if resp != nil && resp.Body != nil { resp.Body.Close() } }() if httpErr != nil { return objectInfo{}, errors.Wrap(httpErr, "resolve package failed") } else if resp == nil { return objectInfo{}, errors.New("resolve package failed: did not retrieve a HTTP response") } parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &objectMetInfoResponse) if parsingErr != nil { return objectInfo{}, errors.Errorf("%v", parsingErr) } return objectMetInfoResponse, nil } func getHistory(config *gctsExecuteABAPQualityChecksOptions, client piperhttp.Sender) (historyResponse, error) { var historyResp historyResponse url := config.Host + "/sap/bc/cts_abapvcs/repository/" + config.Repository + "/getHistory?sap-client=" + config.Client url, urlErr := addQueryToURL(url, config.QueryParameters) if urlErr != nil { return historyResp, urlErr } resp, httpErr := client.SendRequest("GET", url, nil, nil, nil) defer func() { if resp != nil && resp.Body != nil { resp.Body.Close() } }() if httpErr != nil { return historyResponse{}, errors.Wrap(httpErr, "get history failed") } else if resp == nil { return historyResponse{}, errors.New("get history failed: did not retrieve a HTTP response") } parsingErr := piperhttp.ParseHTTPResponseBodyJSON(resp, &historyResp) if parsingErr != nil { return historyResponse{}, errors.Errorf("%v", parsingErr) } return historyResp, nil } type worklist struct { XMLName xml.Name `xml:"worklist"` Text string `xml:",chardata"` ID string `xml:"id,attr"` Timestamp string `xml:"timestamp,attr"` UsedObjectSet string `xml:"usedObjectSet,attr"` ObjectSetIsComplete string `xml:"objectSetIsComplete,attr"` Atcworklist string `xml:"atcworklist,attr"` ObjectSets struct { Text string `xml:",chardata"` ObjectSet []struct { Text string `xml:",chardata"` Name string `xml:"name,attr"` Title string `xml:"title,attr"` Kind string `xml:"kind,attr"` } `xml:"objectSet"` } `xml:"objectSets"` Objects struct { Text string `xml:",chardata"` Object []struct { Text string `xml:",chardata"` URI string `xml:"uri,attr"` Type string `xml:"type,attr"` Name string `xml:"name,attr"` PackageName string `xml:"packageName,attr"` Author string `xml:"author,attr"` Atcobject string `xml:"atcobject,attr"` Adtcore string `xml:"adtcore,attr"` Findings struct { Text string `xml:",chardata"` Finding []struct { Text string `xml:",chardata"` URI string `xml:"uri,attr"` Location string `xml:"location,attr"` Processor string `xml:"processor,attr"` LastChangedBy string `xml:"lastChangedBy,attr"` Priority string `xml:"priority,attr"` CheckId string `xml:"checkId,attr"` CheckTitle string `xml:"checkTitle,attr"` MessageId string `xml:"messageId,attr"` MessageTitle string `xml:"messageTitle,attr"` ExemptionApproval string `xml:"exemptionApproval,attr"` ExemptionKind string `xml:"exemptionKind,attr"` Checksum string `xml:"checksum,attr"` QuickfixInfo string `xml:"quickfixInfo,attr"` Atcfinding string `xml:"atcfinding,attr"` Link struct { Text string `xml:",chardata"` Href string `xml:"href,attr"` Rel string `xml:"rel,attr"` Type string `xml:"type,attr"` Atom string `xml:"atom,attr"` } `xml:"link"` Quickfixes struct { Text string `xml:",chardata"` Manual string `xml:"manual,attr"` Automatic string `xml:"automatic,attr"` Pseudo string `xml:"pseudo,attr"` } `xml:"quickfixes"` } `xml:"finding"` } `xml:"findings"` } `xml:"object"` } `xml:"objects"` } type runResult struct { XMLName xml.Name `xml:"runResult"` Text string `xml:",chardata"` Aunit string `xml:"aunit,attr"` Program []struct { Text string `xml:",chardata"` URI string `xml:"uri,attr"` Type string `xml:"type,attr"` Name string `xml:"name,attr"` URIType string `xml:"uriType,attr"` Adtcore string `xml:"adtcore,attr"` Alerts struct { Text string `xml:",chardata"` Alert struct { Text string `xml:",chardata"` HasSyntaxErrors string `xml:"hasSyntaxErrors,attr"` Kind string `xml:"kind,attr"` Severity string `xml:"severity,attr"` Title string `xml:"title"` Details struct { Text string `xml:",chardata"` Detail struct { Text string `xml:",chardata"` AttrText string `xml:"text,attr"` } `xml:"detail"` } `xml:"details"` Stack struct { Text string `xml:",chardata"` StackEntry struct { Text string `xml:",chardata"` URI string `xml:"uri,attr"` Description string `xml:"description,attr"` } `xml:"stackEntry"` } `xml:"stack"` } `xml:"alert"` } `xml:"alerts"` TestClasses struct { Text string `xml:",chardata"` TestClass []struct { Text string `xml:",chardata"` URI string `xml:"uri,attr"` Type string `xml:"type,attr"` Name string `xml:"name,attr"` URIType string `xml:"uriType,attr"` NavigationURI string `xml:"navigationUri,attr"` DurationCategory string `xml:"durationCategory,attr"` RiskLevel string `xml:"riskLevel,attr"` TestMethods struct { Text string `xml:",chardata"` TestMethod []struct { Text string `xml:",chardata"` URI string `xml:"uri,attr"` Type string `xml:"type,attr"` Name string `xml:"name,attr"` ExecutionTime string `xml:"executionTime,attr"` URIType string `xml:"uriType,attr"` NavigationURI string `xml:"navigationUri,attr"` Unit string `xml:"unit,attr"` Alerts struct { Text string `xml:",chardata"` Alert []struct { Text string `xml:",chardata"` Kind string `xml:"kind,attr"` Severity string `xml:"severity,attr"` Title string `xml:"title"` Details struct { Text string `xml:",chardata"` Detail []struct { Text string `xml:",chardata"` AttrText string `xml:"text,attr"` Details struct { Text string `xml:",chardata"` Detail []struct { Text string `xml:",chardata"` AttrText string `xml:"text,attr"` } `xml:"detail"` } `xml:"details"` } `xml:"detail"` } `xml:"details"` Stack struct { Text string `xml:",chardata"` StackEntry struct { Text string `xml:",chardata"` URI string `xml:"uri,attr"` Type string `xml:"type,attr"` Name string `xml:"name,attr"` Description string `xml:"description,attr"` } `xml:"stackEntry"` } `xml:"stack"` } `xml:"alert"` } `xml:"alerts"` } `xml:"testMethod"` } `xml:"testMethods"` } `xml:"testClass"` } `xml:"testClasses"` } `xml:"program"` } type gctsException struct { Message string `json:"message"` Description string `json:"description"` Code int `json:"code"` } type gctsLogs struct { Time int `json:"time"` User string `json:"user"` Section string `json:"section"` Action string `json:"action"` Severity string `json:"severity"` Message string `json:"message"` Code string `json:"code"` } type commit struct { ID string `json:"id"` } type commitResponse struct { Commits []commit `json:"commits"` ErrorLog []gctsLogs `json:"errorLog"` Log []gctsLogs `json:"log"` Exception gctsException `json:"exception"` } type objectInfo struct { Pgmid string `json:"pgmid"` Object string `json:"object"` ObjName string `json:"objName"` Srcsystem string `json:"srcsystem"` Author string `json:"author"` Devclass string `json:"devclass"` } type repoConfig struct { Key string `json:"key"` Value string `json:"value"` Cprivate string `json:"cprivate"` Cprotected string `json:"cprotected"` Cvisible string `json:"cvisible"` Category string `json:"category"` Scope string `json:"scope"` ChangedAt float64 `json:"changeAt"` ChangedBy string `json:"changedBy"` } type repository struct { Rid string `json:"rid"` Name string `json:"name"` Role string `json:"role"` Type string `json:"type"` Vsid string `json:"vsid"` PrivateFlag string `json:"privateFlag"` Status string `json:"status"` Branch string `json:"branch"` Url string `json:"url"` CreatedBy string `json:"createdBy"` CreatedDate string `json:"createdDate"` Config []repoConfig `json:"config"` Objects int `json:"objects"` CurrentCommit string `json:"currentCommit"` } type repositoryResponse struct { Result repository `json:"result"` Exception gctsException `json:"exception"` } type objects struct { Name string `json:"name"` Type string `json:"type"` Action string `json:"action"` } type objectsResponse struct { Objects []objects `json:"objects"` Log []gctsLogs `json:"log"` Exception gctsException `json:"exception"` ErrorLogs []gctsLogs `json:"errorLog"` } type repoObject struct { Pgmid string `json:"pgmid"` Object string `json:"object"` Type string `json:"type"` Description string `json:"description"` } type repoObjectResponse struct { Objects []repoObject `json:"objects"` Log []gctsLogs `json:"log"` Exception gctsException `json:"exception"` ErrorLogs []gctsLogs `json:"errorLog"` } type layout struct { FormatVersion int `json:"formatVersion"` Format string `json:"format"` ObjectStorage string `json:"objectStorage"` MetaInformation string `json:"metaInformation"` TableContent string `json:"tableContent"` Subdirectory string `json:"subdirectory"` ReadableSource string `json:"readableSource"` KeepClient string `json:"keepClient"` } type layoutResponse struct { Layout layout `json:"layout"` Log []gctsLogs `json:"log"` Exception string `json:"exception"` ErrorLogs []gctsLogs `json:"errorLog"` } type history struct { Rid string `json:"rid"` CheckoutTime int `json:"checkoutTime"` FromCommit string `json:"fromCommit"` ToCommit string `json:"toCommit"` Caller string `json:"caller"` Type string `json:"type"` } type historyResponse struct { Result []history `xml:"result"` Exception string `json:"exception"` } type checkstyleError struct { Text string `xml:",chardata"` Message string `xml:"message,attr"` Source string `xml:"source,attr"` Line string `xml:"line,attr"` Severity string `xml:"severity,attr"` } type file struct { Text string `xml:",chardata"` Name string `xml:"name,attr"` Error []checkstyleError `xml:"error"` } type checkstyle struct { XMLName xml.Name `xml:"checkstyle"` Text string `xml:",chardata"` Version string `xml:"version,attr"` File []file `xml:"file"` }