All Downloads are FREE. Search and download functionalities are using the official Maven repository.

git4idea.update.GitUpdateProcess Maven / Gradle / Ivy

Go to download

A packaging of the IntelliJ Community Edition git4idea library. This is release number 1 of trunk branch 142.

The newest version!
/*
 * 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 git4idea.update;

import com.intellij.dvcs.DvcsUtil;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Clock;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.impl.LocalChangesUnderRoots;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.continuation.ContinuationContext;
import com.intellij.util.continuation.ContinuationFinalTasksInserter;
import com.intellij.util.continuation.TaskDescriptor;
import com.intellij.util.continuation.Where;
import com.intellij.util.text.DateFormatUtil;
import git4idea.GitLocalBranch;
import git4idea.GitPlatformFacade;
import git4idea.GitUtil;
import git4idea.branch.GitBranchPair;
import git4idea.branch.GitBranchUtil;
import git4idea.commands.Git;
import git4idea.config.UpdateMethod;
import git4idea.merge.GitConflictResolver;
import git4idea.merge.GitMergeCommittingConflictResolver;
import git4idea.merge.GitMerger;
import git4idea.rebase.GitRebaser;
import git4idea.repo.GitBranchTrackInfo;
import git4idea.repo.GitRepository;
import git4idea.stash.GitChangesSaver;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import static git4idea.util.GitUIUtil.*;

/**
 * Handles update process (pull via merge or rebase) for several roots.
 *
 * @author Kirill Likhodedov
 */
public class GitUpdateProcess {
  private static final Logger LOG = Logger.getInstance(GitUpdateProcess.class);

  @NotNull private final Project myProject;
  @NotNull private final Git myGit;
  @NotNull private final Collection myRepositories;
  private final boolean myCheckRebaseOverMergeProblem;
  private final UpdatedFiles myUpdatedFiles;
  private final ProgressIndicator myProgressIndicator;
  private final GitMerger myMerger;
  private final GitChangesSaver mySaver;

  private final Map myTrackedBranches = new HashMap();
  private GitUpdateResult myResult;
  private final Collection myRootsToSave;

  public GitUpdateProcess(@NotNull Project project,
                          @NotNull GitPlatformFacade platformFacade,
                          @Nullable ProgressIndicator progressIndicator,
                          @NotNull Collection repositories,
                          @NotNull UpdatedFiles updatedFiles,
                          boolean checkRebaseOverMergeProblem) {
    myProject = project;
    myRepositories = repositories;
    myCheckRebaseOverMergeProblem = checkRebaseOverMergeProblem;
    myGit = ServiceManager.getService(Git.class);
    myUpdatedFiles = updatedFiles;
    myProgressIndicator = progressIndicator == null ? new EmptyProgressIndicator() : progressIndicator;
    myMerger = new GitMerger(myProject);
    mySaver = GitChangesSaver.getSaver(myProject, platformFacade, myGit,
                                       myProgressIndicator,
                                       "Uncommitted changes before update operation at " + DateFormatUtil.formatDateTime(Clock.getTime()));
    myRootsToSave = new HashSet(1);
  }

  /**
   * Checks if update is possible, saves local changes and updates all roots.
   * In case of error shows notification and returns false. If update completes without errors, returns true.
   *
   * Perform update on all roots.
   * 0. Blocks reloading project on external change, saving/syncing on frame deactivation.
   * 1. Checks if update is possible (rebase/merge in progress, no tracked branches...) and provides merge dialog to solve problems.
   * 2. Finds updaters to use (merge or rebase).
   * 3. Preserves local changes if needed (not needed for merge sometimes).
   * 4. Updates via 'git pull' or equivalent.
   * 5. Restores local changes if update completed or failed with error. If update is incomplete, i.e. some unmerged files remain,
   * local changes are not restored.
   *
   */
  @NotNull
  public GitUpdateResult update(final UpdateMethod updateMethod) {
    LOG.info("update started|" + updateMethod);
    String oldText = myProgressIndicator.getText();
    myProgressIndicator.setText("Updating...");

    for (GitRepository repository : myRepositories) {
      repository.update();
    }

    // check if update is possible
    if (checkRebaseInProgress() || isMergeInProgress() || areUnmergedFiles() || !checkTrackedBranchesConfigured()) {
      return GitUpdateResult.NOT_READY;
    }

    if (!fetchAndNotify()) {
      return GitUpdateResult.NOT_READY;
    }

    GitComplexProcess.Operation updateOperation = new GitComplexProcess.Operation() {
      @Override public void run(ContinuationContext continuationContext) {
        AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject);
        try {
          myResult = updateImpl(updateMethod, continuationContext);
        }
        finally {
          DvcsUtil.workingTreeChangeFinished(myProject, token);
        }
      }
    };
    GitComplexProcess.execute(myProject, "update", updateOperation);

