-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SLCORE-1133: Rely on native Git if possible for blaming (#1228)
--------- Co-authored-by: Nicolas Quinquenel <nicolas.quinquenel@sonarsource.com> Co-authored-by: Kirill Knize <kirill.knize@sonarsource.com>
- Loading branch information
1 parent
67fe16c
commit d366685
Showing
10 changed files
with
468 additions
and
14 deletions.
There are no files selected for viewing
85 changes: 85 additions & 0 deletions
85
...nd/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/BlameParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/* | ||
* SonarLint Core - Commons | ||
* Copyright (C) 2016-2025 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.sonarsource.sonarlint.core.commons.util.git; | ||
|
||
import java.io.BufferedReader; | ||
import java.io.IOException; | ||
import java.io.StringReader; | ||
import java.nio.file.Path; | ||
import java.time.Instant; | ||
import java.time.temporal.ChronoUnit; | ||
import java.util.Date; | ||
import java.util.regex.Pattern; | ||
import org.sonar.scm.git.blame.BlameResult; | ||
import org.sonarsource.sonarlint.core.commons.SonarLintBlameResult; | ||
|
||
public class BlameParser { | ||
|
||
private static final String FILENAME = "filename "; | ||
private static final String AUTHOR_MAIL = "author-mail "; | ||
private static final String COMMITTER_TIME = "committer-time "; | ||
// if this text change between different git versions it will break the implementation | ||
private static final String NOT_COMMITTED = "<not.committed.yet>"; | ||
private BlameParser() { | ||
// Utility class | ||
} | ||
|
||
public static SonarLintBlameResult parseBlameOutput(String blameOutput, String currentFilePath, Path projectBaseDir) throws IOException { | ||
var blameResult = new BlameResult(); | ||
var currentFileBlame = new BlameResult.FileBlame(currentFilePath, countCommitterTimeOccurrences(blameOutput)); | ||
blameResult.getFileBlameByPath().put(currentFilePath, currentFileBlame); | ||
var fileSections = blameOutput.split(FILENAME); | ||
var currentLineNumber = 0; | ||
|
||
for (var fileSection : fileSections) { | ||
if (fileSection.isBlank()) { | ||
continue; | ||
} | ||
|
||
try (var reader = new BufferedReader(new StringReader(fileSection))) { | ||
String line; | ||
while ((line = reader.readLine()) != null) { | ||
if (line.startsWith(AUTHOR_MAIL)) { | ||
var authorEmail = line.substring(AUTHOR_MAIL.length()); | ||
currentFileBlame.getAuthorEmails()[currentLineNumber] = authorEmail; | ||
} | ||
if (line.startsWith(COMMITTER_TIME) && !currentFileBlame.getAuthorEmails()[currentLineNumber].equals(NOT_COMMITTED)) { | ||
var committerTime = line.substring(COMMITTER_TIME.length()); | ||
var commitDate = Date.from(Instant.ofEpochSecond(Long.parseLong(committerTime)).truncatedTo(ChronoUnit.SECONDS)); | ||
currentFileBlame.getCommitDates()[currentLineNumber] = commitDate; | ||
} | ||
} | ||
} | ||
currentLineNumber++; | ||
} | ||
|
||
return new SonarLintBlameResult(blameResult, projectBaseDir); | ||
} | ||
|
||
public static int countCommitterTimeOccurrences(String blameOutput) { | ||
var pattern = Pattern.compile("^" + COMMITTER_TIME, Pattern.MULTILINE); | ||
var matcher = pattern.matcher(blameOutput); | ||
var count = 0; | ||
while (matcher.find()) { | ||
count++; | ||
} | ||
return count; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
.../src/main/java/org/sonarsource/sonarlint/core/commons/util/git/ProcessWrapperFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/* | ||
* SonarLint Core - Commons | ||
* Copyright (C) 2016-2025 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.sonarsource.sonarlint.core.commons.util.git; | ||
|
||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.InputStreamReader; | ||
import java.nio.file.Path; | ||
import java.util.HashMap; | ||
import java.util.LinkedList; | ||
import java.util.Map; | ||
import java.util.Scanner; | ||
import java.util.function.Consumer; | ||
import javax.annotation.Nullable; | ||
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; | ||
|
||
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 SonarLintLogger LOG = SonarLintLogger.get(); | ||
|
||
public ProcessWrapperFactory() { | ||
// nothing to do | ||
} | ||
|
||
public ProcessWrapper create(@Nullable Path baseDir, Consumer<String> stdOutLineConsumer, String... command) { | ||
return new ProcessWrapper(baseDir, stdOutLineConsumer, Map.of(), command); | ||
} | ||
|
||
public ProcessWrapper create(@Nullable Path baseDir, Consumer<String> stdOutLineConsumer, Map<String, String> envVariables, String... command) { | ||
return new ProcessWrapper(baseDir, stdOutLineConsumer, envVariables, command); | ||
} | ||
|
||
static class ProcessWrapper { | ||
|
||
private final Path baseDir; | ||
private final Consumer<String> stdOutLineConsumer; | ||
private final String[] command; | ||
private final Map<String, String> envVariables = new HashMap<>(); | ||
|
||
ProcessWrapper(@Nullable Path baseDir, Consumer<String> stdOutLineConsumer, Map<String, String> envVariables, String... command) { | ||
this.baseDir = baseDir; | ||
this.stdOutLineConsumer = stdOutLineConsumer; | ||
this.envVariables.putAll(envVariables); | ||
this.command = command; | ||
} | ||
|
||
private static void processInputStream(InputStream inputStream, Consumer<String> stringConsumer) { | ||
try (var scanner = new Scanner(new InputStreamReader(inputStream, UTF_8))) { | ||
scanner.useDelimiter("\n"); | ||
while (scanner.hasNext()) { | ||
stringConsumer.accept(scanner.next()); | ||
} | ||
} | ||
} | ||
|
||
private static boolean isNotAGitRepo(LinkedList<String> output) { | ||
return output.stream().anyMatch(line -> line.contains("not a git repository")); | ||
} | ||
|
||
public void execute() throws IOException { | ||
var output = new LinkedList<String>(); | ||
var processBuilder = new ProcessBuilder() | ||
.command(command) | ||
.directory(baseDir != null ? baseDir.toFile() : null); | ||
envVariables.forEach(processBuilder.environment()::put); | ||
processBuilder.redirectInput(ProcessBuilder.Redirect.PIPE); | ||
processBuilder.redirectOutput(ProcessBuilder.Redirect.PIPE); | ||
processBuilder.redirectError(ProcessBuilder.Redirect.PIPE); | ||
|
||
var p = processBuilder.start(); | ||
try { | ||
processInputStream(p.getInputStream(), line -> { | ||
output.add(line); | ||
stdOutLineConsumer.accept(line); | ||
}); | ||
|
||
processInputStream(p.getErrorStream(), line -> { | ||
if (!line.isBlank()) { | ||
output.add(line); | ||
LOG.debug(line); | ||
} | ||
}); | ||
|
||
int exit = p.waitFor(); | ||
if (exit != 0) { | ||
if (isNotAGitRepo(output)) { | ||
var dirStr = baseDir != null ? baseDir.toString() : "null"; | ||
throw new GitRepoNotFoundException(dirStr); | ||
} | ||
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(); | ||
} | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.