internal/core/system.go (322 lines of code) (raw):

/* * Copyright 2021-2024 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package core import ( "bufio" "context" "encoding/json" "io" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/JetBrains/qodana-cli/internal/core/corescan" "github.com/JetBrains/qodana-cli/internal/platform" "github.com/JetBrains/qodana-cli/internal/platform/git" "github.com/JetBrains/qodana-cli/internal/platform/msg" "github.com/JetBrains/qodana-cli/internal/platform/nuget" "github.com/JetBrains/qodana-cli/internal/platform/qdenv" "github.com/JetBrains/qodana-cli/internal/platform/strutil" "github.com/JetBrains/qodana-cli/internal/platform/utils" cienvironment "github.com/cucumber/ci-environment/go" "github.com/docker/docker/client" "github.com/pterm/pterm" log "github.com/sirupsen/logrus" ) var ( // DisableCheckUpdates flag to disable checking for updates DisableCheckUpdates = false releaseUrl = "https://api.github.com/repos/JetBrains/qodana-cli/releases/latest" ) // CheckForUpdates check GitHub https://github.com/JetBrains/qodana-cli/ for the latest version of CLI release. func CheckForUpdates(currentVersion string) { if currentVersion == "dev" || strings.HasSuffix( currentVersion, "nightly", ) || qdenv.IsContainer() || cienvironment.DetectCIEnvironment() != nil || DisableCheckUpdates { return } latestVersion := getLatestVersion() if latestVersion != "" && latestVersion != currentVersion { msg.WarningMessage( "New version of %s CLI is available: %s. See https://jb.gg/qodana-cli/update\n", msg.PrimaryBold("qodana"), latestVersion, ) DisableCheckUpdates = true } } // getLatestVersion returns the latest published version of the CLI. func getLatestVersion() string { resp, err := http.Get(releaseUrl) if err != nil { return "" } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { return } }(resp.Body) if resp.StatusCode < 200 || resp.StatusCode > 299 { return "" } bodyText, err := io.ReadAll(resp.Body) if err != nil { return "" } result := make(map[string]interface{}) err = json.Unmarshal(bodyText, &result) if err != nil { return "" } return strings.TrimPrefix(result["tag_name"].(string), "v") } // OpenDir opens directory in the default file manager func OpenDir(path string) error { var cmd string var args []string switch runtime.GOOS { case "windows": cmd = "explorer" case "darwin": cmd = "open" default: // "linux", "freebsd", "openbsd", "netbsd" cmd = "xdg-open" } args = append(args, path) return exec.Command(cmd, args...).Start() } // IsHomeDirectory returns true if the given path is the user's home directory. func IsHomeDirectory(path string) bool { absPath, err := filepath.Abs(path) if err != nil { return false } home, err := os.UserHomeDir() if err != nil { return false } return absPath == home } // RunAnalysis runs the linter with the given options. func RunAnalysis(ctx context.Context, c corescan.Context) int { log.Debug("Running analysis with options") platform.LogContext(&c) if !utils.IsInstalled("git") && (c.FullHistory() || c.Commit() != "" || c.DiffStart() != "" || c.DiffEnd() != "") { log.Fatal("Cannot use git related functionality without a git executable") } startHash, err := c.StartHash() if err != nil { log.Fatal(err) } scenario := c.DetermineRunScenario(startHash != "") if scenario != corescan.RunScenarioDefault && !git.RevisionExists(c.RepositoryRoot(), startHash, c.LogDir()) { msg.WarningMessageCI( "Cannot run analysis for commit %s because it doesn't exist in the repository. Check that you retrieve the full git history before running Qodana.", startHash, ) scenario = corescan.RunScenarioDefault // backoff to regular analysis c = c.BackoffToDefaultAnalysisBecauseOfMissingCommit() } installPlugins(c) // this way of running needs to do bootstrap twice on different commits and will do it internally if !corescan.IsScopedScenario(scenario) && !c.Analyser().IsContainer() { utils.Bootstrap(c.QodanaYamlConfig().Bootstrap, c.ProjectDir()) } switch scenario { case corescan.RunScenarioFullHistory: return runWithFullHistory(ctx, c, startHash) case corescan.RunScenarioLocalChanges: return runLocalChanges(ctx, c, startHash) case corescan.RunScenarioScoped: analyzer := NewScopedAnalyzer(ctx, c, startHash, c.DiffEnd(), defaultRunner) return analyzer.RunAnalysis() case corescan.RunScenarioReversedScoped: analyzer := NewReverseScopedAnalyzer(ctx, c, startHash, c.DiffEnd(), defaultRunner) return analyzer.RunAnalysis() case corescan.RunScenarioDefault: return runQodana(ctx, c) default: log.Fatalf("Unknown run scenario %s", scenario) panic("Unreachable") } } func runLocalChanges(ctx context.Context, c corescan.Context, startHash string) int { var exitCode int gitReset := false r, err := git.CurrentRevision(c.RepositoryRoot(), c.LogDir()) if err != nil { log.Fatal(err) } if c.DiffEnd() != "" && c.DiffEnd() != r { msg.WarningMessage("Cannot run local-changes because --diff-end is %s and HEAD is %s", c.DiffEnd(), r) } else { err := git.Reset(c.RepositoryRoot(), startHash, c.LogDir()) if err != nil { msg.WarningMessage("Could not reset git repository, no --commit option will be applied: %s", err) } else { c = c.ForcedLocalChanges() gitReset = true } } exitCode = runQodana(ctx, c) if gitReset { _ = git.ResetBack(c.RepositoryRoot(), c.LogDir()) } return exitCode } func runWithFullHistory(ctx context.Context, c corescan.Context, startHash string) int { remoteUrl, err := git.RemoteUrl(c.RepositoryRoot(), c.LogDir()) if err != nil { log.Fatal(err) } branch, err := git.Branch(c.RepositoryRoot(), c.LogDir()) if err != nil { log.Fatal(err) } if remoteUrl == "" && branch == "" { log.Fatal("Please check that project is located within the Git repo. If you specified --repository-root option, check that it points to the right directory.") } err = git.Clean(c.RepositoryRoot(), c.LogDir()) if err != nil { log.Fatal(err) } revisions := git.Revisions(c.RepositoryRoot()) allCommits := len(revisions) counter := 0 var exitCode int if startHash != "" { for i, revision := range revisions { counter++ if revision == startHash { revisions = revisions[i:] break } } } for _, revision := range revisions { counter++ msg.WarningMessage("[%d/%d] Running analysis for revision %s", counter+1, allCommits, revision) err = git.CheckoutAndUpdateSubmodule(c.RepositoryRoot(), revision, true, c.LogDir()) if err != nil { log.Fatal(err) } msg.EmptyMessage() contextForAnalysis := c.WithVcsEnvForFullHistoryAnalysisIteration(remoteUrl, branch, revision) exitCode = runQodana(ctx, contextForAnalysis) } err = git.CheckoutAndUpdateSubmodule(c.RepositoryRoot(), branch, true, c.LogDir()) if err != nil { log.Fatal(err) } return exitCode } func runQodana(ctx context.Context, c corescan.Context) int { var exitCode int var err error if c.Analyser().IsContainer() { exitCode = runQodanaContainer(ctx, c) } else { nuget.UnsetNugetVariables() // TODO: get rid of it from 241 release exitCode, err = runQodanaLocal(c) if err != nil { log.Fatal(err) } } return exitCode } // followLinter follows the linter logs and prints the progress. func followLinter(client client.APIClient, containerName string, progress *pterm.SpinnerPrinter, scanStages []string) { reader, err := client.ContainerLogs(context.Background(), containerName, containerLogsOptions) if err != nil { log.Fatal(err.Error()) } defer func(reader io.ReadCloser) { err := reader.Close() if err != nil { log.Fatal(err.Error()) } }(reader) scanner := bufio.NewScanner(reader) interactive := msg.IsInteractive() for scanner.Scan() { line := scanner.Text() if !interactive && len(line) >= dockerSpecialCharsLength { line = line[dockerSpecialCharsLength:] } line = strings.TrimSuffix(line, "\n") if err == nil || len(line) > 0 { if strings.Contains(line, "Starting up") { msg.UpdateText(progress, scanStages[2]) } if strings.Contains(line, "The Project opening stage completed in") { msg.UpdateText(progress, scanStages[3]) } if strings.Contains(line, "The Project configuration stage completed in") { msg.UpdateText(progress, scanStages[4]) } if strings.Contains(line, "Detailed summary") { msg.UpdateText(progress, scanStages[5]) if !msg.IsInteractive() { msg.EmptyMessage() } } msg.PrintLinterLog(line) } if err != nil { if err != io.EOF { log.Errorf("Error scanning docker log stream: %s", err) } return } } } func getScanStages() []string { scanStages := []string{ "Preparing Qodana Docker images", "Starting the analysis engine", "Opening the project", "Configuring the project", "Analyzing the project", "Preparing the report", } for i, stage := range scanStages { scanStages[i] = msg.PrimaryBold("[%d/%d] ", i+1, len(scanStages)+1) + msg.Primary(stage) } return scanStages } // saveReport saves web files to expect, and generates json. func saveReport(c corescan.Context) { prod := c.Prod() if !qdenv.IsContainer() || (!c.SaveReport() && !c.ShowReport()) { return } reportConverter := filepath.Join(prod.IdeBin(), "intellij-report-converter.jar") if _, err := os.Stat(reportConverter); os.IsNotExist(err) { log.Fatal("Not able to save the report: report-converter is missing") return } log.Println("Generating HTML report ...") javaPath := prod.JbrJava() if javaPath == "" { log.Error( "HTML report is not generated because Java is not installed. " + "See requirements in our documentation: https://www.jetbrains.com/help/qodana/deploy-qodana.html", ) return } if res, err := utils.RunCmd( "", strutil.QuoteForWindows(prod.JbrJava()), "-jar", strutil.QuoteForWindows(reportConverter), "-s", strutil.QuoteForWindows(c.ProjectDir()), "-d", strutil.QuoteForWindows(c.ResultsDir()), "-o", strutil.QuoteForWindows(platform.ReportResultsPath(c.ReportDir())), "-n", "result-allProblems.json", "-f", ); res > 0 || err != nil { os.Exit(res) } err := utils.CopyDir(filepath.Join(prod.Home, "web"), c.ReportDir()) if err != nil { log.Fatal("Not able to save the report: ", err) return } }