IDEA-104806 'Error creating patch: null' fix; binary content revisions implemented for hg vcs

This commit is contained in:
Nadya Zabrodina
2015-05-27 22:50:25 +04:00
parent 8b2db92b53
commit f81d50346c
12 changed files with 134 additions and 84 deletions
@@ -0,0 +1,37 @@
/*
* 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 org.zmlx.hg4idea;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.BinaryContentRevision;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class HgBinaryContentRevision extends HgContentRevision implements BinaryContentRevision {
public HgBinaryContentRevision(Project project,
@NotNull HgFile hgFile,
@NotNull HgRevisionNumber revisionNumber) {
super(project, hgFile, revisionNumber);
}
@Nullable
@Override
public byte[] getBinaryContent() throws VcsException {
return getContentAsBytes();
}
}
@@ -14,18 +14,19 @@ package org.zmlx.hg4idea;
import com.google.common.base.Objects;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.util.Throwable2Computable;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vcs.impl.ContentRevisionCache;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.zmlx.hg4idea.command.HgCatCommand;
import org.zmlx.hg4idea.execution.HgCommandResult;
import org.zmlx.hg4idea.util.HgUtil;
import java.io.UnsupportedEncodingException;
import java.io.IOException;
public class HgContentRevision implements ContentRevision {
@@ -34,44 +35,65 @@ public class HgContentRevision implements ContentRevision {
@NotNull private final HgRevisionNumber myRevisionNumber;
private FilePath filePath;
private String content;
public HgContentRevision(Project project, @NotNull HgFile hgFile, @NotNull HgRevisionNumber revisionNumber) {
protected HgContentRevision(Project project, @NotNull HgFile hgFile, @NotNull HgRevisionNumber revisionNumber) {
myProject = project;
myHgFile = hgFile;
myRevisionNumber = revisionNumber;
}
@Nullable
public String getContent() throws VcsException {
if (StringUtil.isEmptyOrSpaces(content)) {
if (myRevisionNumber.isWorkingVersion()) {
content = VcsUtil.getFileContent(myHgFile.getFile().getPath());
} else {
HgFile fileToCat = HgUtil.getFileNameInTargetRevision(myProject, myRevisionNumber, myHgFile);
content = new HgCatCommand(myProject).execute(fileToCat, myRevisionNumber, getFile().getCharset());
}
}
return content;
@NotNull
public static HgContentRevision create(Project project, @NotNull HgFile hgFile, @NotNull HgRevisionNumber revisionNumber) {
return !hgFile.toFilePath().getFileType().isBinary()
? new HgContentRevision(project, hgFile, revisionNumber)
: new HgBinaryContentRevision(project, hgFile, revisionNumber);
}
/**
* A wrapper for getContent(), that just converts String to byte[]
*/
@Nullable
@Override
public String getContent() throws VcsException {
if (myRevisionNumber.isWorkingVersion()) return VcsUtil.getFileContent(myHgFile.getFile().getPath());
final HgFile fileToCat = HgUtil.getFileNameInTargetRevision(myProject, myRevisionNumber, myHgFile);
FilePath filePath = fileToCat.toFilePath();
try {
return ContentRevisionCache
.getOrLoadAsString(myProject, filePath, myRevisionNumber, HgVcs.getKey(), ContentRevisionCache.UniqueType.REPOSITORY_CONTENT,
new Throwable2Computable<byte[], VcsException, IOException>() {
@Override
public byte[] compute() throws VcsException, IOException {
return loadContent(fileToCat);
}
}, filePath.getCharset());
}
catch (IOException e) {
throw new VcsException(e);
}
}
@NotNull
private byte[] loadContent(@NotNull HgFile fileToCat) {
HgCommandResult result = new HgCatCommand(myProject).execute(fileToCat, myRevisionNumber, getFile().getCharset());
return result != null && result.getExitValue() == 0 ? result.getBytesOutput() : new byte[0];
}
@Nullable
public byte[] getContentAsBytes() throws VcsException {
final String content = getContent();
if (content == null) {
return null;
}
final HgFile fileToCat = HgUtil.getFileNameInTargetRevision(myProject, myRevisionNumber, myHgFile);
try {
final VirtualFile vf = VcsUtil.getVirtualFile(myHgFile.getFile());
if (vf == null) {
return null;
}
return content.getBytes(vf.getCharset().name());
} catch (UnsupportedEncodingException e) {
throw new VcsException("Couldn't retrieve file content due to a UnsupportedEncodingException", e);
return ContentRevisionCache
.getOrLoadAsBytes(myProject, VcsUtil.getFilePath(fileToCat.getFile()), myRevisionNumber, HgVcs.getKey(),
ContentRevisionCache.UniqueType.REPOSITORY_CONTENT,
new Throwable2Computable<byte[], VcsException, IOException>() {
@Override
public byte[] compute() throws VcsException, IOException {
return loadContent(fileToCat);
}
});
}
catch (IOException e) {
throw new VcsException(e);
}
}
@@ -20,11 +20,10 @@ import com.intellij.openapi.vcs.history.VcsFileRevision;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.zmlx.hg4idea.command.HgCatCommand;
import org.zmlx.hg4idea.execution.HgCommandResult;
import org.zmlx.hg4idea.util.HgUtil;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.Map;
import java.util.Set;
@@ -41,7 +40,7 @@ public class HgFileRevision implements VcsFileRevision {
private final Set<String> myFilesModified;
private final Set<String> myFilesAdded;
private final Set<String> myFilesDeleted;
private Map<String,String> myFilesCopied;
private final Map<String, String> myFilesCopied;
public HgFileRevision(Project project, @NotNull HgFile hgFile, @NotNull HgRevisionNumber vcsRevisionNumber,
String branchName, Date revisionDate, String author, String commitMessage,
@@ -59,6 +58,7 @@ public class HgFileRevision implements VcsFileRevision {
myFilesCopied = filesCopied;
}
@NotNull
public HgRevisionNumber getRevisionNumber() {
return myRevisionNumber;
}
@@ -101,20 +101,11 @@ public class HgFileRevision implements VcsFileRevision {
return myFilesCopied;
}
@NotNull
public byte[] loadContent() throws IOException, VcsException {
try {
Charset charset = myFile.toFilePath().getCharset();
HgFile fileToCat = HgUtil.getFileNameInTargetRevision(myProject, myRevisionNumber, myFile);
String result = new HgCatCommand(myProject).execute(fileToCat, myRevisionNumber, charset);
if (result == null) {
return new byte[0];
} else {
return result.getBytes(charset.name());
}
} catch (UnsupportedEncodingException e) {
throw new VcsException(e);
}
HgFile fileToCat = HgUtil.getFileNameInTargetRevision(myProject, myRevisionNumber, myFile);
HgCommandResult result = new HgCatCommand(myProject).execute(fileToCat, myRevisionNumber, myFile.toFilePath().getCharset());
return result != null && result.getExitValue() == 0 ? result.getBytesOutput() : new byte[0];
}
public byte[] getContent() throws IOException, VcsException {
@@ -14,6 +14,7 @@ package org.zmlx.hg4idea.command;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.zmlx.hg4idea.HgFile;
import org.zmlx.hg4idea.HgRevisionNumber;
@@ -33,21 +34,13 @@ public class HgCatCommand {
}
@Nullable
public String execute(HgFile hgFile, HgRevisionNumber vcsRevisionNumber, Charset charset) {
public HgCommandResult execute(@NotNull HgFile hgFile, @Nullable HgRevisionNumber vcsRevisionNumber, @Nullable Charset charset) {
final List<String> arguments = createArguments(vcsRevisionNumber, hgFile.getRelativePath());
final HgCommandExecutor executor = new HgCommandExecutor(myProject);
executor.setSilent(true);
executor.setOutputAlwaysSuppressed(true);
executor.setCharset(charset);
final HgCommandResult result = executor.executeInCurrentThread(hgFile.getRepo(), "cat", arguments);
if (result == null) { // in case of error
return null;
}
if (result.getExitValue() == 1) { // file not found in given revision
return null;
}
return result.getRawOutput();
return executor.executeInCurrentThread(hgFile.getRepo(), "cat", arguments);
}
private static List<String> createArguments(HgRevisionNumber vcsRevisionNumber, String fileName) {
@@ -46,7 +46,6 @@ class HgCommandProcessHandler extends OSProcessHandler {
return myBinary ? new MyBinaryOutputReader(myProcess.getInputStream(), sleepingPolicy) : super.createOutputDataReader(sleepingPolicy);
}
@SuppressWarnings("unused")
@NotNull
public ByteArrayOutputStream getBinaryOutput() {
return myBinaryOutput;
@@ -55,6 +55,11 @@ public final class HgCommandResult {
return myProcessOutput.getStderr();
}
@NotNull
public byte[] getBytesOutput() {
return myByteArrayOutputStream.toByteArray();
}
public int getExitValue() {
return myProcessOutput.getExitCode();
}
@@ -217,7 +217,7 @@ public class HgHistoryUtil {
}
List<String> errors = result.getErrorLines();
if (errors != null && !errors.isEmpty()) {
if (!errors.isEmpty()) {
if (result.getExitValue() != 0) {
if (silent) {
LOG.debug(errors.toString());
@@ -323,14 +323,15 @@ public class HgHistoryUtil {
FileStatus aStatus) {
HgContentRevision beforeRevision =
fileBefore == null ? null : new HgContentRevision(project, new HgFile(root, new File(root.getPath(), fileBefore)), revisionBefore);
fileBefore == null ? null : HgContentRevision.create(project, new HgFile(root, new File(root.getPath(), fileBefore)), revisionBefore);
if (revisionAfter == null && fileBefore != null) {
ContentRevision currentRevision =
CurrentContentRevision.create(new HgFile(root, new File(root.getPath(), fileBefore)).toFilePath());
return new Change(beforeRevision, currentRevision, aStatus);
}
HgContentRevision afterRevision =
fileAfter == null ? null : new HgContentRevision(project, new HgFile(root, new File(root.getPath(), fileAfter)), revisionAfter);
HgContentRevision afterRevision = fileAfter == null ? null :
HgContentRevision
.create(project, new HgFile(root, new File(root.getPath(), fileAfter)), revisionAfter);
return new Change(beforeRevision, afterRevision, aStatus);
}
@@ -103,7 +103,7 @@ public class HgCachingCommittedChangesProvider implements CachingCommittedChange
if (!StringUtil.isEmpty(revisionPath)) {
VirtualFile root = ((HgRepositoryLocation)repositoryLocation).getRoot();
return new HgContentRevision(project, new HgFile(root, new File(revisionPath)), revisionNumber);
return HgContentRevision.create(project, new HgFile(root, new File(revisionPath)), revisionNumber);
}
else {
return null;
@@ -267,9 +267,9 @@ public class HgCachingCommittedChangesProvider implements CachingCommittedChange
FileStatus aStatus) {
HgContentRevision beforeRevision =
fileBefore == null ? null : new HgContentRevision(project, new HgFile(root, new File(root.getPath(), fileBefore)), revisionBefore);
fileBefore == null ? null : HgContentRevision.create(project, new HgFile(root, new File(root.getPath(), fileBefore)), revisionBefore);
HgContentRevision afterRevision =
fileAfter == null ? null : new HgContentRevision(project, new HgFile(root, new File(root.getPath(), fileAfter)), revisionAfter);
fileAfter == null ? null : HgContentRevision.create(project, new HgFile(root, new File(root.getPath(), fileAfter)), revisionAfter);
return new Change(beforeRevision, afterRevision, aStatus);
}
@@ -128,7 +128,7 @@ public class HgChangeProvider implements ChangeProvider {
&& resolveStatus.get(afterFile) == HgResolveStatusEnum.UNRESOLVED) {
builder.processChange(
new Change(
new HgContentRevision(myProject, beforeFile, parentRevision),
HgContentRevision.create(myProject, beforeFile, parentRevision),
HgCurrentContentRevision.create(afterFile, workingRevision),
FileStatus.MERGED_WITH_CONFLICTS
), myVcsKey);
@@ -182,7 +182,7 @@ public class HgChangeProvider implements ChangeProvider {
if (root != null && HgUtil.isHgRoot(root)) {
final HgRevisionNumber beforeRevisionNumber = new HgWorkingCopyRevisionsCommand(myProject).tip(root);
final ContentRevision beforeRevision = (beforeRevisionNumber == null ? null :
new HgContentRevision(myProject, new HgFile(myProject, vf), beforeRevisionNumber));
HgContentRevision.create(myProject, new HgFile(myProject, vf), beforeRevisionNumber));
builder.processChange(new Change(beforeRevision, CurrentContentRevision.create(filePath), FileStatus.MODIFIED), myVcsKey);
}
}
@@ -212,7 +212,7 @@ public class HgChangeProvider implements ChangeProvider {
HgRevisionNumber currentNumber, HgRevisionNumber parentRevision,
HgFile beforeFile, HgFile afterFile) {
processChange(
new HgContentRevision(project, beforeFile, parentRevision),
HgContentRevision.create(project, beforeFile, parentRevision),
null,
FileStatus.DELETED,
builder,
@@ -259,9 +259,9 @@ public class HgChangeProvider implements ChangeProvider {
} else {
// The original file does not exists so this is a rename.
processChange(
new HgContentRevision(project, beforeFile, parentRevision),
HgContentRevision.create(project, beforeFile, parentRevision),
HgCurrentContentRevision.create(afterFile, currentNumber),
HgChangeProvider.RENAMED,
RENAMED,
builder,
vcsKey
);
@@ -275,7 +275,7 @@ public class HgChangeProvider implements ChangeProvider {
HgRevisionNumber currentNumber, HgRevisionNumber parentRevision,
HgFile beforeFile, HgFile afterFile) {
processChange(
new HgContentRevision(project, beforeFile, parentRevision),
HgContentRevision.create(project, beforeFile, parentRevision),
HgCurrentContentRevision.create(afterFile, currentNumber),
FileStatus.MODIFIED,
builder,
@@ -19,7 +19,7 @@ import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.diff.DiffProvider;
import com.intellij.openapi.vcs.diff.ItemLatestState;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.vcsUtil.VcsUtil;
import org.zmlx.hg4idea.HgContentRevision;
@@ -95,8 +95,8 @@ public class HgDiffProvider implements DiffProvider {
if (hgRevisionNumber.isWorkingVersion()) {
throw new IllegalStateException("Should not compare against working copy");
}
HgFile hgFile = new HgFile(vcsRoot, VfsUtil.virtualToIoFile(file));
return new HgContentRevision(project, hgFile, hgRevisionNumber);
HgFile hgFile = new HgFile(vcsRoot, VfsUtilCore.virtualToIoFile(file));
return HgContentRevision.create(project, hgFile, hgRevisionNumber);
}
}
@@ -74,14 +74,14 @@ public class HgMergeProvider implements MergeProvider {
// the second one is "their" revision pulled from the parent repo,
// first parent is the local change.
// to retrieve the base version we get the parent of the local change, i.e. the [only] parent of the first parent.
//Whick one is local revision depends on which one is merged with,
// i.e if you update to 17 revision and then merge it woth 23, so 17 is your local and 17->parent is your base revision.
//Which one is local revision depends on which one is merged with,
// i.e if you update to 17 revision and then merge it with 23, so 17 is your local and 17->parent is your base revision.
// This may produce misunderstanding when you update your project with merging (your update firstly to next revisions and then
// merge with previous). see http://hgbook.red-bean.com/read/managing-releases-and-branchy-development.html
final Couple<HgRevisionNumber> parents = command.parents(repo, file);
serverRevisionNumber = parents.second;
localRevisionNumber = parents.first;
final HgContentRevision local = new HgContentRevision(myProject, hgFile, localRevisionNumber);
final HgContentRevision local = HgContentRevision.create(myProject, hgFile, localRevisionNumber);
mergeData.CURRENT = local.getContentAsBytes();
// we are sure that we have a common ancestor, because otherwise we'll get "repository is unrelated" error while pulling,
// due to different root changesets which is prohibited.
@@ -125,14 +125,14 @@ public class HgMergeProvider implements MergeProvider {
}
if (baseRevisionNumber != null) {
final HgContentRevision base = new HgContentRevision(myProject, hgFile, baseRevisionNumber);
final HgContentRevision base = HgContentRevision.create(myProject, hgFile, baseRevisionNumber);
//if file doesn't exist in ancestor revision the base revision should be empty
mergeData.ORIGINAL = base.getContent() != null ? base.getContentAsBytes() : new byte[0];
}
else { // no base revision means that the file was added simultaneously with different content in both repositories
mergeData.ORIGINAL = new byte[0];
}
final HgContentRevision server = new HgContentRevision(myProject, hgFile, serverRevisionNumber);
final HgContentRevision server = HgContentRevision.create(myProject, hgFile, serverRevisionNumber);
mergeData.LAST = server.getContentAsBytes();
file.refresh(false, false);
}
@@ -4,12 +4,14 @@ import org.testng.annotations.Test;
import org.zmlx.hg4idea.HgRevisionNumber;
import org.zmlx.hg4idea.command.HgCatCommand;
import org.zmlx.hg4idea.command.HgRevertCommand;
import org.zmlx.hg4idea.execution.HgCommandResult;
import java.io.File;
import java.nio.charset.Charset;
import java.util.Collections;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
public class HgRevertTest extends HgSingleUserTest {
@Test
@@ -24,9 +26,9 @@ public class HgRevertTest extends HgSingleUserTest {
revertCommand.execute(myRepo.getDir(), Collections.singleton(new File(myProjectDir, "file.txt").getPath()), null, false);
HgCatCommand catCommand = new HgCatCommand(myProject);
String content = catCommand.execute(getHgFile("file.txt"), null, Charset.defaultCharset());
assertEquals(content, "initial contents");
HgCommandResult result = catCommand.execute(getHgFile("file.txt"), null, Charset.defaultCharset());
assertNotNull(result);
assertEquals(result.getRawOutput(), "initial contents");
}
@@ -44,9 +46,9 @@ public class HgRevertTest extends HgSingleUserTest {
HgRevisionNumber.getLocalInstance("0"), false);
HgCatCommand catCommand = new HgCatCommand(myProject);
String content = catCommand.execute(getHgFile("file.txt"), HgRevisionNumber.getLocalInstance("0"), Charset.defaultCharset());
assertEquals(content, "initial contents");
HgCommandResult result = catCommand.execute(getHgFile("file.txt"), HgRevisionNumber.getLocalInstance("0"), Charset.defaultCharset());
assertNotNull(result);
assertEquals(result.getRawOutput(), "initial contents");
}
}