/*
* Copyright 2000-2015 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 com.intellij.diff.tools.external;
import com.intellij.diff.contents.*;
import com.intellij.diff.merge.MergeResult;
import com.intellij.diff.merge.ThreesideMergeRequest;
import com.intellij.diff.util.DiffUserDataKeysEx;
import com.intellij.diff.util.Side;
import com.intellij.diff.util.ThreeSide;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileWithoutContent;
import com.intellij.util.ArrayUtil;
import com.intellij.util.LineSeparator;
import com.intellij.util.PathUtil;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.hash.HashMap;
import com.intellij.util.execution.ParametersListUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class ExternalDiffToolUtil {
public static boolean canCreateFile(@NotNull DiffContent content) {
if (content instanceof EmptyContent) return true;
if (content instanceof DocumentContent) return true;
if (content instanceof FileContent) {
VirtualFile file = ((FileContent)content).getFile();
if (file instanceof VirtualFileWithoutContent) return false;
return true;
}
if (content instanceof DirectoryContent) return ((DirectoryContent)content).getFile().isInLocalFileSystem();
return false;
}
@NotNull
private static InputFile createFile(@NotNull DiffContent content, @NotNull FileNameInfo fileName)
throws IOException {
if (content instanceof EmptyContent) {
return new TempInputFile(createFile(new byte[0], fileName));
}
else if (content instanceof FileContent) {
VirtualFile file = ((FileContent)content).getFile();
Document document = FileDocumentManager.getInstance().getCachedDocument(file);
if (document != null) {
FileDocumentManager.getInstance().saveDocument(document);
}
if (file.isInLocalFileSystem()) {
return new LocalInputFile(file);
}
return new TempInputFile(createTempFile(file, fileName));
}
else if (content instanceof DocumentContent) {
return new TempInputFile(createTempFile((DocumentContent)content, fileName));
}
else if (content instanceof DirectoryContent) {
VirtualFile file = ((DirectoryContent)content).getFile();
if (file.isInLocalFileSystem()) {
return new LocalInputFile(file);
}
throw new IllegalArgumentException(content.toString());
}
throw new IllegalArgumentException(content.toString());
}
@NotNull
private static File createTempFile(@NotNull final DocumentContent content, @NotNull FileNameInfo fileName) throws IOException {
FileDocumentManager.getInstance().saveDocument(content.getDocument());
LineSeparator separator = content.getLineSeparator();
if (separator == null) separator = LineSeparator.getSystemLineSeparator();
Charset charset = content.getCharset();
if (charset == null) charset = Charset.defaultCharset();
Boolean hasBom = content.hasBom();
if (hasBom == null) hasBom = CharsetToolkit.getMandatoryBom(charset) != null;
String contentData = ReadAction.compute(() -> {
return content.getDocument().getText();
});
if (separator != LineSeparator.LF) {
contentData = StringUtil.convertLineSeparators(contentData, separator.getSeparatorString());
}
byte[] bytes = contentData.getBytes(charset);
byte[] bom = hasBom ? CharsetToolkit.getPossibleBom(charset) : null;
if (bom != null) {
bytes = ArrayUtil.mergeArrays(bom, bytes);
}
return createFile(bytes, fileName);
}
@NotNull
private static File createTempFile(@NotNull VirtualFile file, @NotNull FileNameInfo fileName) throws IOException {
byte[] bytes = file.contentsToByteArray();
return createFile(bytes, fileName);
}
@NotNull
private static OutputFile createOutputFile(@NotNull DiffContent content, @NotNull FileNameInfo fileName) throws IOException {
if (content instanceof FileContent) {
VirtualFile file = ((FileContent)content).getFile();
Document document = FileDocumentManager.getInstance().getCachedDocument(file);
if (document != null) {
FileDocumentManager.getInstance().saveDocument(document);
}
if (file.isInLocalFileSystem()) {
return new LocalOutputFile(file);
}
File tempFile = createTempFile(file, fileName);
return new NonLocalOutputFile(file, tempFile);
}
else if (content instanceof DocumentContent) {
File tempFile = createTempFile(((DocumentContent)content), fileName);
return new DocumentOutputFile(((DocumentContent)content).getDocument(), ((DocumentContent)content).getCharset(), tempFile);
}
throw new IllegalArgumentException(content.toString());
}
@NotNull
private static File createFile(@NotNull byte[] bytes, @NotNull FileNameInfo fileName) throws IOException {
File tempFile = FileUtil.createTempFile(fileName.prefix + "_", "_" + fileName.name, true);
FileUtil.writeToFile(tempFile, bytes);
return tempFile;
}
public static void execute(@NotNull ExternalDiffSettings settings,
@NotNull List<? extends DiffContent> contents,
@NotNull List<String> titles,
@Nullable String windowTitle)
throws IOException, ExecutionException {
assert contents.size() == 2 || contents.size() == 3;
assert titles.size() == contents.size();
List<InputFile> files = new ArrayList<>();
for (int i = 0; i < contents.size(); i++) {
DiffContent content = contents.get(i);
FileNameInfo fileName = FileNameInfo.create(contents, titles, windowTitle, i);
files.add(createFile(content, fileName));
}
Map<String, String> patterns = ContainerUtil.newHashMap();
if (files.size() == 2) {
patterns.put("%1", files.get(0).getPath());
patterns.put("%2", files.get(1).getPath());
patterns.put("%3", "");
}
else {
patterns.put("%1", files.get(0).getPath());
patterns.put("%2", files.get(2).getPath());
patterns.put("%3", files.get(1).getPath());
}
execute(settings.getDiffExePath(), settings.getDiffParameters(), patterns);
}
public static void executeMerge(@Nullable Project project,
@NotNull ExternalDiffSettings settings,
@NotNull ThreesideMergeRequest request)
throws IOException, ExecutionException {
boolean success = false;
OutputFile outputFile = null;
List<InputFile> inputFiles = new ArrayList<>();
try {
DiffContent outputContent = request.getOutputContent();
List<? extends DiffContent> contents = request.getContents();
List<String> titles = request.getContentTitles();
String windowTitle = request.getTitle();
assert contents.size() == 3;
assert titles.size() == contents.size();
for (int i = 0; i < contents.size(); i++) {
DiffContent content = contents.get(i);
FileNameInfo fileName = FileNameInfo.create(contents, titles, windowTitle, i);
inputFiles.add(createFile(content, fileName));
}
outputFile = createOutputFile(outputContent, FileNameInfo.createMergeResult(outputContent, windowTitle));
Map<String, String> patterns = new HashMap<>();
patterns.put("%1", inputFiles.get(0).getPath());
patterns.put("%2", inputFiles.get(2).getPath());
patterns.put("%3", inputFiles.get(1).getPath());
patterns.put("%4", outputFile.getPath());
final Process process = execute(settings.getMergeExePath(), settings.getMergeParameters(), patterns);
if (settings.isMergeTrustExitCode()) {
final Ref<Boolean> resultRef = new Ref<>();
ProgressManager.getInstance().run(new Task.Modal(project, "Waiting for External Tool", true) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
final Semaphore semaphore = new Semaphore(0);
final Thread waiter = new Thread("external process waiter") {
@Override
public void run() {
try {
resultRef.set(process.waitFor() == 0);
}
catch (InterruptedException ignore) {
}
finally {
semaphore.release();
}
}
};
waiter.start();
try {
while (true) {
indicator.checkCanceled();
if (semaphore.tryAcquire(200, TimeUnit.MILLISECONDS)) break;
}
}
catch (InterruptedException ignore) {
}
finally {
waiter.interrupt();
}
}
});
success = resultRef.get() == Boolean.TRUE;
}
else {
ProgressManager.getInstance().run(new Task.Modal(project, "Launching External Tool", false) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
indicator.setIndeterminate(true);
TimeoutUtil.sleep(1000);
}
});
success = Messages.showYesNoDialog(project,
"Press \"Mark as Resolved\" when you finish resolving conflicts in the external tool",
"Merge In External Tool", "Mark as Resolved", "Revert", null) == Messages.YES;
}
if (success) outputFile.apply();
}
finally {
request.applyResult(success ? MergeResult.RESOLVED : MergeResult.CANCEL);
if (outputFile != null) outputFile.cleanup();
for (InputFile file : inputFiles) {
file.cleanup();
}
}
}
@NotNull
private static Process execute(@NotNull String exePath, @NotNull String parametersTemplate, @NotNull Map<String, String> patterns)
throws ExecutionException {
List<String> parameters = ParametersListUtil.parse(parametersTemplate, true);
List<String> from = new ArrayList<>();
List<String> to = new ArrayList<>();
for (Map.Entry<String, String> entry : patterns.entrySet()) {
from.add(entry.getKey());
to.add(entry.getValue());
}
List<String> args = new ArrayList<>();
for (String parameter : parameters) {
String arg = StringUtil.replace(parameter, from, to);
if (!StringUtil.isEmptyOrSpaces(arg)) args.add(arg);
}
GeneralCommandLine commandLine = new GeneralCommandLine();
commandLine.setExePath(exePath);
commandLine.addParameters(args);
return commandLine.createProcess();
}
//
// Helpers
//
private interface InputFile {
@NotNull
String getPath();
void cleanup();
}
private interface OutputFile extends InputFile {
void apply() throws IOException;
}
private static class LocalOutputFile extends LocalInputFile implements OutputFile {
public LocalOutputFile(@NotNull VirtualFile file) {
super(file);
}
@Override
public void apply() {
myFile.refresh(false, false);
}
}
private static class NonLocalOutputFile extends TempInputFile implements OutputFile {
@NotNull private final VirtualFile myFile;
public NonLocalOutputFile(@NotNull VirtualFile file, @NotNull File localFile) {
super(localFile);
myFile = file;
}
@Override
public void apply() throws IOException {
myFile.setBinaryContent(FileUtil.loadFileBytes(myLocalFile));
}
}
private static class DocumentOutputFile extends TempInputFile implements OutputFile {
@NotNull private final Document myDocument;
@NotNull private final Charset myCharset;
public DocumentOutputFile(@NotNull Document document, @Nullable Charset charset, @NotNull File localFile) {
super(localFile);
myDocument = document;
// TODO: potentially dangerous operation - we're using default charset
myCharset = charset != null ? charset : Charset.defaultCharset();
}
@Override
public void apply() throws IOException {
final String content = StringUtil.convertLineSeparators(FileUtil.loadFile(myLocalFile, myCharset));
ApplicationManager.getApplication().runWriteAction(() -> {
myDocument.setText(content);
});
}
}
private static class LocalInputFile implements InputFile {
@NotNull protected final VirtualFile myFile;
public LocalInputFile(@NotNull VirtualFile file) {
myFile = file;
}
@NotNull
@Override
public String getPath() {
return myFile.getPath();
}
@Override
public void cleanup() {
}
}
private static class TempInputFile implements InputFile {
@NotNull protected final File myLocalFile;
public TempInputFile(@NotNull File localFile) {
myLocalFile = localFile;
}
@NotNull
@Override
public String getPath() {
return myLocalFile.getPath();
}
@Override
public void cleanup() {
FileUtil.delete(myLocalFile);
}
}
private static class FileNameInfo {
@NotNull public final String prefix;
@NotNull public final String name;
public FileNameInfo(@NotNull String prefix, @NotNull String name) {
this.prefix = prefix;
this.name = name;
}
@NotNull
public static FileNameInfo create(@NotNull List<? extends DiffContent> contents,
@NotNull List<String> titles,
@Nullable String windowTitle,
int index) {
if (contents.size() == 2) {
Side side = Side.fromIndex(index);
DiffContent content = side.select(contents);
String title = side.select(titles);
String prefix = side.select("before", "after");
String name = getFileName(content, title, windowTitle);
return new FileNameInfo(prefix, name);
}
else if (contents.size() == 3) {
ThreeSide side = ThreeSide.fromIndex(index);
DiffContent content = side.select(contents);
String title = side.select(titles);
String prefix = side.select("left", "base", "right");
String name = getFileName(content, title, windowTitle);
return new FileNameInfo(prefix, name);
}
else {
throw new IllegalArgumentException(String.valueOf(contents.size()));
}
}
@NotNull
public static FileNameInfo createMergeResult(@NotNull DiffContent content, @Nullable String windowTitle) {
String name = getFileName(content, null, windowTitle);
return new FileNameInfo("merge_result", name);
}
@NotNull
private static String getFileName(@NotNull DiffContent content,
@Nullable String title,
@Nullable String windowTitle) {
if (content instanceof EmptyContent) {
return "no_content.tmp";
}
String fileName = content.getUserData(DiffUserDataKeysEx.FILE_NAME);
if (fileName == null && content instanceof DocumentContent) {
VirtualFile highlightFile = ((DocumentContent)content).getHighlightFile();
fileName = highlightFile != null ? highlightFile.getName() : null;
}
if (fileName == null && content instanceof FileContent) {
fileName = ((FileContent)content).getFile().getName();
}
if (!StringUtil.isEmptyOrSpaces(fileName)) {
return fileName;
}
FileType fileType = content.getContentType();
String ext = fileType != null ? fileType.getDefaultExtension() : null;
if (StringUtil.isEmptyOrSpaces(ext)) ext = "tmp";
String name = "";
if (title != null && windowTitle != null) {
name = title + "_" + windowTitle;
}
else if (title != null || windowTitle != null) {
name = title != null ? title : windowTitle;
}
if (name.length() > 50) name = name.substring(0, 50);
return PathUtil.suggestFileName(name + "." + ext, true, false);
}
}
}