blob: c9630038df949abe9ab6f42b49fa729490d8e720 [file] [log] [blame]
// Copyright 2008-2010 Victor Iacoban
//
// 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.update;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.update.FileGroup;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.zmlx.hg4idea.*;
import org.zmlx.hg4idea.action.HgCommandResultNotifier;
import org.zmlx.hg4idea.command.*;
import org.zmlx.hg4idea.execution.HgCommandException;
import org.zmlx.hg4idea.execution.HgCommandResult;
import org.zmlx.hg4idea.repo.HgRepository;
import org.zmlx.hg4idea.util.HgErrorUtil;
import org.zmlx.hg4idea.util.HgUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.zmlx.hg4idea.HgErrorHandler.ensureSuccess;
import static org.zmlx.hg4idea.provider.update.HgUpdateType.MERGE;
import static org.zmlx.hg4idea.provider.update.HgUpdateType.ONLY_UPDATE;
public class HgRegularUpdater implements HgUpdater {
@NotNull private final Project project;
@NotNull private final VirtualFile repoRoot;
@NotNull private final HgUpdateConfigurationSettings updateConfiguration;
public HgRegularUpdater(@NotNull Project project, @NotNull VirtualFile repository, @NotNull HgUpdateConfigurationSettings configuration) {
this.project = project;
this.repoRoot = repository;
this.updateConfiguration = configuration;
}
public boolean update(final UpdatedFiles updatedFiles, ProgressIndicator indicator, List<VcsException> warnings)
throws VcsException {
indicator.setText(HgVcsMessages.message("hg4idea.progress.updating", repoRoot.getPath()));
String defaultPath = HgUtil.getRepositoryDefaultPath(project, repoRoot);
if (StringUtil.isEmptyOrSpaces(defaultPath)) {
throw new VcsException(HgVcsMessages.message("hg4idea.warning.no-default-update-path", repoRoot.getPath()));
}
List<HgRevisionNumber> branchHeadsBeforePull = new HgHeadsCommand(project, repoRoot).execute();
if (branchHeadsBeforePull.size() > 1) {
reportWarning(warnings, HgVcsMessages.message("hg4idea.update.warning.multipleHeadsBeforeUpdate", repoRoot.getPath()));
}
//TODO perhaps report a warning in this case ?
// //if the parent of the working dir is not the tip of the current branch, the user has
// //manually brought his working copy to some specific revision. In that case we won't touch
// //his setup
// if (!parentRevision.equals(currentBranchHead)) {
// throw new VcsException("working dir not at branch tip (use \"Update to...\" to check out branch tip)");
// }
if (updateConfiguration.shouldPull()) {
HgCommandExitCode pullResult = pull(repoRoot, indicator);
if (pullResult == HgCommandExitCode.ERROR) {
return false;
}
}
List<HgRevisionNumber> parentsBeforeUpdate = new HgWorkingCopyRevisionsCommand(project).parents(repoRoot);
if (parentsBeforeUpdate.size() > 1) {
throw new VcsException(HgVcsMessages.message("hg4idea.update.error.uncommittedMerge", repoRoot.getPath()));
}
indicator.setText2(HgVcsMessages.message("hg4idea.progress.countingHeads"));
List<HgRevisionNumber> branchHeadsAfterPull = new HgHeadsCommand(project, repoRoot).execute();
List<HgRevisionNumber> pulledBranchHeads = determinePulledBranchHeads(branchHeadsBeforePull, branchHeadsAfterPull);
List<HgRevisionNumber> remainingOriginalBranchHeads =
determingRemainingOriginalBranchHeads(branchHeadsBeforePull, branchHeadsAfterPull);
HgUpdateType updateType = updateConfiguration.getUpdateType();
if (branchHeadsAfterPull.size() > 1 && updateType != ONLY_UPDATE) {
// merge strategy
if (updateType == MERGE) {
abortOnLocalChanges();
abortOnMultiplePulledHeads(pulledBranchHeads);
abortOnMultipleLocalHeads(remainingOriginalBranchHeads);
HgCommandResult mergeResult = doMerge(indicator);
if (updateConfiguration.shouldCommitAfterMerge()) {
commitOrWarnAboutConflicts(warnings, mergeResult);
}
}
//rebase strategy
else {
processRebase(indicator, updatedFiles); //resolve conflicts processed during rebase
return true;
}
}
//if pull complete successfully and there are only one head, we need just update working directory to the head
else {
//in case of multiple heads the update will report the appropriate error
update(repoRoot, indicator, updatedFiles, warnings);
}
//any kind of update could have resulted in merges and merge conflicts, so run the resolver
resolvePossibleConflicts(updatedFiles);
return true;
}
private static List<HgRevisionNumber> determingRemainingOriginalBranchHeads(List<HgRevisionNumber> branchHeadsBeforePull,
List<HgRevisionNumber> branchHeadsAfterPull) {
List<HgRevisionNumber> originalBranchHeadsRemaining = new ArrayList<HgRevisionNumber>();
for (HgRevisionNumber headAfterPull : branchHeadsAfterPull) {
if (branchHeadsBeforePull.contains(headAfterPull)) {
originalBranchHeadsRemaining.add(headAfterPull);
}
}
return originalBranchHeadsRemaining;
}
private static List<HgRevisionNumber> determinePulledBranchHeads(List<HgRevisionNumber> branchHeadsBeforePull,
List<HgRevisionNumber> branchHeadsAfterPull) {
List<HgRevisionNumber> pulledBranchHeads = new ArrayList<HgRevisionNumber>(branchHeadsAfterPull);
pulledBranchHeads.removeAll(branchHeadsBeforePull);
return pulledBranchHeads;
}
private void abortOnMultipleLocalHeads(List<HgRevisionNumber> originalBranchHeadsRemaining) throws VcsException {
if (originalBranchHeadsRemaining.size() != 1) {
throw new VcsException(HgVcsMessages.message("hg4idea.update.error.merge.multipleLocalHeads", repoRoot.getPath()));
}
}
private void abortOnMultiplePulledHeads(List<HgRevisionNumber> newBranchHeadsAfterPull) throws VcsException {
if (newBranchHeadsAfterPull.size() != 1) {
throw new VcsException(HgVcsMessages.message("hg4idea.update.error.merge.multipleRemoteHeads", newBranchHeadsAfterPull.size(),
repoRoot.getPath()));
}
}
private void updateToPulledHead(VirtualFile repo, UpdatedFiles updatedFiles, HgRevisionNumber newHead, ProgressIndicator indicator) {
indicator.setText2(HgVcsMessages.message("hg4idea.update.progress.updating.to.pulled.head"));
HgRevisionNumber parentBeforeUpdate = new HgWorkingCopyRevisionsCommand(project).firstParent(repo);
HgUpdateCommand updateCommand = new HgUpdateCommand(project, repoRoot);
updateCommand.setRevision(newHead.getChangeset());
updateCommand.setClean(true);
updateCommand.execute();
HgRevisionNumber commonParent = findCommonParent(newHead, parentBeforeUpdate);
addUpdatedFiles(repo, updatedFiles, commonParent, newHead);
}
private @Nullable HgRevisionNumber findCommonParent(HgRevisionNumber newHead, HgRevisionNumber parentBeforeUpdate) {
// hg log -r 0:source --prune dest --limit 1
final List<HgRevisionNumber> pulledRevisions = new HgMergePreviewCommand(project, newHead, parentBeforeUpdate, 1).execute(repoRoot);
if (pulledRevisions == null || pulledRevisions.isEmpty()) {
return null;
}
HgRevisionNumber pulledRevision = pulledRevisions.get(0);
final List<HgRevisionNumber> parentRevisions = new HgWorkingCopyRevisionsCommand(project).getRevisions(repoRoot, "parent", null, pulledRevision, true);
if (parentRevisions.isEmpty()) {
return null;
}
return parentRevisions.get(0);
}
private void commitOrWarnAboutConflicts(List<VcsException> exceptions, HgCommandResult mergeResult) throws VcsException {
if (mergeResult.getExitValue() == 0) { //operation successful and no conflicts
try {
new HgCommitCommand(project, repoRoot, "Automated merge").execute();
} catch (HgCommandException e) {
throw new VcsException(e);
}
} else {
reportWarning(exceptions, HgVcsMessages.message("hg4idea.update.warning.merge.conflicts", repoRoot.getPath()));
}
}
private HgCommandResult doMerge(ProgressIndicator indicator) throws VcsException {
indicator.setText2(HgVcsMessages.message("hg4idea.update.progress.merging"));
HgMergeCommand mergeCommand = new HgMergeCommand(project, repoRoot);
//do not explicitly set the revision, that way mercurial itself checks that there are exactly
//two heads in this branch
// mergeCommand.setRevision(headToMerge.getRevision());
return new HgHeadMerger(project, mergeCommand).merge(repoRoot);
}
private void processRebase(ProgressIndicator indicator, final UpdatedFiles updatedFiles) throws VcsException {
indicator.setText2(HgVcsMessages.message("hg4idea.progress.rebase"));
HgRepository repository = HgUtil.getRepositoryManager(project).getRepositoryForRoot(repoRoot);
if (repository == null) {
throw new VcsException("Repository not found for root " + repoRoot);
}
HgRebaseCommand rebaseCommand = new HgRebaseCommand(project, repository);
HgCommandResult result = new HgRebaseCommand(project, repository).startRebase();
if (HgErrorUtil.isAbort(result)) {
new HgCommandResultNotifier(project).notifyError(result, "Hg Error", "Couldn't rebase repository.");
return;
}
//noinspection ConstantConditions
while (result.getExitValue() == 1) { //if result == null isAbort will be true;
resolvePossibleConflicts(updatedFiles);
if (!HgConflictResolver.findConflicts(project, repoRoot).isEmpty()) {
break;
}
result = rebaseCommand.continueRebase();
if (HgErrorUtil.isAbort(result)) {
new HgCommandResultNotifier(project).notifyError(result, "Hg Error", "Couldn't continue rebasing");
break;
}
}
repository.update();
repoRoot.refresh(true, true);
}
private void abortOnLocalChanges() throws VcsException {
if (getLocalChanges().size() != 0) {
throw new VcsException(HgVcsMessages.message("hg4idea.update.error.localchanges", repoRoot.getPath()));
}
}
private void resolvePossibleConflicts(final UpdatedFiles updatedFiles) {
new HgConflictResolver(project, updatedFiles).resolve(repoRoot);
}
private Set<HgChange> getLocalChanges() {
HgStatusCommand statusCommand = new HgStatusCommand.Builder(true).unknown(false).ignored(false).build(project);
return statusCommand.execute(repoRoot);
}
private HgCommandExitCode pull(VirtualFile repo, ProgressIndicator indicator)
throws VcsException {
indicator.setText2(HgVcsMessages.message("hg4idea.progress.pull.with.update"));
HgPullCommand hgPullCommand = new HgPullCommand(project, repo);
final String defaultPath = HgUtil.getRepositoryDefaultPath(project, repo);
hgPullCommand.setSource(defaultPath);
return hgPullCommand.execute();
}
private void update(@NotNull VirtualFile repo, ProgressIndicator indicator, UpdatedFiles updatedFiles, List<VcsException> warnings) throws VcsException {
indicator.setText2(HgVcsMessages.message("hg4idea.progress.updatingworkingdir"));
HgRevisionNumber parentBeforeUpdate = new HgWorkingCopyRevisionsCommand(project).firstParent(repo);
HgUpdateCommand hgUpdateCommand = new HgUpdateCommand(project, repo);
HgCommandResult updateResult = hgUpdateCommand.execute();
String warningMessages = ensureSuccess(updateResult).getRawError();
handlePossibleWarning(warnings, warningMessages);
HgRevisionNumber parentAfterUpdate = new HgWorkingCopyRevisionsCommand(project).firstParent(repo);
addUpdatedFiles(repo, updatedFiles, parentBeforeUpdate, parentAfterUpdate);
}
private static void handlePossibleWarning(List<VcsException> exceptions, String possibleWarning) {
if (!StringUtil.isEmptyOrSpaces(possibleWarning)) {
reportWarning(exceptions, possibleWarning);
}
}
private static void reportWarning(List<VcsException> exceptions, String warningMessage) {
@SuppressWarnings({"ThrowableInstanceNeverThrown"})
VcsException warningException = new VcsException(warningMessage);
warningException.setIsWarning(true);
exceptions.add(warningException);
}
private void addUpdatedFiles(VirtualFile repo, UpdatedFiles updatedFiles, HgRevisionNumber parentBeforeUpdate, HgRevisionNumber parentAfterUpdate) {
if (parentAfterUpdate == null || parentBeforeUpdate == null) {
return;
}
if (parentAfterUpdate.equals(parentBeforeUpdate)) { // nothing to update => returning not to capture local uncommitted changes
return;
}
HgStatusCommand statusCommand = new HgStatusCommand.Builder(true).ignored(false).unknown(false).baseRevision(parentBeforeUpdate).targetRevision(
parentAfterUpdate).build(project);
Set<HgChange> changes = statusCommand.execute(repo);
for (HgChange change : changes) {
HgFileStatusEnum status = change.getStatus();
switch (status) {
case ADDED:
addToGroup(updatedFiles, change, FileGroup.CREATED_ID);
break;
case MODIFIED:
addToGroup(updatedFiles, change, FileGroup.UPDATED_ID);
break;
case DELETED:
addToGroup(updatedFiles, change, FileGroup.REMOVED_FROM_REPOSITORY_ID);
break;
case COPY:
addToGroup(updatedFiles, change, FileGroup.CHANGED_ON_SERVER_ID);
break;
default:
//do nothing
break;
}
}
}
private static void addToGroup(UpdatedFiles updatedFiles, HgChange change, String id) {
updatedFiles.getGroupById(id).add(change.afterFile().getFile().getAbsolutePath(), HgVcs.VCS_NAME, null);
}
}