blob: e78039ef9d71649246cd501d6a21c7bdd8c62207 [file] [log] [blame]
/*
* Copyright 2000-2011 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.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.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.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<GitRepository> myRepositories;
private final UpdatedFiles myUpdatedFiles;
private final ProgressIndicator myProgressIndicator;
private final GitMerger myMerger;
private final GitChangesSaver mySaver;
private final Map<VirtualFile, GitBranchPair> myTrackedBranches = new HashMap<VirtualFile, GitBranchPair>();
private GitUpdateResult myResult;
private final Collection<VirtualFile> myRootsToSave;
public enum UpdateMethod {
MERGE,
REBASE,
READ_FROM_SETTINGS
}
public GitUpdateProcess(@NotNull Project project, @NotNull GitPlatformFacade platformFacade, @Nullable ProgressIndicator progressIndicator,
@NotNull Collection<GitRepository> repositories, @NotNull UpdatedFiles updatedFiles) {
myProject = project;
myRepositories = repositories;
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<VirtualFile>(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) {
GitUtil.workingTreeChangeStarted(myProject);
try {
myResult = updateImpl(updateMethod, continuationContext);
}
finally {
GitUtil.workingTreeChangeFinished(myProject);
}
}
};
GitComplexProcess.execute(myProject, "update", updateOperation);
myProgressIndicator.setText(oldText);
return myResult;
}
@NotNull
private GitUpdateResult updateImpl(@NotNull UpdateMethod updateMethod, ContinuationContext context) {
Map<VirtualFile, GitUpdater> 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;
}
// save local changes if needed (update via merge may perform without saving).
LOG.info("updateImpl: identifying if save is needed...");
for (Map.Entry<VirtualFile, GitUpdater> 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.<br/>" +
"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<VirtualFile, GitUpdater> 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 Map<VirtualFile, GitUpdater> tryFastForwardMergeForRebaseUpdaters(@NotNull Map<VirtualFile, GitUpdater> updaters) {
Map<VirtualFile, GitUpdater> modifiedUpdaters = new HashMap<VirtualFile, GitUpdater>();
Map<VirtualFile, Collection<Change>> changesUnderRoots =
new LocalChangesUnderRoots(ChangeListManager.getInstance(myProject), ProjectLevelVcsManager.getInstance(myProject)).
getChangesUnderRoots(updaters.keySet());
for (Map.Entry<VirtualFile, GitUpdater> updaterEntry : updaters.entrySet()) {
VirtualFile root = updaterEntry.getKey();
GitUpdater updater = updaterEntry.getValue();
Collection<Change> 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<VirtualFile, GitUpdater> defineUpdaters(@NotNull UpdateMethod updateMethod) throws VcsException {
final Map<VirtualFile, GitUpdater> updaters = new HashMap<VirtualFile, GitUpdater>();
LOG.info("updateImpl: defining updaters...");
for (GitRepository repository : myRepositories) {
VirtualFile root = repository.getRoot();
final GitUpdater updater;
if (updateMethod == UpdateMethod.MERGE) {
updater = new GitMergeUpdater(myProject, myGit, root, myTrackedBranches, myProgressIndicator, myUpdatedFiles);
} else if (updateMethod == UpdateMethod.REBASE) {
updater = new GitRebaseUpdater(myProject, myGit, root, myTrackedBranches, myProgressIndicator, myUpdatedFiles);
} else {
updater = GitUpdater.getUpdater(myProject, myGit, myTrackedBranches, root, myProgressIndicator, myUpdatedFiles);
}
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<VcsException>() {
@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.<br/>" + 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,<br/>" +
"<code>git branch --set-upstream " + branchName + " origin/" + branchName + "</code>");
return false;
}
myTrackedBranches.put(root, new GitBranchPair(branch, trackInfo.getRemoteBranch()));
}
return true;
}
private String rootStringIfNeeded(@NotNull VirtualFile root) {
if (myRepositories.size() < 2) {
return ".<br/>";
}
return "<br/>in Git repository " + code(root.getPresentableUrl()) + "<br/>";
}
/**
* 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<VirtualFile> 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<VirtualFile> 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 <b>continue rebase</b>. <br/> You also may <b>abort rebase</b> 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();
}
}