From f82dbec738e0bc90fe652d144baf91bb73414433 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 25 May 2022 13:05:58 +0200 Subject: SONAR-16416 Fix SSF-266 --- .../org/sonar/scm/git/CompositeBlameCommand.java | 2 +- .../java/org/sonar/scm/git/GitBlameCommand.java | 100 +++++++++++---------- .../main/java/org/sonar/scm/git/GitScmSupport.java | 1 + .../org/sonar/scm/git/ProcessWrapperFactory.java | 88 ++++++++++++++++++ 4 files changed, 145 insertions(+), 46 deletions(-) create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scm/git/ProcessWrapperFactory.java (limited to 'sonar-scanner-engine/src/main/java/org/sonar') diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java index 762e20b65d0..a39b6401767 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/CompositeBlameCommand.java @@ -71,7 +71,7 @@ public class CompositeBlameCommand extends BlameCommand { profiler.startDebug("Collecting committed files"); Set committedFiles = collectAllCommittedFiles(repo); profiler.stopDebug(); - nativeGitEnabled = nativeCmd.isEnabled(); + nativeGitEnabled = nativeCmd.checkIfEnabled(); ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new GitThreadFactory()); for (InputFile inputFile : input.filesToBlame()) { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java index 06a216d51ab..10733386574 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitBlameCommand.java @@ -19,53 +19,60 @@ */ package org.sonar.scm.git; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.IOException; import java.nio.file.Path; import java.time.Instant; import java.util.Date; import java.util.LinkedList; import java.util.List; -import java.util.Scanner; -import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.annotation.Nullable; import org.sonar.api.batch.scm.BlameLine; +import org.sonar.api.utils.System2; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.springframework.beans.factory.annotation.Autowired; -import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.emptyList; import static org.sonar.api.utils.Preconditions.checkState; public class GitBlameCommand { private static final Logger LOG = Loggers.get(GitBlameCommand.class); private static final Pattern EMAIL_PATTERN = Pattern.compile("<(\\S*?)>"); - private static final String COMITTER_TIME = "committer-time "; - private static final String COMITTER_MAIL = "committer-mail "; + private static final String COMMITTER_TIME = "committer-time "; + private static final String COMMITTER_MAIL = "committer-mail "; - private static final String GIT_COMMAND = "git"; + private static final String DEFAULT_GIT_COMMAND = "git"; private static final String BLAME_COMMAND = "blame"; private static final String BLAME_LINE_PORCELAIN_FLAG = "--line-porcelain"; private static final String IGNORE_WHITESPACES = "-w"; - private final String gitCommand; + private final System2 system; + private final ProcessWrapperFactory processWrapperFactory; + private String gitCommand; @Autowired - public GitBlameCommand() { - this(GIT_COMMAND); + public GitBlameCommand(System2 system, ProcessWrapperFactory processWrapperFactory) { + this.system = system; + this.processWrapperFactory = processWrapperFactory; } - public GitBlameCommand(String gitCommand) { + GitBlameCommand(String gitCommand, System2 system, ProcessWrapperFactory processWrapperFactory) { this.gitCommand = gitCommand; + this.system = system; + this.processWrapperFactory = processWrapperFactory; } - public boolean isEnabled() { + /** + * This method must be executed before org.sonar.scm.git.GitBlameCommand#blame + * + * @return true, if native git is installed + */ + public boolean checkIfEnabled() { try { + this.gitCommand = locateDefaultGit(); MutableString stdOut = new MutableString(); - executeCommand(null, l -> stdOut.string = l, gitCommand, "--version"); + this.processWrapperFactory.create(null, l -> stdOut.string = l, gitCommand, "--version").execute(); return stdOut.string != null && stdOut.string.startsWith("git version"); } catch (Exception e) { LOG.debug("Failed to find git native client", e); @@ -73,10 +80,38 @@ public class GitBlameCommand { } } + private String locateDefaultGit() throws IOException { + if (this.gitCommand != null) { + return this.gitCommand; + } + // if not set fall back to defaults + if (system.isOsWindows()) { + return locateGitOnWindows(); + } + return DEFAULT_GIT_COMMAND; + } + + private String locateGitOnWindows() throws IOException { + // Windows will search current directory in addition to the PATH variable, which is unsecure. + // To avoid it we use where.exe to find git binary only in PATH. + LOG.debug("Looking for git command in the PATH using where.exe (Windows)"); + List whereCommandResult = new LinkedList<>(); + this.processWrapperFactory.create(null, whereCommandResult::add, "C:\\Windows\\System32\\where.exe", "$PATH:git.exe") + .execute(); + + if (!whereCommandResult.isEmpty()) { + String out = whereCommandResult.get(0).trim(); + LOG.debug("Found git.exe at {}", out); + return out; + } + throw new IllegalStateException("git.exe not found in PATH. PATH value was: " + system.property("PATH")); + } + public List blame(Path baseDir, String fileName) throws Exception { BlameOutputProcessor outputProcessor = new BlameOutputProcessor(); try { - executeCommand(baseDir, outputProcessor::process, gitCommand, BLAME_COMMAND, BLAME_LINE_PORCELAIN_FLAG, IGNORE_WHITESPACES, fileName); + this.processWrapperFactory.create(baseDir, outputProcessor::process, gitCommand, BLAME_COMMAND, BLAME_LINE_PORCELAIN_FLAG, IGNORE_WHITESPACES, fileName) + .execute(); } catch (UncommittedLineException e) { LOG.debug("Unable to blame file '{}' - it has uncommitted changes", fileName); return emptyList(); @@ -84,31 +119,6 @@ public class GitBlameCommand { return outputProcessor.getBlameLines(); } - private static void executeCommand(@Nullable Path baseDir, Consumer stdOutLineConsumer, String... command) throws Exception { - ProcessBuilder pb = new ProcessBuilder() - .command(command) - .directory(baseDir != null ? baseDir.toFile() : null); - - Process p = pb.start(); - try { - InputStream processStdOutput = p.getInputStream(); - // don't use BufferedReader#readLine because it will also parse CR, which may be part of the actual source code line - try (Scanner scanner = new Scanner(new InputStreamReader(processStdOutput, UTF_8))) { - scanner.useDelimiter("\n"); - while (scanner.hasNext()) { - stdOutLineConsumer.accept(scanner.next()); - } - } - - int exit = p.waitFor(); - if (exit != 0) { - throw new IllegalStateException(String.format("Command execution exited with code: %d", exit)); - } - } finally { - p.destroy(); - } - } - private static class BlameOutputProcessor { private final List blameLines = new LinkedList<>(); private String sha1 = null; @@ -124,11 +134,11 @@ public class GitBlameCommand { sha1 = line.split(" ")[0]; } else if (line.startsWith("\t")) { saveEntry(); - } else if (line.startsWith(COMITTER_TIME)) { - committerTime = line.substring(COMITTER_TIME.length()); - } else if (line.startsWith(COMITTER_MAIL)) { + } else if (line.startsWith(COMMITTER_TIME)) { + committerTime = line.substring(COMMITTER_TIME.length()); + } else if (line.startsWith(COMMITTER_MAIL)) { Matcher matcher = EMAIL_PATTERN.matcher(line); - if (!matcher.find(COMITTER_MAIL.length()) || matcher.groupCount() != 1) { + if (!matcher.find(COMMITTER_MAIL.length()) || matcher.groupCount() != 1) { throw new IllegalStateException("Couldn't parse committer email from: " + line); } committerMail = matcher.group(1); diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmSupport.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmSupport.java index a67bd56320d..65a8044dfbe 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmSupport.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmSupport.java @@ -33,6 +33,7 @@ public final class GitScmSupport { return Arrays.asList( JGitBlameCommand.class, CompositeBlameCommand.class, + ProcessWrapperFactory.class, GitBlameCommand.class, GitScmProvider.class, GitIgnoreCommand.class); diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/ProcessWrapperFactory.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/ProcessWrapperFactory.java new file mode 100644 index 00000000000..6d9c602d1c2 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/ProcessWrapperFactory.java @@ -0,0 +1,88 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.scm.git; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.Scanner; +import java.util.function.Consumer; +import javax.annotation.Nullable; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import static java.lang.String.format; +import static java.lang.String.join; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class ProcessWrapperFactory { + private static final Logger LOG = Loggers.get(ProcessWrapperFactory.class); + + public ProcessWrapperFactory() { + // nothing to do + } + + public ProcessWrapper create(@Nullable Path baseDir, Consumer stdOutLineConsumer, String... command) { + return new ProcessWrapper(baseDir, stdOutLineConsumer, command); + } + + static class ProcessWrapper { + + private final Path baseDir; + private final Consumer stdOutLineConsumer; + private final String[] command; + + ProcessWrapper(@Nullable Path baseDir, Consumer stdOutLineConsumer, String... command) { + this.baseDir = baseDir; + this.stdOutLineConsumer = stdOutLineConsumer; + this.command = command; + } + + public void execute() throws IOException { + ProcessBuilder pb = new ProcessBuilder() + .command(command) + .directory(baseDir != null ? baseDir.toFile() : null); + + Process p = pb.start(); + try { + InputStream processStdOutput = p.getInputStream(); + // don't use BufferedReader#readLine because it will also parse CR, which may be part of the actual source code line + try (Scanner scanner = new Scanner(new InputStreamReader(processStdOutput, UTF_8))) { + scanner.useDelimiter("\n"); + while (scanner.hasNext()) { + stdOutLineConsumer.accept(scanner.next()); + } + } + + int exit = p.waitFor(); + if (exit != 0) { + throw new IllegalStateException(format("Command execution exited with code: %d", exit)); + } + } catch (InterruptedException e) { + LOG.warn(format("Command [%s] interrupted", join(" ", command)), e); + Thread.currentThread().interrupt(); + } finally { + p.destroy(); + } + } + } + +} -- cgit v1.2.3