blob: c3ecccb95d45149e584b3670374dc3466e259851 [file] [log] [blame]
/*
* Copyright 2000-2014 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.provider;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsBundle;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.merge.MergeData;
import com.intellij.openapi.vcs.merge.MergeProvider;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.vcsUtil.VcsRunnable;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.zmlx.hg4idea.HgContentRevision;
import org.zmlx.hg4idea.HgFile;
import org.zmlx.hg4idea.HgRevisionNumber;
import org.zmlx.hg4idea.HgVcsMessages;
import org.zmlx.hg4idea.action.HgCommandResultNotifier;
import org.zmlx.hg4idea.command.HgResolveCommand;
import org.zmlx.hg4idea.command.HgWorkingCopyRevisionsCommand;
import org.zmlx.hg4idea.execution.HgCommandResult;
import org.zmlx.hg4idea.execution.HgPromptCommandExecutor;
import org.zmlx.hg4idea.util.HgUtil;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author Kirill Likhodedov
*/
public class HgMergeProvider implements MergeProvider {
private static final Logger LOG = Logger.getInstance(HgMergeProvider.class.getName());
private final Project myProject;
public HgMergeProvider(Project project) {
myProject = project;
}
@NotNull
@Override
public MergeData loadRevisions(final VirtualFile file) throws VcsException {
final MergeData mergeData = new MergeData();
final VcsRunnable runnable = new VcsRunnable() {
public void run() throws VcsException {
final HgWorkingCopyRevisionsCommand command = new HgWorkingCopyRevisionsCommand(myProject);
final VirtualFile repo = HgUtil.getHgRootOrThrow(myProject, file);
final HgFile hgFile = new HgFile(myProject, file);
HgRevisionNumber serverRevisionNumber;
HgRevisionNumber localRevisionNumber;
HgRevisionNumber baseRevisionNumber = null;
// there are two possibilities: we have checked in local changes in the selected file or we didn't.
if (wasFileCheckedIn(repo, file)) {
// 1. We checked in.
// We have a merge in progress, which means we have 2 heads (parents).
// 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.
// 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);
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.
// Find common ancestor of two revisions : hg debugancestor rev1 rev2
// Using quotes may produce wrong escaping errors on Unix-type systems
List<String> arguments = new ArrayList<String>();
String localChangeset = localRevisionNumber.getChangeset();
String serverChangeset = serverRevisionNumber.getChangeset();
arguments.add(StringUtil.isEmptyOrSpaces(localChangeset) ? localRevisionNumber.getRevision() : localChangeset);
arguments.add(StringUtil.isEmptyOrSpaces(serverChangeset) ? serverRevisionNumber.getRevision() : serverChangeset);
HgCommandResult result = new HgPromptCommandExecutor(myProject).executeInCurrentThread(repo, "debugancestor", arguments);
if (result != null) {
String output = result.getRawOutput();
final List<String> parts = StringUtil.split(output, ":");
if (parts.size() < 2) {
LOG.info("Couldn't parse result of debugancestor command execution " + arguments);
new HgCommandResultNotifier(myProject)
.notifyError(null, HgVcsMessages.message("hg4idea.error.debugancestor.command.execution"),
HgVcsMessages.message("hg4idea.error.debugancestor.command.description"));
}
else {
baseRevisionNumber = HgRevisionNumber.getInstance(parts.get(0), parts.get(1));
}
}
else {
LOG.info(HgVcsMessages.message("hg4idea.error.debugancestor.command.execution") + arguments);
new HgCommandResultNotifier(myProject)
.notifyError(null, HgVcsMessages.message("hg4idea.error.debugancestor.command.execution"),
HgVcsMessages.message("hg4idea.error.debugancestor.command.description"));
}
} else {
// 2. local changes are not checked in.
// then there is only one parent, which is server changes.
// local changes are retrieved from the file system, they are not in the Mercurial yet.
// base is the only parent of server changes.
serverRevisionNumber = command.parents(repo, file).first;
baseRevisionNumber = command.parents(repo, file, serverRevisionNumber).first;
final File origFile = new File(file.getPath() + ".orig");
try {
mergeData.CURRENT = VcsUtil.getFileByteContent(origFile);
} catch (IOException e) {
LOG.info("Couldn't retrieve byte content of the file: " + origFile.getPath(), e);
}
}
if (baseRevisionNumber != null) {
final HgContentRevision base = new HgContentRevision(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);
mergeData.LAST = server.getContentAsBytes();
file.refresh(false, false);
}
};
VcsUtil.runVcsProcessWithProgress(runnable, VcsBundle.message("multiple.file.merge.loading.progress.title"), false, myProject);
return mergeData;
}
@Override
public void conflictResolvedForFile(VirtualFile file) {
try {
new HgResolveCommand(myProject).markResolved(HgUtil.getHgRootOrThrow(myProject, file), file);
} catch (VcsException e) {
LOG.error("Couldn't mark file resolved, because it is not under Mercurial root.");
}
}
@Override
public boolean isBinary(@NotNull VirtualFile file) {
return file.getFileType().isBinary();
}
/**
* Checks if the given file was checked in before the merge start.
* @param repo repository to work on.
* @param file file to be checked.
* @return True if the file was checked in before merge, false if it wasn't.
*/
private boolean wasFileCheckedIn(VirtualFile repo, VirtualFile file) {
// in the case of merge if the file was checked in, it will have 2 parents after hg pull. If it wasn't, it would have only one parent
final Couple<HgRevisionNumber> parents = new HgWorkingCopyRevisionsCommand(myProject).parents(repo, file);
return parents.second != null;
}
}