git4idea.update.GitUpdateProcess Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of git4idea Show documentation
Show all versions of git4idea Show documentation
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();
}
}