[hg] parse hg log output line-by-line

This commit is contained in:
Julia Beliaeva
2017-08-07 17:52:49 +03:00
parent 625716a245
commit 2869ec0017
8 changed files with 295 additions and 87 deletions
@@ -149,12 +149,13 @@ public class VcsFileUtil {
// to avoid empty chunks
rc.add(arguments.subList(i, i + groupSize));
start = i + groupSize;
size = 0;
}
else {
rc.add(arguments.subList(start, i));
start = i;
size = length;
}
size = 0;
}
else {
size += length;
@@ -25,6 +25,7 @@ import org.zmlx.hg4idea.HgVcs;
import org.zmlx.hg4idea.execution.HgCommandException;
import org.zmlx.hg4idea.execution.HgCommandExecutor;
import org.zmlx.hg4idea.execution.HgCommandResult;
import org.zmlx.hg4idea.execution.HgLineProcessListener;
import org.zmlx.hg4idea.log.HgBaseLogParser;
import org.zmlx.hg4idea.log.HgFileRevisionLogParser;
import org.zmlx.hg4idea.log.HgHistoryUtil;
@@ -96,13 +97,12 @@ public class HgLogCommand {
HgFile originalHgFile = new HgFile(hgFile.getRepo(), originalFileName);
HgCommandResult result = execute(hgFile.getRepo(), template, limit, originalHgFile, argsForCmd);
return HgHistoryUtil.getCommitRecords(myProject, result,
new HgFileRevisionLogParser(myProject, originalHgFile, myVersion));
return HgHistoryUtil.getCommitRecords(myProject, result,
new HgFileRevisionLogParser(myProject, originalHgFile, myVersion));
}
@Nullable
public HgCommandResult execute(@NotNull VirtualFile repo, @NotNull String template, int limit, @Nullable HgFile hgFile,
@Nullable List<String> argsForCmd) {
@NotNull
private List<String> createArguments(@NotNull String template, int limit, @Nullable HgFile hgFile, @Nullable List<String> argsForCmd) {
List<String> arguments = new LinkedList<>();
if (myIncludeRemoved) {
// There is a bug in mercurial that causes --follow --removed <file> to cause
@@ -134,9 +134,23 @@ public class HgLogCommand {
if (myLogFile && hgFile != null) {
arguments.add(hgFile.getRelativePath());
}
return arguments;
}
@Nullable
public HgCommandResult execute(@NotNull VirtualFile repo, @NotNull String template, int limit, @Nullable HgFile hgFile,
@Nullable List<String> argsForCmd) {
List<String> arguments = createArguments(template, limit, hgFile, argsForCmd);
HgCommandExecutor commandExecutor = new HgCommandExecutor(myProject);
commandExecutor.setOutputAlwaysSuppressed(true);
return commandExecutor.executeInCurrentThread(repo, "log", arguments);
}
public void execute(@NotNull VirtualFile repo, @NotNull String template, int limit, @Nullable HgFile hgFile,
@Nullable List<String> argsForCmd, @NotNull HgLineProcessListener listener) {
List<String> arguments = createArguments(template, limit, hgFile, argsForCmd);
HgCommandExecutor commandExecutor = new HgCommandExecutor(myProject);
commandExecutor.setOutputAlwaysSuppressed(true);
commandExecutor.executeInCurrentThread(repo, "log", arguments, listener);
}
}
@@ -20,6 +20,7 @@ import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.SystemProperties;
import com.intellij.vcsUtil.VcsImplUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -153,6 +154,38 @@ public class HgCommandExecutor {
}
}
public void executeInCurrentThread(@Nullable VirtualFile repo,
@NotNull String operation,
@Nullable List<String> arguments,
@NotNull HgLineProcessListener listener) {
executeInCurrentThreadAndLog(repo, operation, arguments, listener);
if (HgErrorUtil.isUnknownEncodingError(StringUtil.split(listener.getErrorOutput().toString(), SystemProperties.getLineSeparator()))) {
setCharset(Charset.forName("utf8"));
executeInCurrentThreadAndLog(repo, operation, arguments, listener);
}
}
public void executeInCurrentThreadAndLog(@Nullable VirtualFile repo,
@NotNull String operation,
@Nullable List<String> arguments,
@NotNull HgLineProcessListener listener) {
if (myProject == null || myProject.isDisposed() || myVcs == null) return;
ShellCommand shellCommand = createShellCommandWithArgs(repo, operation, arguments, false);
try {
long startTime = System.currentTimeMillis();
LOG.debug(String.format("hg %s started", operation));
shellCommand.execute(myShowOutput, listener);
LOG.debug(String.format("hg %s finished. Took %s ms", operation, System.currentTimeMillis() - startTime));
}
catch (ShellCommandException e) {
processError(e);
}
catch (InterruptedException e) { // this may happen during project closing, no need to notify the user.
LOG.info(e.getMessage(), e);
}
}
private void processError(@NotNull ShellCommandException e) {
if (myVcs.getExecutableValidator().checkExecutableAndNotifyIfNeeded()) {
// if the problem was not with invalid executable - show error.
@@ -203,7 +236,7 @@ public class HgCommandExecutor {
final int lastSlashIndex = settings.getHgExecutable().lastIndexOf(File.separator);
exeName = settings.getHgExecutable().substring(lastSlashIndex + 1);
String str = String.format("%s %s %s", exeName, operation, arguments == null ? "" : StringUtil.join(arguments, " "));
String str = String.format("%s %s %s", exeName, operation, arguments == null ? "" : StringUtil.escapeStringCharacters(StringUtil.join(arguments, " ")));
//remove password from path before log
final String cmdString = myDestination != null ? HgUtil.removePasswordIfNeeded(str) : str;
// log command
@@ -0,0 +1,52 @@
/*
* Copyright 2000-2017 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
*
* http://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 org.zmlx.hg4idea.execution;
import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vcs.VcsException;
import org.jetbrains.annotations.NotNull;
public abstract class HgLineProcessListener {
@NotNull private final StringBuilder myErrorOutput = new StringBuilder();
private int myExitCode;
public void onLineAvailable(String line, Key outputType) {
if (ProcessOutputTypes.STDOUT == outputType) {
processOutputLine(line);
}
else if (ProcessOutputTypes.STDERR == outputType) {
myErrorOutput.append(line).append("\n");
}
}
protected abstract void processOutputLine(@NotNull String line);
@NotNull
public StringBuilder getErrorOutput() {
return myErrorOutput;
}
public void finish() throws VcsException {
if (myExitCode != 0 && myErrorOutput.length() != 0) {
throw new VcsException(myErrorOutput.toString());
}
}
public void setExitCode(int exitCode) {
myExitCode = exitCode;
}
}
@@ -90,4 +90,40 @@ public final class ShellCommand {
throw new ShellCommandException(e);
}
}
public void execute(boolean showTextOnIndicator, @NotNull HgLineProcessListener listener)
throws ShellCommandException, InterruptedException {
final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
try {
OSProcessHandler processHandler = new OSProcessHandler(myCommandLine);
ProcessAdapter outputAdapter = new ProcessAdapter() {
@Override
public void onTextAvailable(ProcessEvent event, Key outputType) {
for (String line : LineHandlerHelper.splitText(event.getText())) {
if (ProcessOutputTypes.STDOUT == outputType && indicator != null && showTextOnIndicator) {
indicator.setText2(line);
}
listener.onLineAvailable(line, outputType);
}
}
@Override
public void processTerminated(ProcessEvent event) {
listener.setExitCode(event.getExitCode());
}
};
processHandler.addProcessListener(outputAdapter);
processHandler.startNotify();
while (!processHandler.waitFor(300)) {
if (indicator != null && indicator.isCanceled()) {
processHandler.destroyProcess();
listener.setExitCode(255);
break;
}
}
}
catch (ExecutionException e) {
throw new ShellCommandException(e);
}
}
}
@@ -28,10 +28,7 @@ import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.changes.CurrentContentRevision;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.SmartList;
import com.intellij.util.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcs.log.*;
import com.intellij.vcsUtil.VcsFileUtil;
@@ -41,6 +38,7 @@ import org.jetbrains.annotations.Nullable;
import org.zmlx.hg4idea.*;
import org.zmlx.hg4idea.command.HgLogCommand;
import org.zmlx.hg4idea.execution.HgCommandResult;
import org.zmlx.hg4idea.execution.HgLineProcessListener;
import org.zmlx.hg4idea.provider.HgChangeProvider;
import org.zmlx.hg4idea.util.HgChangesetUtil;
import org.zmlx.hg4idea.util.HgUtil;
@@ -95,14 +93,6 @@ public class HgHistoryUtil {
return getCommitRecords(project, result, baseParser);
}
@NotNull
public static List<? extends VcsFullCommitDetails> history(@NotNull final Project project,
@NotNull final VirtualFile root,
int limit,
@NotNull List<String> hashParameters) throws VcsException {
return history(project, root, limit, hashParameters, false);
}
/**
* <p>Get & parse hg log detailed output with commits, their parents and their changes.
* For null destination return log command result</p>
@@ -111,25 +101,40 @@ public class HgHistoryUtil {
* and it can occupy too much memory. The estimate is ~600Kb for 1000 commits.</p>
*/
@NotNull
public static List<? extends VcsFullCommitDetails> history(@NotNull final Project project,
@NotNull final VirtualFile root, final int limit,
@NotNull List<String> hashParameters, final boolean silent)
public static List<? extends VcsFullCommitDetails> history(@NotNull Project project, @NotNull VirtualFile root, int limit,
@NotNull List<String> hashParameters, boolean silent)
throws VcsException {
HgVcs hgvcs = HgVcs.getInstance(project);
assert hgvcs != null;
final HgVersion version = hgvcs.getVersion();
final String[] templates = HgBaseLogParser.constructFullTemplateArgument(true, version);
HgVersion version = hgvcs.getVersion();
String[] templates = HgBaseLogParser.constructFullTemplateArgument(true, version);
return VcsFileUtil.foreachChunk(hashParameters, 2,
strings -> {
HgCommandResult logResult =
getLogResult(project, root, version, limit, strings, HgChangesetUtil.makeTemplate(templates));
if (logResult == null) return Collections.emptyList();
if (!logResult.getErrorLines().isEmpty()) throw new VcsException(logResult.getRawError());
return createFullCommitsFromResult(project, root, logResult, version, silent);
});
ArrayList<VcsFullCommitDetails> result = ContainerUtil.newArrayList();
VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(project);
if (factory == null) {
return Collections.emptyList();
}
HgFileRevisionLogParser parser = new HgFileRevisionLogParser(project, getOriginalHgFile(project, root), hgvcs.getVersion());
try {
readLog(project, root, hgvcs.getVersion(), limit, hashParameters, HgChangesetUtil.makeTemplate(templates),
stringBuilder -> {
HgFileRevision revision = parser.convert(stringBuilder.toString());
if (revision != null) {
result.add(createDetails(project, root, factory, revision));
}
});
}
catch (VcsException e) {
if (!silent) {
VcsNotifier.getInstance(project).notifyError(HgVcsMessages.message("hg4idea.error.log.command.execution"), e.getMessage());
}
throw e;
}
return result;
}
@NotNull
public static List<? extends VcsFullCommitDetails> createFullCommitsFromResult(@NotNull Project project,
@NotNull VirtualFile root,
@Nullable HgCommandResult result,
@@ -142,58 +147,89 @@ public class HgHistoryUtil {
getCommitRecords(project, result, new HgFileRevisionLogParser(project, getOriginalHgFile(project, root), version), silent);
List<VcsFullCommitDetails> vcsFullCommitDetailsList = new ArrayList<>();
for (HgFileRevision revision : hgRevisions) {
HgRevisionNumber vcsRevisionNumber = revision.getRevisionNumber();
List<HgRevisionNumber> parents = vcsRevisionNumber.getParents();
HgRevisionNumber firstParent = parents.isEmpty() ? null : parents.get(0); // can have no parents if it is a root
List<Hash> parentsHash = new SmartList<>();
for (HgRevisionNumber parent : parents) {
parentsHash.add(factory.createHash(parent.getChangeset()));
}
final Collection<Change> changes = new ArrayList<>();
for (String file : revision.getModifiedFiles()) {
changes.add(createChange(project, root, file, firstParent, file, vcsRevisionNumber, FileStatus.MODIFIED));
}
for (String file : revision.getAddedFiles()) {
changes.add(createChange(project, root, null, null, file, vcsRevisionNumber, FileStatus.ADDED));
}
for (String file : revision.getDeletedFiles()) {
changes.add(createChange(project, root, file, firstParent, null, vcsRevisionNumber, FileStatus.DELETED));
}
for (Map.Entry<String, String> copiedFile : revision.getMovedFiles().entrySet()) {
changes.add(createChange(project, root, copiedFile.getKey(), firstParent, copiedFile.getValue(), vcsRevisionNumber,
HgChangeProvider.RENAMED));
}
vcsFullCommitDetailsList.add(factory.createFullDetails(factory.createHash(vcsRevisionNumber.getChangeset()), parentsHash,
revision.getRevisionDate().getTime(), root,
vcsRevisionNumber.getSubject(),
vcsRevisionNumber.getName(), vcsRevisionNumber.getEmail(),
vcsRevisionNumber.getCommitMessage(), vcsRevisionNumber.getName(),
vcsRevisionNumber.getEmail(), revision.getRevisionDate().getTime(),
() -> changes
));
vcsFullCommitDetailsList.add(createDetails(project, root, factory, revision));
}
return vcsFullCommitDetailsList;
}
@NotNull
public static VcsFullCommitDetails createDetails(@NotNull Project project,
@NotNull VirtualFile root,
@NotNull VcsLogObjectsFactory factory,
@NotNull HgFileRevision revision) {
HgRevisionNumber vcsRevisionNumber = revision.getRevisionNumber();
List<HgRevisionNumber> parents = vcsRevisionNumber.getParents();
HgRevisionNumber firstParent = parents.isEmpty() ? null : parents.get(0); // can have no parents if it is a root
List<Hash> parentsHash = new SmartList<>();
for (HgRevisionNumber parent : parents) {
parentsHash.add(factory.createHash(parent.getChangeset()));
}
final Collection<Change> changes = new ArrayList<>();
for (String file : revision.getModifiedFiles()) {
changes.add(createChange(project, root, file, firstParent, file, vcsRevisionNumber, FileStatus.MODIFIED));
}
for (String file : revision.getAddedFiles()) {
changes.add(createChange(project, root, null, null, file, vcsRevisionNumber, FileStatus.ADDED));
}
for (String file : revision.getDeletedFiles()) {
changes.add(createChange(project, root, file, firstParent, null, vcsRevisionNumber, FileStatus.DELETED));
}
for (Map.Entry<String, String> copiedFile : revision.getMovedFiles().entrySet()) {
changes.add(createChange(project, root, copiedFile.getKey(), firstParent, copiedFile.getValue(), vcsRevisionNumber,
HgChangeProvider.RENAMED));
}
return factory.createFullDetails(factory.createHash(vcsRevisionNumber.getChangeset()), parentsHash,
revision.getRevisionDate().getTime(), root,
vcsRevisionNumber.getSubject(),
vcsRevisionNumber.getName(), vcsRevisionNumber.getEmail(),
vcsRevisionNumber.getCommitMessage(), vcsRevisionNumber.getName(),
vcsRevisionNumber.getEmail(), revision.getRevisionDate().getTime(),
() -> changes);
}
@Nullable
public static HgCommandResult getLogResult(@NotNull final Project project,
@NotNull final VirtualFile root, @NotNull HgVersion version, int limit,
@NotNull List<String> parameters, @NotNull String template) {
HgFile originalHgFile = getOriginalHgFile(project, root);
HgLogCommand hgLogCommand = new HgLogCommand(project);
List<String> args = new ArrayList<>(parameters);
hgLogCommand.setLogFile(false);
List<String> args = new ArrayList<>(parameters);
if (!version.isParentRevisionTemplateSupported()) {
args.add("--debug");
}
return hgLogCommand.execute(root, template, limit, originalHgFile, args);
return hgLogCommand.execute(root, template, limit, getOriginalHgFile(project, root), args);
}
public static HgFile getOriginalHgFile(Project project, VirtualFile root) {
public static void readLog(@NotNull Project project, @NotNull VirtualFile root, @NotNull HgVersion version, int limit,
@NotNull List<String> hashes, @NotNull String template, @NotNull Consumer<StringBuilder> consumer)
throws VcsException {
HgLogCommand hgLogCommand = new HgLogCommand(project);
hgLogCommand.setLogFile(false);
ThrowableConsumer<List<String>, VcsException> logRunner = hashesChunk -> {
HgLogOutputSplitter splitter = new HgLogOutputSplitter(consumer);
List<String> args = new ArrayList<>(hashesChunk);
if (!version.isParentRevisionTemplateSupported()) {
args.add("--debug");
}
hgLogCommand.execute(root, template, limit, getOriginalHgFile(project, root), args, splitter);
splitter.finish();
};
if (hashes.isEmpty()) {
// no hashes provided means need to read the whole thing
logRunner.consume(hashes);
}
else {
VcsFileUtil.foreachChunk(hashes, 2, logRunner);
}
}
public static HgFile getOriginalHgFile(@NotNull Project project, @NotNull VirtualFile root) {
HgFile hgFile = new HgFile(root, VcsUtil.getFilePath(root.getPath()));
if (project.isDisposed()) {
return hgFile;
@@ -316,7 +352,8 @@ public class HgHistoryUtil {
});
}
private static VcsLogObjectsFactory getObjectsFactoryWithDisposeCheck(Project project) {
@Nullable
static VcsLogObjectsFactory getObjectsFactoryWithDisposeCheck(Project project) {
if (!project.isDisposed()) {
return ServiceManager.getService(project, VcsLogObjectsFactory.class);
}
@@ -388,4 +425,34 @@ public class HgHistoryUtil {
public static String prepareParameter(String paramName, String value) {
return "--" + paramName + "=" + value; // no value escaping needed, because the parameter itself will be quoted by GeneralCommandLine
}
private static class HgLogOutputSplitter extends HgLineProcessListener {
@NotNull private final StringBuilder myOutput;
private final Consumer<StringBuilder> myConsumer;
public HgLogOutputSplitter(Consumer<StringBuilder> consumer) {
myConsumer = consumer;
myOutput = new StringBuilder();
}
@Override
protected void processOutputLine(@NotNull String line) {
int separatorIndex;
while ((separatorIndex = line.indexOf(HgChangesetUtil.CHANGESET_SEPARATOR)) >= 0) {
myOutput.append(line.substring(0, separatorIndex));
myConsumer.consume(myOutput);
myOutput.setLength(0); // maybe also call myOutput.trimToSize() to free some memory ?
line = line.substring(separatorIndex + 1);
}
myOutput.append(line);
}
public void finish() throws VcsException {
super.finish();
if (myOutput.length() != 0) {
myConsumer.consume(myOutput);
myOutput.setLength(0);
}
}
}
}
@@ -35,20 +35,21 @@ import com.intellij.vcs.log.util.UserNameRegex;
import com.intellij.vcs.log.util.VcsUserUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.zmlx.hg4idea.HgFileRevision;
import org.zmlx.hg4idea.HgNameWithHashInfo;
import org.zmlx.hg4idea.HgUpdater;
import org.zmlx.hg4idea.HgVcs;
import org.zmlx.hg4idea.execution.HgCommandResult;
import org.zmlx.hg4idea.repo.HgConfig;
import org.zmlx.hg4idea.repo.HgRepository;
import org.zmlx.hg4idea.repo.HgRepositoryManager;
import org.zmlx.hg4idea.util.HgChangesetUtil;
import org.zmlx.hg4idea.util.HgUtil;
import org.zmlx.hg4idea.util.HgVersion;
import java.text.SimpleDateFormat;
import java.util.*;
import static org.zmlx.hg4idea.log.HgHistoryUtil.getObjectsFactoryWithDisposeCheck;
import static org.zmlx.hg4idea.log.HgHistoryUtil.getOriginalHgFile;
import static org.zmlx.hg4idea.util.HgUtil.HEAD_REFERENCE;
import static org.zmlx.hg4idea.util.HgUtil.TIP_REFERENCE;
@@ -103,18 +104,25 @@ public class HgLogProvider implements VcsLogProvider {
// parameter fast is currently not used
// since this method is not called from index yet, fast always is false
// but when implementing indexing mercurial commits, we'll need to avoid rename/move detection when fast = true
// also we'll need to process commits one by one
HgVcs hgvcs = HgVcs.getInstance(myProject);
assert hgvcs != null;
final HgVersion version = hgvcs.getVersion();
final String[] templates = HgBaseLogParser.constructFullTemplateArgument(true, version);
String[] templates = HgBaseLogParser.constructFullTemplateArgument(true, hgvcs.getVersion());
VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(myProject);
if (factory == null) {
return;
}
HgCommandResult logResult = HgHistoryUtil.getLogResult(myProject, root, version, -1,
HgHistoryUtil.prepareHashes(hashes), HgChangesetUtil.makeTemplate(templates));
if (logResult == null) return;
if (!logResult.getErrorLines().isEmpty()) throw new VcsException(logResult.getRawError());
HgHistoryUtil.createFullCommitsFromResult(myProject, root, logResult, version, false).forEach(commitConsumer::consume);
HgFileRevisionLogParser parser = new HgFileRevisionLogParser(myProject, getOriginalHgFile(myProject, root), hgvcs.getVersion());
HgHistoryUtil.readLog(myProject, root, hgvcs.getVersion(), -1,
HgHistoryUtil.prepareHashes(hashes),
HgChangesetUtil.makeTemplate(templates),
stringBuilder -> {
HgFileRevision revision = parser.convert(stringBuilder.toString());
if (revision != null) {
commitConsumer.consume(HgHistoryUtil.createDetails(myProject, root, factory, revision));
}
});
}
@NotNull
@@ -124,12 +132,6 @@ public class HgLogProvider implements VcsLogProvider {
return HgHistoryUtil.readMiniDetails(myProject, root, hashes);
}
@NotNull
@Override
public List<? extends VcsFullCommitDetails> readFullDetails(@NotNull VirtualFile root, @NotNull List<String> hashes) throws VcsException {
return HgHistoryUtil.history(myProject, root, -1, HgHistoryUtil.prepareHashes(hashes));
}
@NotNull
private Set<VcsRef> readAllRefs(@NotNull VirtualFile root) throws VcsException {
if (myProject.isDisposed()) {
@@ -122,7 +122,10 @@ public final class HgErrorUtil {
if (result == null) {
return false;
}
List<String> errorLines = result.getErrorLines();
return isUnknownEncodingError(result.getErrorLines());
}
public static boolean isUnknownEncodingError(@NotNull List<String> errorLines) {
if (errorLines.isEmpty()) {
return false;
}