git: stash-unstash just once during the push-with-update IDEA-76774

This commit is contained in:
Kirill Likhodedov
2018-05-28 19:22:40 +03:00
parent 0a51d9ee96
commit 9d9c015d95
3 changed files with 137 additions and 49 deletions
@@ -28,6 +28,7 @@ import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.util.BackgroundTaskUtil; import com.intellij.openapi.progress.util.BackgroundTaskUtil;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.update.UpdatedFiles; import com.intellij.openapi.vcs.update.UpdatedFiles;
@@ -55,6 +56,7 @@ import git4idea.update.GitRebaseOverMergeProblem;
import git4idea.update.GitUpdateProcess; import git4idea.update.GitUpdateProcess;
import git4idea.update.GitUpdateResult; import git4idea.update.GitUpdateResult;
import git4idea.update.GitUpdater; import git4idea.update.GitUpdater;
import git4idea.util.GitPreservingProcess;
import one.util.streamex.StreamEx; import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -137,6 +139,7 @@ public class GitPushOperation {
final Map<GitRepository, GitPushRepoResult> results = ContainerUtil.newHashMap(); final Map<GitRepository, GitPushRepoResult> results = ContainerUtil.newHashMap();
Map<GitRepository, GitUpdateResult> updatedRoots = ContainerUtil.newHashMap(); Map<GitRepository, GitUpdateResult> updatedRoots = ContainerUtil.newHashMap();
GitPreservingProcess preservingProcess = null;
try { try {
Collection<GitRepository> remainingRoots = myPushSpecs.keySet(); Collection<GitRepository> remainingRoots = myPushSpecs.keySet();
@@ -182,7 +185,13 @@ public class GitPushOperation {
beforePushLabel = LocalHistory.getInstance().putSystemLabel(myProject, "Before push"); beforePushLabel = LocalHistory.getInstance().putSystemLabel(myProject, "Before push");
} }
Collection<GitRepository> rootsToUpdate = getRootsToUpdate(updateSettings, result.rejected.keySet()); Collection<GitRepository> rootsToUpdate = getRootsToUpdate(updateSettings, result.rejected.keySet());
GitUpdateResult updateResult = update(rootsToUpdate, updateSettings.getUpdateMethod(), rebaseOverMergeProblemDetected == null); Pair<GitUpdateResult, GitPreservingProcess> pair = update(rootsToUpdate, updateSettings.getUpdateMethod(),
rebaseOverMergeProblemDetected == null,
preservingProcess == null);
if (preservingProcess == null) { // multiple updates => multiple preserving processes, but all are fake ones except the first
preservingProcess = pair.second;
}
GitUpdateResult updateResult = pair.first;
for (GitRepository repository : rootsToUpdate) { for (GitRepository repository : rootsToUpdate) {
updatedRoots.put(repository, updateResult); // TODO update result in GitUpdateProcess is a single for several roots updatedRoots.put(repository, updateResult); // TODO update result in GitUpdateProcess is a single for several roots
} }
@@ -195,6 +204,9 @@ public class GitPushOperation {
if (myPushProcessCustomization != null) myPushProcessCustomization.executeAfterPush(results); if (myPushProcessCustomization != null) myPushProcessCustomization.executeAfterPush(results);
} }
finally { finally {
if (preservingProcess != null) {
preservingProcess.load();
}
if (beforePushLabel != null) { if (beforePushLabel != null) {
afterPushLabel = LocalHistory.getInstance().putSystemLabel(myProject, "After push"); afterPushLabel = LocalHistory.getInstance().putSystemLabel(myProject, "After push");
} }
@@ -453,17 +465,30 @@ public class GitPushOperation {
} }
@NotNull @NotNull
protected GitUpdateResult update(@NotNull Collection<GitRepository> rootsToUpdate, protected Pair<GitUpdateResult, GitPreservingProcess> update(@NotNull Collection<GitRepository> rootsToUpdate,
@NotNull UpdateMethod updateMethod, @NotNull UpdateMethod updateMethod,
boolean checkForRebaseOverMergeProblem) { boolean checkForRebaseOverMergeProblem,
GitUpdateResult updateResult = new GitUpdateProcess(myProject, myProgressIndicator, boolean stashBeforeUpdate) {
new HashSet<>(rootsToUpdate), UpdatedFiles.create(), GitUpdateProcess updateProcess = new GitUpdateProcess(myProject, myProgressIndicator,
checkForRebaseOverMergeProblem, false).update(updateMethod); new HashSet<>(rootsToUpdate), UpdatedFiles.create(),
checkForRebaseOverMergeProblem, false) {
@Override
protected boolean stashBeforeUpdate() {
return stashBeforeUpdate;
}
@Override
protected boolean unstashAfterUpdate() {
return false;
}
};
GitUpdateResult updateResult = updateProcess.update(updateMethod);
GitPreservingProcess preservingProcess = stashBeforeUpdate ? updateProcess.getPreservingProcess() : null;
for (GitRepository repository : rootsToUpdate) { for (GitRepository repository : rootsToUpdate) {
repository.getRoot().refresh(true, true); repository.getRoot().refresh(true, true);
repository.update(); repository.update();
} }
return updateResult; return Pair.create(updateResult, preservingProcess);
} }
private static class ResultWithOutput { private static class ResultWithOutput {
@@ -62,8 +62,6 @@ import static git4idea.util.GitUIUtil.*;
/** /**
* Handles update process (pull via merge or rebase) for several roots. * Handles update process (pull via merge or rebase) for several roots.
*
* @author Kirill Likhodedov
*/ */
public class GitUpdateProcess { public class GitUpdateProcess {
private static final Logger LOG = Logger.getInstance(GitUpdateProcess.class); private static final Logger LOG = Logger.getInstance(GitUpdateProcess.class);
@@ -80,6 +78,8 @@ public class GitUpdateProcess {
@NotNull private final ProgressIndicator myProgressIndicator; @NotNull private final ProgressIndicator myProgressIndicator;
@NotNull private final GitMerger myMerger; @NotNull private final GitMerger myMerger;
@Nullable private GitPreservingProcess myPreservingProcess;
public GitUpdateProcess(@NotNull Project project, public GitUpdateProcess(@NotNull Project project,
@Nullable ProgressIndicator progressIndicator, @Nullable ProgressIndicator progressIndicator,
@NotNull Collection<GitRepository> repositories, @NotNull Collection<GitRepository> repositories,
@@ -143,6 +143,14 @@ public class GitUpdateProcess {
return result; return result;
} }
protected boolean unstashAfterUpdate() {
return true;
}
protected boolean stashBeforeUpdate() {
return true;
}
@NotNull @NotNull
private GitUpdateResult updateImpl(@NotNull UpdateMethod updateMethod) { private GitUpdateResult updateImpl(@NotNull UpdateMethod updateMethod) {
Map<VirtualFile, GitBranchPair> trackedBranches = checkTrackedBranchesConfiguration(); Map<VirtualFile, GitBranchPair> trackedBranches = checkTrackedBranchesConfiguration();
@@ -195,12 +203,14 @@ public class GitUpdateProcess {
// save local changes if needed (update via merge may perform without saving). // save local changes if needed (update via merge may perform without saving).
final Collection<VirtualFile> myRootsToSave = ContainerUtil.newArrayList(); final Collection<VirtualFile> myRootsToSave = ContainerUtil.newArrayList();
LOG.info("updateImpl: identifying if save is needed..."); LOG.info("updateImpl: identifying if save is needed...");
for (Map.Entry<GitRepository, GitUpdater> entry : updaters.entrySet()) { if (stashBeforeUpdate()) {
GitRepository repo = entry.getKey(); for (Map.Entry<GitRepository, GitUpdater> entry : updaters.entrySet()) {
GitUpdater updater = entry.getValue(); GitRepository repo = entry.getKey();
if (updater.isSaveNeeded()) { GitUpdater updater = entry.getValue();
myRootsToSave.add(repo.getRoot()); if (updater.isSaveNeeded()) {
LOG.info("update| root " + repo + " needs save"); myRootsToSave.add(repo.getRoot());
LOG.info("update| root " + repo + " needs save");
}
} }
} }
@@ -208,30 +218,33 @@ public class GitUpdateProcess {
final Ref<Boolean> incomplete = Ref.create(false); final Ref<Boolean> incomplete = Ref.create(false);
final Ref<GitUpdateResult> compoundResult = Ref.create(); final Ref<GitUpdateResult> compoundResult = Ref.create();
final Map<GitRepository, GitUpdater> finalUpdaters = updaters; final Map<GitRepository, GitUpdater> finalUpdaters = updaters;
new GitPreservingProcess(myProject, myGit, myRootsToSave, "Update", "Remote", myPreservingProcess = new GitPreservingProcess(myProject, myGit, myRootsToSave, "Update", "Remote",
GitVcsSettings.getInstance(myProject).updateChangesPolicy(), myProgressIndicator, () -> { GitVcsSettings.getInstance(myProject).updateChangesPolicy(), myProgressIndicator, () -> {
LOG.info("updateImpl: updating..."); LOG.info("updateImpl: updating...");
GitRepository currentlyUpdatedRoot = null; GitRepository currentlyUpdatedRoot = null;
try { try {
for (GitRepository repo : myRepositories) { for (GitRepository repo : myRepositories) {
GitUpdater updater = finalUpdaters.get(repo); GitUpdater updater = finalUpdaters.get(repo);
if (updater == null) continue; if (updater == null) continue;
currentlyUpdatedRoot = repo; currentlyUpdatedRoot = repo;
GitUpdateResult res = updater.update(); GitUpdateResult res = updater.update();
LOG.info("updating root " + currentlyUpdatedRoot + " finished: " + res); LOG.info("updating root " + currentlyUpdatedRoot + " finished: " + res);
if (res == GitUpdateResult.INCOMPLETE) { if (res == GitUpdateResult.INCOMPLETE) {
incomplete.set(true); incomplete.set(true);
} }
compoundResult.set(joinResults(compoundResult.get(), res)); compoundResult.set(joinResults(compoundResult.get(), res));
} }
} }
catch (VcsException e) { catch (VcsException e) {
String rootName = (currentlyUpdatedRoot == null) ? "" : getShortRepositoryName(currentlyUpdatedRoot); String rootName = (currentlyUpdatedRoot == null) ? "" : getShortRepositoryName(currentlyUpdatedRoot);
LOG.info("Error updating changes for root " + currentlyUpdatedRoot, e); LOG.info("Error updating changes for root " + currentlyUpdatedRoot, e);
notifyImportantError(myProject, "Error updating " + rootName, notifyImportantError(myProject, "Error updating " + rootName,
"Updating " + rootName + " failed with an error: " + e.getLocalizedMessage()); "Updating " + rootName + " failed with an error: " + e.getLocalizedMessage());
} }
}).execute(() -> { });
myPreservingProcess.execute(() -> {
if (!unstashAfterUpdate()) return false;
// Note: compoundResult normally should not be null, because the updaters map was checked for non-emptiness. // 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. // 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. // In this case we don't restore local changes either, because update failed.
@@ -295,6 +308,11 @@ public class GitUpdateProcess {
return updaters; return updaters;
} }
@Nullable
public GitPreservingProcess getPreservingProcess() {
return myPreservingProcess;
}
@NotNull @NotNull
private static GitUpdateResult joinResults(@Nullable GitUpdateResult compoundResult, GitUpdateResult result) { private static GitUpdateResult joinResults(@Nullable GitUpdateResult compoundResult, GitUpdateResult result) {
if (compoundResult == null) { if (compoundResult == null) {
@@ -23,7 +23,6 @@ import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vcs.Executor import com.intellij.openapi.vcs.Executor
import com.intellij.openapi.vcs.update.FileGroup import com.intellij.openapi.vcs.update.FileGroup
import com.intellij.openapi.vcs.update.UpdatedFiles import com.intellij.openapi.vcs.update.UpdatedFiles
import com.intellij.testFramework.UsefulTestCase
import com.intellij.util.containers.ContainerUtil import com.intellij.util.containers.ContainerUtil
import git4idea.branch.GitBranchUtil import git4idea.branch.GitBranchUtil
import git4idea.config.GitVersionSpecialty import git4idea.config.GitVersionSpecialty
@@ -33,6 +32,7 @@ import git4idea.repo.GitRepository
import git4idea.test.* import git4idea.test.*
import git4idea.update.GitRebaseOverMergeProblem import git4idea.update.GitRebaseOverMergeProblem
import git4idea.update.GitUpdateResult import git4idea.update.GitUpdateResult
import git4idea.util.GitPreservingProcess
import org.junit.Assume.assumeTrue import org.junit.Assume.assumeTrue
import java.io.File import java.io.File
import java.util.Collections.singletonMap import java.util.Collections.singletonMap
@@ -141,10 +141,11 @@ class GitPushOperationSingleRepoTest : GitPushOperationBaseTest() {
val pushSpec = makePushSpec(repository, "master", "origin/master") val pushSpec = makePushSpec(repository, "master", "origin/master")
val result = object : GitPushOperation(project, pushSupport, singletonMap(repository, pushSpec), null, false, false) { val result = object : GitPushOperation(project, pushSupport, singletonMap(repository, pushSpec), null, false, false) {
override fun update(rootsToUpdate: Collection<GitRepository>, override fun update(rootsToUpdate: MutableCollection<GitRepository>,
updateMethod: UpdateMethod, updateMethod: UpdateMethod,
checkForRebaseOverMergeProblem: Boolean): GitUpdateResult { checkForRebaseOverMergeProblem: Boolean,
val updateResult = super.update(rootsToUpdate, updateMethod, checkForRebaseOverMergeProblem) stashBeforeUpdate: Boolean): Pair<GitUpdateResult, GitPreservingProcess> {
val updateResult = super.update(rootsToUpdate, updateMethod, checkForRebaseOverMergeProblem, stashBeforeUpdate)
pushCommitFromBro() pushCommitFromBro()
return updateResult return updateResult
} }
@@ -169,10 +170,11 @@ class GitPushOperationSingleRepoTest : GitPushOperationBaseTest() {
val result = object : GitPushOperation(project, pushSupport, singletonMap(repository, pushSpec), null, false, false) { val result = object : GitPushOperation(project, pushSupport, singletonMap(repository, pushSpec), null, false, false) {
internal var updateHappened: Boolean = false internal var updateHappened: Boolean = false
override fun update(rootsToUpdate: Collection<GitRepository>, override fun update(rootsToUpdate: MutableCollection<GitRepository>,
updateMethod: UpdateMethod, updateMethod: UpdateMethod,
checkForRebaseOverMergeProblem: Boolean): GitUpdateResult { checkForRebaseOverMergeProblem: Boolean,
val updateResult = super.update(rootsToUpdate, updateMethod, checkForRebaseOverMergeProblem) stashBeforeUpdate: Boolean): Pair<GitUpdateResult, GitPreservingProcess> {
val updateResult = super.update(rootsToUpdate, updateMethod, checkForRebaseOverMergeProblem, stashBeforeUpdate)
if (!updateHappened) { if (!updateHappened) {
updateHappened = true updateHappened = true
pushCommitFromBro() pushCommitFromBro()
@@ -426,6 +428,49 @@ class GitPushOperationSingleRepoTest : GitPushOperationBaseTest() {
assertEquals(UpdateMethod.BRANCH_DEFAULT, settings.updateType) assertEquals(UpdateMethod.BRANCH_DEFAULT, settings.updateType)
} }
// IDEA-76774
fun `test local changes are stashed-unstashed only once for several push rejections`() {
pushCommitFromBro()
cd(repository)
makeCommit("afile.txt")
val localFile = repository.file("local-changes.txt")
localFile.create("Local changes").add()
updateRepositories()
refresh()
updateChangeListManager()
agreeToUpdate(GitRejectedPushUpdateDialog.REBASE_EXIT_CODE)
val pushSpec = makePushSpec(repository, "master", "origin/master")
var stashCalled = 0
git.stashListener = {
stashCalled++
}
var pushRejected = 0
val result = object : GitPushOperation(project, pushSupport, singletonMap(repository, pushSpec), null, false, false) {
override fun update(rootsToUpdate: MutableCollection<GitRepository>,
updateMethod: UpdateMethod,
checkForRebaseOverMergeProblem: Boolean,
stashBeforeUpdate: Boolean): Pair<GitUpdateResult, GitPreservingProcess> {
val updateResult = super.update(rootsToUpdate, updateMethod, checkForRebaseOverMergeProblem, stashBeforeUpdate)
if (pushRejected < 2) {
pushCommitFromBro()
pushRejected++
}
return updateResult
}
}.execute()
assertResult(SUCCESS, 1, "master", "origin/master", result)
assertTrue("Locally changed file wasn't unstashed", localFile.exists())
repository.assertStatus(localFile.file, 'A')
assertEquals("Stash should have been called only once", 1, stashCalled)
}
private fun generateUpdateNeeded() { private fun generateUpdateNeeded() {
pushCommitFromBro() pushCommitFromBro()
cd(repository) cd(repository)
@@ -464,8 +509,8 @@ class GitPushOperationSingleRepoTest : GitPushOperationBaseTest() {
updatedFiles: List<String>?, updatedFiles: List<String>?,
actualResult: GitPushResult) { actualResult: GitPushResult) {
assertResult(type, pushedCommits, from, to, updateResult, actualResult.results[repository]!!) assertResult(type, pushedCommits, from, to, updateResult, actualResult.results[repository]!!)
UsefulTestCase.assertSameElements("Updated files set is incorrect", if (updatedFiles != null)
getUpdatedFiles(actualResult.updatedFiles), ContainerUtil.notNullize(updatedFiles)) assertSameElements("Updated files set is incorrect", getUpdatedFiles(actualResult.updatedFiles), updatedFiles)
} }
private fun getUpdatedFiles(updatedFiles: UpdatedFiles): Collection<String> { private fun getUpdatedFiles(updatedFiles: UpdatedFiles): Collection<String> {