blob: be084b0986d01bf3a7cc23ebe8c2f5276c7f7128 [file] [log] [blame]
/*
* Copyright 2000-2013 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.jetbrains.idea.svn.diff;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.changes.CurrentContentRevision;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.SvnStatusConvertor;
import org.jetbrains.idea.svn.SvnUtil;
import org.jetbrains.idea.svn.WorkingCopyFormat;
import org.jetbrains.idea.svn.api.BaseSvnClient;
import org.jetbrains.idea.svn.api.NodeKind;
import org.jetbrains.idea.svn.commandLine.CommandExecutor;
import org.jetbrains.idea.svn.commandLine.CommandUtil;
import org.jetbrains.idea.svn.commandLine.SvnBindException;
import org.jetbrains.idea.svn.commandLine.SvnCommandName;
import org.jetbrains.idea.svn.history.SvnRepositoryContentRevision;
import org.jetbrains.idea.svn.status.SvnStatusHandler;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.*;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
/**
* @author Konstantin Kolosovsky.
*/
public class CmdDiffClient extends BaseSvnClient implements DiffClient {
@NotNull
@Override
public List<Change> compare(@NotNull SvnTarget target1, @NotNull SvnTarget target2) throws VcsException {
assertUrl(target1);
if (target2.isFile()) {
// Such combination (file and url) with "--summarize" option is supported only in svn 1.8.
// For svn 1.7 "--summarize" is only supported when both targets are repository urls.
assertDirectory(target2);
WorkingCopyFormat format = WorkingCopyFormat.from(myFactory.createVersionClient().getVersion());
if (format.less(WorkingCopyFormat.ONE_DOT_EIGHT)) {
throw new SvnBindException("Could not compare local file and remote url with executable for svn " + format);
}
}
List<String> parameters = new ArrayList<String>();
CommandUtil.put(parameters, target1);
CommandUtil.put(parameters, target2);
parameters.add("--xml");
parameters.add("--summarize");
CommandExecutor executor = execute(myVcs, target1, SvnCommandName.diff, parameters, null);
return parseOutput(target1, target2, executor);
}
@Override
public void unifiedDiff(@NotNull SvnTarget target1, @NotNull SvnTarget target2, @NotNull OutputStream output) throws VcsException {
assertUrl(target1);
assertUrl(target2);
List<String> parameters = ContainerUtil.newArrayList();
CommandUtil.put(parameters, target1);
CommandUtil.put(parameters, target2);
CommandExecutor executor = execute(myVcs, target1, SvnCommandName.diff, parameters, null);
try {
executor.getBinaryOutput().writeTo(output);
}
catch (IOException e) {
throw new SvnBindException(e);
}
}
@NotNull
private List<Change> parseOutput(@NotNull SvnTarget target1, @NotNull SvnTarget target2, @NotNull CommandExecutor executor)
throws SvnBindException {
try {
DiffInfo diffInfo = CommandUtil.parse(executor.getOutput(), DiffInfo.class);
List<Change> result = ContainerUtil.newArrayList();
if (diffInfo != null) {
for (DiffPath path : diffInfo.diffPaths) {
result.add(createChange(target1, target2, path));
}
}
return result;
}
catch (JAXBException e) {
throw new SvnBindException(e);
}
}
@NotNull
private ContentRevision createRevision(@NotNull FilePath path,
@NotNull FilePath localPath,
@NotNull SVNRevision revision,
@NotNull FileStatus status) {
ContentRevision result;
if (path.isNonLocal()) {
// explicitly use local path for deleted items - so these items will be correctly displayed as deleted under local working copy node
// and not as deleted under remote branch node (in ChangesBrowser)
// NOTE, that content is still retrieved using remotePath.
result = SvnRepositoryContentRevision.create(myVcs, path, status == FileStatus.DELETED ? localPath : null, revision.getNumber());
}
else {
result = CurrentContentRevision.create(path);
}
return result;
}
private static FilePath createFilePath(@NotNull SvnTarget target, boolean isDirectory) {
return target.isFile()
? VcsUtil.getFilePath(target.getFile(), isDirectory)
: VcsUtil.getFilePathOnNonLocal(SvnUtil.toDecodedString(target), isDirectory);
}
@NotNull
private Change createChange(@NotNull SvnTarget target1, @NotNull SvnTarget target2, @NotNull DiffPath diffPath) throws SvnBindException {
// TODO: 1) Unify logic of creating Change instance with SvnDiffEditor and SvnChangeProviderContext
// TODO: 2) If some directory is switched, files inside it are returned as modified in "svn diff --summarize", even if they are equal
// TODO: to branch files by content - possibly add separate processing of all switched files
// TODO: 3) Properties change is currently not added as part of result change like in SvnChangeProviderContext.patchWithPropertyChange
SvnTarget subTarget1 = SvnUtil.append(target1, diffPath.path, true);
String relativePath = SvnUtil.getRelativeUrl(SvnUtil.toDecodedString(target1), SvnUtil.toDecodedString(subTarget1));
if (relativePath == null) {
throw new SvnBindException("Could not get relative path for " + target1 + " and " + subTarget1);
}
SvnTarget subTarget2 = SvnUtil.append(target2, FileUtil.toSystemIndependentName(relativePath));
FilePath target1Path = createFilePath(subTarget1, diffPath.isDirectory());
FilePath target2Path = createFilePath(subTarget2, diffPath.isDirectory());
FileStatus status = SvnStatusConvertor
.convertStatus(SvnStatusHandler.getStatus(diffPath.itemStatus), SvnStatusHandler.getStatus(diffPath.propertiesStatus));
// statuses determine changes needs to be done to "target1" to get "target2" state
ContentRevision beforeRevision = status == FileStatus.ADDED
? null
: createRevision(target1Path, target2Path, target1.getPegRevision(), status);
ContentRevision afterRevision = status == FileStatus.DELETED
? null
: createRevision(target2Path, target1Path, target2.getPegRevision(), status);
return createChange(status, beforeRevision, afterRevision);
}
@NotNull
private static Change createChange(@NotNull final FileStatus status,
@Nullable final ContentRevision beforeRevision,
@Nullable final ContentRevision afterRevision) {
// isRenamed() and isMoved() are always false here not to have text like "moved from ..." in changes window - by default different
// paths in before and after revisions are treated as move, but this is not the case for "Compare with Branch"
return new Change(beforeRevision, afterRevision, status) {
@Override
public boolean isRenamed() {
return false;
}
@Override
public boolean isMoved() {
return false;
}
};
}
@XmlRootElement(name = "diff")
public static class DiffInfo {
@XmlElementWrapper(name = "paths")
@XmlElement(name = "path")
public List<DiffPath> diffPaths = new ArrayList<DiffPath>();
}
public static class DiffPath {
@XmlAttribute(name = "kind", required = true)
public NodeKind kind;
@XmlAttribute(name = "props")
public String propertiesStatus;
@XmlAttribute(name = "item")
public String itemStatus;
@XmlValue
public String path;
public boolean isDirectory() {
return kind.isDirectory();
}
}
}