| // 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); |
| } |
| } |