    myProgressIndicator.setText(oldText);
    return myResult;
  }

  @NotNull
  private GitUpdateResult updateImpl(@NotNull UpdateMethod updateMethod, ContinuationContext context) {
    Map updaters;
    try {
      updaters = defineUpdaters(updateMethod);
    }
    catch (VcsException e) {
      LOG.info(e);
      notifyError(myProject, "Git update failed", e.getMessage(), true, e);
      return GitUpdateResult.ERROR;
    }

    if (updaters.isEmpty()) {
      return GitUpdateResult.NOTHING_TO_UPDATE;
    }

    updaters = tryFastForwardMergeForRebaseUpdaters(updaters);

    if (updaters.isEmpty()) {
      // everything was updated via the fast-forward merge
      return GitUpdateResult.SUCCESS;
    }

    if (myCheckRebaseOverMergeProblem) {
      Collection problematicRoots = findRootsRebasingOverMerge(updaters);
      if (!problematicRoots.isEmpty()) {
        GitRebaseOverMergeProblem.Decision decision = GitRebaseOverMergeProblem.showDialog();
        if (decision == GitRebaseOverMergeProblem.Decision.MERGE_INSTEAD) {
          for (VirtualFile root : problematicRoots) {
            updaters.put(root, new GitMergeUpdater(myProject, myGit, root, myTrackedBranches, myProgressIndicator, myUpdatedFiles));
          }
        }
        else if (decision == GitRebaseOverMergeProblem.Decision.CANCEL_OPERATION) {
          return GitUpdateResult.CANCEL;
        }
      }
    }

    // save local changes if needed (update via merge may perform without saving).
    LOG.info("updateImpl: identifying if save is needed...");
    for (Map.Entry entry : updaters.entrySet()) {
      VirtualFile root = entry.getKey();
      GitUpdater updater = entry.getValue();
      if (updater.isSaveNeeded()) {
        myRootsToSave.add(root);
        LOG.info("update| root " + root + " needs save");
      }
    }

    LOG.info("updateImpl: saving local changes...");
    try {
      mySaver.saveLocalChanges(myRootsToSave);
    } catch (VcsException e) {
      LOG.info("Couldn't save local changes", e);
      notifyError(myProject, "Git update failed",
                  "Tried to save uncommitted changes in " + mySaver.getSaverName() + " before update, but failed with an error.
" + "Update was cancelled.", true, e); return GitUpdateResult.ERROR; } // update each root LOG.info("updateImpl: updating..."); boolean incomplete = false; GitUpdateResult compoundResult = null; VirtualFile currentlyUpdatedRoot = null; try { for (Map.Entry entry : updaters.entrySet()) { currentlyUpdatedRoot = entry.getKey(); GitUpdater updater = entry.getValue(); GitUpdateResult res = updater.update(); LOG.info("updating root " + currentlyUpdatedRoot + " finished: " + res); if (res == GitUpdateResult.INCOMPLETE) { incomplete = true; } compoundResult = joinResults(compoundResult, res); } } catch (VcsException e) { String rootName = (currentlyUpdatedRoot == null) ? "" : currentlyUpdatedRoot.getName(); LOG.info("Error updating changes for root " + currentlyUpdatedRoot, e); notifyImportantError(myProject, "Error updating " + rootName, "Updating " + rootName + " failed with an error: " + e.getLocalizedMessage()); } finally { // Note: compoundResult normally should not be null, because the updaters map was checked for non-emptiness. // But if updater.update() fails with exception for the first root, then the value would not be assigned. // In this case we don't restore local changes either, because update failed. if (incomplete || compoundResult == null || !compoundResult.isSuccess()) { mySaver.notifyLocalChangesAreNotRestored(); } else { LOG.info("updateImpl: restoring local changes..."); restoreLocalChanges(context); } } return compoundResult; } @NotNull private Collection findRootsRebasingOverMerge(@NotNull final Map updaters) { return ContainerUtil.mapNotNull(updaters.keySet(), new Function() { @Override public VirtualFile fun(VirtualFile root) { GitUpdater updater = updaters.get(root); if (updater instanceof GitRebaseUpdater) { String currentRef = updater.getSourceAndTarget().getBranch().getFullName(); String baseRef = ObjectUtils.assertNotNull(updater.getSourceAndTarget().getDest()).getFullName(); return GitRebaseOverMergeProblem.hasProblem(myProject, root, baseRef, currentRef) ? root : null; } return null; } }); } @NotNull private Map tryFastForwardMergeForRebaseUpdaters(@NotNull Map updaters) { Map modifiedUpdaters = new HashMap(); Map> changesUnderRoots = new LocalChangesUnderRoots(ChangeListManager.getInstance(myProject), ProjectLevelVcsManager.getInstance(myProject)). getChangesUnderRoots(updaters.keySet()); for (Map.Entry updaterEntry : updaters.entrySet()) { VirtualFile root = updaterEntry.getKey(); GitUpdater updater = updaterEntry.getValue(); Collection changes = changesUnderRoots.get(root); if (updater instanceof GitRebaseUpdater && changes != null && !changes.isEmpty()) { // check only if there are local changes, otherwise stash won't happen anyway and there would be no optimization GitRebaseUpdater rebaseUpdater = (GitRebaseUpdater) updater; if (rebaseUpdater.fastForwardMerge()) { continue; } } modifiedUpdaters.put(root, updater); } return modifiedUpdaters; } @NotNull private Map defineUpdaters(@NotNull UpdateMethod updateMethod) throws VcsException { final Map updaters = new HashMap(); LOG.info("updateImpl: defining updaters..."); for (GitRepository repository : myRepositories) { VirtualFile root = repository.getRoot(); GitUpdater updater = GitUpdater.getUpdater(myProject, myGit, myTrackedBranches, root, myProgressIndicator, myUpdatedFiles, updateMethod); if (updater.isUpdateNeeded()) { updaters.put(root, updater); } LOG.info("update| root=" + root + " ,updater=" + updater); } return updaters; } @NotNull private static GitUpdateResult joinResults(@Nullable GitUpdateResult compoundResult, GitUpdateResult result) { if (compoundResult == null) { return result; } return compoundResult.join(result); } private void restoreLocalChanges(ContinuationContext context) { context.addExceptionHandler(VcsException.class, new Consumer() { @Override public void consume(VcsException e) { LOG.info("Couldn't restore local changes after update", e); notifyImportantError(myProject, "Couldn't restore local changes after update", "Restoring changes saved before update failed with an error.
" + e.getLocalizedMessage()); } }); // try restore changes under all circumstances final ContinuationFinalTasksInserter finalTasksInserter = new ContinuationFinalTasksInserter(context); finalTasksInserter.allNextAreFinal(); // !!!! this task is put NEXT, i.e. if unshelve/unstash will be done synchronously or scheduled on context, // it is unimportant -> files will be refreshed after context.next(new TaskDescriptor("Refresh local files", Where.POOLED) { @Override public void run(ContinuationContext context) { mySaver.refresh(); } }); mySaver.restoreLocalChanges(context); finalTasksInserter.removeFinalPropertyAdder(); } // fetch all roots. If an error happens, return false and notify about errors. private boolean fetchAndNotify() { return new GitFetcher(myProject, myProgressIndicator, false).fetchRootsAndNotify(myRepositories, "Update failed", false); } /** * For each root check that the repository is on branch, and this branch is tracking a remote branch, * and the remote branch exists. * If it is not true for at least one of roots, notify and return false. * If branch configuration is OK for all roots, return true. */ private boolean checkTrackedBranchesConfigured() { LOG.info("checking tracked branch configuration..."); for (GitRepository repository : myRepositories) { VirtualFile root = repository.getRoot(); final GitLocalBranch branch = repository.getCurrentBranch(); if (branch == null) { LOG.info("checkTrackedBranchesConfigured: current branch is null in " + repository); notifyImportantError(myProject, "Can't update: no current branch", "You are in 'detached HEAD' state, which means that you're not on any branch" + rootStringIfNeeded(root) + "Checkout a branch to make update possible."); return false; } GitBranchTrackInfo trackInfo = GitBranchUtil.getTrackInfoForBranch(repository, branch); if (trackInfo == null) { final String branchName = branch.getName(); LOG.info(String.format("checkTrackedBranchesConfigured: no track info for current branch %s in %s", branch, repository)); notifyImportantError(myProject, "Can't update: no tracked branch", "No tracked branch configured for branch " + code(branchName) + rootStringIfNeeded(root) + "To make your branch track a remote branch call, for example,
" + "git branch --set-upstream " + branchName + " origin/" + branchName + ""); return false; } myTrackedBranches.put(root, new GitBranchPair(branch, trackInfo.getRemoteBranch())); } return true; } private String rootStringIfNeeded(@NotNull VirtualFile root) { if (myRepositories.size() < 2) { return ".
"; } return "
in Git repository " + code(root.getPresentableUrl()) + "
"; } /** * Check if merge is in progress, propose to resolve conflicts. * @return true if merge is in progress, which means that update can't continue. */ private boolean isMergeInProgress() { LOG.info("isMergeInProgress: checking if there is an unfinished merge process..."); final Collection mergingRoots = myMerger.getMergingRoots(); if (mergingRoots.isEmpty()) { return false; } LOG.info("isMergeInProgress: roots with unfinished merge: " + mergingRoots); GitConflictResolver.Params params = new GitConflictResolver.Params(); params.setErrorNotificationTitle("Can't update"); params.setMergeDescription("You have unfinished merge. These conflicts must be resolved before update."); return !new GitMergeCommittingConflictResolver(myProject, myGit, myMerger, mergingRoots, params, false).merge(); } /** * Checks if there are unmerged files (which may still be possible even if rebase or merge have finished) * @return true if there are unmerged files at */ private boolean areUnmergedFiles() { LOG.info("areUnmergedFiles: checking if there are unmerged files..."); GitConflictResolver.Params params = new GitConflictResolver.Params(); params.setErrorNotificationTitle("Update was not started"); params.setMergeDescription("Unmerged files detected. These conflicts must be resolved before update."); return !new GitMergeCommittingConflictResolver(myProject, myGit, myMerger, GitUtil.getRootsFromRepositories(myRepositories), params, false).merge(); } /** * Check if rebase is in progress, propose to resolve conflicts. * @return true if rebase is in progress, which means that update can't continue. */ private boolean checkRebaseInProgress() { LOG.info("checkRebaseInProgress: checking if there is an unfinished rebase process..."); final GitRebaser rebaser = new GitRebaser(myProject, myGit, myProgressIndicator); final Collection rebasingRoots = rebaser.getRebasingRoots(); if (rebasingRoots.isEmpty()) { return false; } LOG.info("checkRebaseInProgress: roots with unfinished rebase: " + rebasingRoots); GitConflictResolver.Params params = new GitConflictResolver.Params(); params.setErrorNotificationTitle("Can't update"); params.setMergeDescription("You have unfinished rebase process. These conflicts must be resolved before update."); params.setErrorNotificationAdditionalDescription("Then you may continue rebase.
You also may abort rebase to restore the original branch and stop rebasing."); params.setReverse(true); return !new GitConflictResolver(myProject, myGit, ServiceManager.getService(GitPlatformFacade.class), rebasingRoots, params) { @Override protected boolean proceedIfNothingToMerge() { return rebaser.continueRebase(rebasingRoots); } @Override protected boolean proceedAfterAllMerged() { return rebaser.continueRebase(rebasingRoots); } }.merge(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy