From e3deb2f4fa810eb539b25ce4674f404dfd0a96a8 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Fri, 10 Mar 2017 10:24:18 +0000 Subject: [PATCH] Refresh only that part of the VFS that has been changed by Git operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refreshing the whole VFS is reliable but slow, especially in case of feature branches, when branches diverge just a little, and refreshing only a couple of files is required which is much faster. On the other hand, collecting the difference between current working tree and the target reference is quick enough: even `git diff origin/143` takes ~1–2s. A registry option is introduced to be able to turn the optimization off in case there is a report that not files were refreshed. Following operations are optimized: * reset (including "undo commit"): uses `git diff ` which compares with working tree: working tree may change as the result of reset --hard. * checkout and merge: use `git diff HEAD..`. Comparing with working tree is unnecessary ("local changes would be overwritten by checkout/merge" means that local changes are not touched by these operations). Merge should actually compare with the merge base (`git diff HEAD...`), but that's much slower, while refreshing several more files is not that slower. * rebase can happen together with checkout (`git rebase master topic`), so at first it checks the diff between current branch and the branch which is to be checked out; and then it additionally checks the diff for the rebase itself (again, should be calculated with "..." for more precise results, but ".." is used because it is faster). Note that the reason ".." can be used instead of "..." is because we don't care about the changes themselves and how accurate they are: we just need a superset of affected files. Only trivial cases are covered: e.g. smart checkout, rollbacks, conflicts, continue rebase, etc. still perform full refresh for now, since they are relatively rare and a bit trickier). This fixes IDEA-163043 --- .../util/resources/misc/registry.properties | 5 ++ plugins/git4idea/src/git4idea/GitUtil.java | 33 +++++++++++ .../git4idea/branch/GitBranchOperation.java | 10 ---- .../git4idea/branch/GitCheckoutOperation.java | 25 +++----- .../git4idea/branch/GitMergeOperation.java | 24 ++++---- .../src/git4idea/changes/GitChangeUtils.java | 34 ++++++++++- .../src/git4idea/rebase/GitRebaseProcess.java | 39 +++++++++--- .../src/git4idea/reset/GitResetOperation.java | 10 +++- .../tests/git4idea/reset/GitResetTest.kt | 59 +++++++++++++++++++ .../tests/git4idea/test/GitSingleRepoTest.kt | 8 +-- 10 files changed, 187 insertions(+), 60 deletions(-) create mode 100644 plugins/git4idea/tests/git4idea/reset/GitResetTest.kt diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index b970eda80b06..f1e01b2e792e 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -916,6 +916,11 @@ git.diff.renameLimit.description=Sets diff.renameLimit configuration variable va Setting this value too high (or zero) may lead to increased memory consumption by a Git process and longer execution time.\ This variable only used when reading git commits for indexing.\ Value from git configuration (1000 by default) is used for other purposes (eg for displaying changes in changes view). +git.refresh.vfs.total=false +git.refresh.vfs.total.description=If set to true, the whole Git repository root will be refreshed after performing a Git operation.\ + Otherwise only the affected files will be refreshed.\ + Affects only a subset of Git operations.\ + The option is needed to debug possible "incomplete VFS refresh" reports, if they appear. hg4idea.process.ignored=true hg4idea.process.ignored.description=Process and highlight mercurial ignored files. \ diff --git a/plugins/git4idea/src/git4idea/GitUtil.java b/plugins/git4idea/src/git4idea/GitUtil.java index ab55b8da5b31..140989c9387e 100644 --- a/plugins/git4idea/src/git4idea/GitUtil.java +++ b/plugins/git4idea/src/git4idea/GitUtil.java @@ -25,6 +25,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogBuilder; import com.intellij.openapi.ui.ex.MultiLineLabel; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.AbstractVcsHelper; import com.intellij.openapi.vcs.FilePath; @@ -34,9 +35,11 @@ import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vcs.changes.ChangeListManagerEx; import com.intellij.openapi.vcs.changes.ContentRevision; +import com.intellij.openapi.vcs.update.RefreshVFsSynchronously; import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.Consumer; import com.intellij.util.Function; @@ -71,6 +74,7 @@ import java.util.stream.Collectors; import static com.intellij.dvcs.DvcsUtil.getShortRepositoryName; import static com.intellij.dvcs.DvcsUtil.joinShortNames; import static com.intellij.util.ObjectUtils.assertNotNull; +import static java.util.Arrays.stream; /** * Git utility/helper methods @@ -1033,4 +1037,33 @@ public class GitUtil { OpenTHashSet allChanges = new OpenTHashSet<>(changeListManager.getAllChanges()); return ContainerUtil.mapNotNull(originalChanges, allChanges::get); } + + /** + * A convenience method to refresh either a part of the VFS modified by the given changes, or the whole root recursively. + * + * @param changes The changes which files were modified by a Git operation. + * If null, the whole root is refreshed. Otherwise, only the files touched by these changes. + */ + public static void refreshVfs(@NotNull VirtualFile root, @Nullable Collection changes) { + if (changes == null || Registry.is("git.refresh.vfs.total")) { + VfsUtil.markDirtyAndRefresh(false, true, false, root); + } + else { + RefreshVFsSynchronously.updateChanges(changes); + } + } + + public static void updateAndRefreshVfs(@NotNull GitRepository repository, @Nullable Collection changes) { + repository.update(); + refreshVfs(repository.getRoot(), changes); + } + + public static void updateAndRefreshVfs(GitRepository... repositories) { + // repositories state will be auto-updated with the following VFS refresh => there is no need to call GitRepository#update() + // but we want repository state to be updated as soon as possible, without waiting for the whole VFS refresh to complete. + stream(repositories).forEach(GitRepository::update); + for (GitRepository repository : repositories) { + refreshVfs(repository.getRoot(), null); + } + } } diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java index f352d3807e34..128479d5cf3b 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java @@ -27,7 +27,6 @@ import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.VcsNotifier; import com.intellij.openapi.vcs.changes.Change; -import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.containers.MultiMap; import git4idea.GitUtil; @@ -293,15 +292,6 @@ abstract class GitBranchOperation { myUiHandler.showUnmergedFilesNotification(getOperationName(), getRepositories()); } - /** - * Asynchronously refreshes the VFS root directory of the given repository. - */ - protected void refreshRoot(@NotNull GitRepository repository) { - // marking all files dirty, because sometimes FileWatcher is unable to process such a large set of changes that can happen during - // checkout on a large repository: IDEA-89944 - VfsUtil.markDirtyAndRefresh(false, true, false, repository.getRoot()); - } - protected void fatalLocalChangesError(@NotNull String reference) { String title = String.format("Couldn't %s %s", getOperationName(), reference); if (wereSuccessful()) { diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index 3c0d2e054ee1..c176ad93db14 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -27,6 +27,7 @@ import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ArrayUtil; import git4idea.GitUtil; +import git4idea.changes.GitChangeUtils; import git4idea.commands.*; import git4idea.repo.GitRepository; import git4idea.util.GitPreservingProcess; @@ -39,12 +40,11 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import static com.intellij.util.containers.UtilKt.getIfSingle; -import static git4idea.GitUtil.getRootsFromRepositories; +import static git4idea.GitUtil.*; import static git4idea.branch.GitSmartOperationDialog.Choice.FORCE; import static git4idea.branch.GitSmartOperationDialog.Choice.SMART; import static git4idea.config.GitVcsSettings.UpdateChangesPolicy.STASH; import static git4idea.util.GitUIUtil.code; -import static java.util.Arrays.stream; /** * Represents {@code git checkout} operation. @@ -85,8 +85,10 @@ class GitCheckoutOperation extends GitBranchOperation { try { while (hasMoreRepositories() && !fatalErrorHappened) { final GitRepository repository = next(); - VirtualFile root = repository.getRoot(); + + Collection changes = GitChangeUtils.getDiff(repository, HEAD, myStartPointReference); + GitLocalChangesWouldBeOverwrittenDetector localChangesDetector = new GitLocalChangesWouldBeOverwrittenDetector(root, GitLocalChangesWouldBeOverwrittenDetector.Operation.CHECKOUT); GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_CHECKOUT); @@ -97,7 +99,7 @@ class GitCheckoutOperation extends GitBranchOperation { GitCommandResult result = myGit.checkout(repository, myStartPointReference, myNewBranch, false, myDetach, localChangesDetector, unmergedFiles, unknownPathspec, untrackedOverwrittenByCheckout); if (result.success()) { - refresh(repository); + updateAndRefreshVfs(repository, changes); markSuccessful(repository); } else if (unmergedFiles.hasHappened()) { @@ -168,7 +170,7 @@ class GitCheckoutOperation extends GitBranchOperation { if (smartCheckedOutSuccessfully) { for (GitRepository conflictingRepository : allConflictingRepositories) { markSuccessful(conflictingRepository); - refresh(conflictingRepository); + updateAndRefreshVfs(conflictingRepository); } return true; } @@ -181,7 +183,7 @@ class GitCheckoutOperation extends GitBranchOperation { boolean forceCheckoutSucceeded = checkoutOrNotify(allConflictingRepositories, myStartPointReference, myNewBranch, true); if (forceCheckoutSucceeded) { markSuccessful(ArrayUtil.toObjectArray(allConflictingRepositories, GitRepository.class)); - refresh(ArrayUtil.toObjectArray(allConflictingRepositories, GitRepository.class)); + updateAndRefreshVfs(ArrayUtil.toObjectArray(allConflictingRepositories, GitRepository.class)); } return forceCheckoutSucceeded; } @@ -223,7 +225,7 @@ class GitCheckoutOperation extends GitBranchOperation { */ deleteResult.append(repository, myGit.branchDelete(repository, myNewBranch, true)); } - refresh(repository); + updateAndRefreshVfs(repository); } if (!checkoutResult.totalSuccess() || !deleteResult.totalSuccess()) { StringBuilder message = new StringBuilder(); @@ -281,15 +283,6 @@ class GitCheckoutOperation extends GitBranchOperation { return false; } - private void refresh(GitRepository... repositories) { - // repositories state will be auto-updated with the following VFS refresh => there is no need to call GitRepository#update() - // but we want repository state to be updated as soon as possible, without waiting for the whole VFS refresh to complete. - stream(repositories).forEach(GitRepository::update); - for (GitRepository repository : repositories) { - refreshRoot(repository); - } - } - private class RollbackOperationNotificationListener implements NotificationListener { @Override public void hyperlinkUpdate(@NotNull Notification notification, diff --git a/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java index 679078906833..132a1b11a64e 100644 --- a/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java @@ -28,6 +28,7 @@ import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vfs.VirtualFile; import git4idea.GitUtil; +import git4idea.changes.GitChangeUtils; import git4idea.commands.*; import git4idea.merge.GitMergeCommittingConflictResolver; import git4idea.merge.GitMerger; @@ -40,6 +41,8 @@ import javax.swing.event.HyperlinkEvent; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; +import static git4idea.GitUtil.HEAD; +import static git4idea.GitUtil.updateAndRefreshVfs; import static git4idea.config.GitVcsSettings.UpdateChangesPolicy.STASH; class GitMergeOperation extends GitBranchOperation { @@ -75,8 +78,10 @@ class GitMergeOperation extends GitBranchOperation { while (hasMoreRepositories() && !fatalErrorHappened) { final GitRepository repository = next(); LOG.info("next repository: " + repository); - VirtualFile root = repository.getRoot(); + + Collection changes = GitChangeUtils.getDiff(repository, HEAD, myBranchToMerge); + GitLocalChangesWouldBeOverwrittenDetector localChangesDetector = new GitLocalChangesWouldBeOverwrittenDetector(root, GitLocalChangesWouldBeOverwrittenDetector.Operation.MERGE); GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_MERGE); @@ -90,7 +95,7 @@ class GitMergeOperation extends GitBranchOperation { alreadyUpToDateDetector); if (result.success()) { LOG.info("Merged successfully"); - refresh(repository); + updateAndRefreshVfs(repository, changes); markSuccessful(repository); if (alreadyUpToDateDetector.hasHappened()) { alreadyUpToDateRepositories += 1; @@ -111,7 +116,7 @@ class GitMergeOperation extends GitBranchOperation { else if (mergeConflict.hasHappened()) { LOG.info("Merge conflict"); myConflictedRepositories.put(repository, Boolean.FALSE); - refresh(repository); + updateAndRefreshVfs(repository); markSuccessful(repository); } else if (untrackedOverwrittenByMerge.wasMessageDetected()) { @@ -233,7 +238,7 @@ class GitMergeOperation extends GitBranchOperation { if (!result.success()) { if (mergeConflict.hasHappened()) { myConflictedRepositories.put(repository, Boolean.TRUE); - refresh(repository); + updateAndRefreshVfs(repository); markSuccessful(repository); } else { @@ -242,7 +247,7 @@ class GitMergeOperation extends GitBranchOperation { } } else { - refresh(repository); + updateAndRefreshVfs(repository); markSuccessful(repository); } } @@ -313,7 +318,7 @@ class GitMergeOperation extends GitBranchOperation { @NotNull private GitCommandResult rollbackMerge(@NotNull GitRepository repository) { GitCommandResult result = myGit.resetMerge(repository, null); - refresh(repository); + updateAndRefreshVfs(repository); return result; } @@ -342,13 +347,6 @@ class GitMergeOperation extends GitBranchOperation { return "merge"; } - private void refresh(GitRepository... repositories) { - for (GitRepository repository : repositories) { - refreshRoot(repository); - repository.update(); - } - } - private class MyMergeConflictResolver extends GitMergeCommittingConflictResolver { public MyMergeConflictResolver() { super(GitMergeOperation.this.myProject, myGit, new GitMerger(GitMergeOperation.this.myProject), diff --git a/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java b/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java index 0be754cf77e3..d30ca12987f3 100644 --- a/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java +++ b/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java @@ -34,6 +34,7 @@ import git4idea.commands.GitCommand; import git4idea.commands.GitHandler; import git4idea.commands.GitSimpleHandler; import git4idea.history.browser.SHAHash; +import git4idea.repo.GitRepository; import git4idea.util.StringScanner; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -43,6 +44,7 @@ import java.io.File; import java.util.*; import static com.intellij.util.ObjectUtils.assertNotNull; +import static java.util.Collections.emptySet; /** * Change related utilities @@ -391,7 +393,7 @@ public class GitChangeUtils { String output = getDiffOutput(project, root, range, dirtyPaths); Collection changes = new ArrayList<>(); - parseChanges(project, root, newRev, oldRev, output, changes, Collections.emptySet()); + parseChanges(project, root, newRev, oldRev, output, changes, emptySet()); return changes; } @@ -402,7 +404,7 @@ public class GitChangeUtils { String output = diff.run(); Collection changes = new ArrayList<>(); - parseChanges(project, root, null, GitRevisionNumber.HEAD, output, changes, Collections.emptySet()); + parseChanges(project, root, null, GitRevisionNumber.HEAD, output, changes, emptySet()); return changes; } @@ -415,7 +417,7 @@ public class GitChangeUtils { Collection changes = new ArrayList<>(); final GitRevisionNumber revisionNumber = resolveReference(project, root, oldRevision); parseChanges(project, root, reverse ? revisionNumber : null, reverse ? null : revisionNumber, output, changes, - Collections.emptySet()); + emptySet()); return changes; } @@ -465,4 +467,30 @@ public class GitChangeUtils { return handler; } + /** + * Returns the changes between current working tree state and the given ref, or null if fails to get the diff. + */ + @Nullable + public static Collection getDiffWithWorkingTree(@NotNull GitRepository repository, @NotNull String refToCompare) { + Collection changes; + try { + changes = getDiffWithWorkingDir(repository.getProject(), repository.getRoot(), refToCompare, null, false); + } + catch (VcsException e) { + LOG.warn("Couldn't collect diff", e); + changes = null; + } + return changes; + } + + @Nullable + public static Collection getDiff(@NotNull GitRepository repository, @NotNull String oldRevision, @NotNull String newRevision) { + try { + return getDiff(repository.getProject(), repository.getRoot(), oldRevision, newRevision, null); + } + catch (VcsException e) { + LOG.warn("Couldn't collect changes between " + oldRevision + " and " + newRevision, e); + return null; + } + } } diff --git a/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java b/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java index 4d250ad384df..a02ebfb90b30 100644 --- a/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java +++ b/plugins/git4idea/src/git4idea/rebase/GitRebaseProcess.java @@ -30,6 +30,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.VcsNotifier; +import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; @@ -38,6 +39,7 @@ import com.intellij.util.ThreeState; import com.intellij.util.containers.MultiMap; import git4idea.GitUtil; import git4idea.branch.GitRebaseParams; +import git4idea.changes.GitChangeUtils; import git4idea.commands.Git; import git4idea.commands.GitCommandResult; import git4idea.commands.GitLineHandlerListener; @@ -55,19 +57,16 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.event.HyperlinkEvent; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import static com.intellij.dvcs.DvcsUtil.getShortRepositoryName; import static com.intellij.openapi.vfs.VfsUtilCore.toVirtualFileArray; import static com.intellij.util.ObjectUtils.assertNotNull; +import static com.intellij.util.ObjectUtils.coalesce; import static com.intellij.util.ObjectUtils.notNull; import static com.intellij.util.containers.ContainerUtil.*; import static com.intellij.util.containers.ContainerUtilRt.newArrayList; -import static com.intellij.util.containers.ContainerUtilRt.newLinkedHashSet; -import static git4idea.GitUtil.getRootsFromRepositories; +import static git4idea.GitUtil.*; import static java.util.Collections.singleton; public class GitRebaseProcess { @@ -117,7 +116,6 @@ public class GitRebaseProcess { LOG.debug("Started rebase with the following spec: " + myRebaseSpec); Map statuses = newLinkedHashMap(myRebaseSpec.getStatuses()); - Collection toRefresh = newLinkedHashSet(); List repositoriesToRebase = myRepositoryManager.sortByDependency(myRebaseSpec.getIncompleteRepositories()); AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject); try { @@ -130,11 +128,13 @@ public class GitRebaseProcess { customMode = myCustomMode == null ? GitRebaseResumeMode.CONTINUE : myCustomMode; } + Collection changes = collectFutureChanges(repository); + GitRebaseStatus rebaseStatus = rebaseSingleRoot(repository, customMode, getSuccessfulRepositories(statuses)); repository.update(); // make the repo state info actual ASAP statuses.put(repository, rebaseStatus); if (shouldBeRefreshed(rebaseStatus)) { - toRefresh.add(repository); + refreshVfs(repository.getRoot(), changes); } if (rebaseStatus.getType() != GitRebaseStatus.Type.SUCCESS) { failed = repository; @@ -146,7 +146,6 @@ public class GitRebaseProcess { LOG.debug("Rebase completed successfully."); mySaver.load(); } - refresh(toRefresh); if (failed == null) { notifySuccess(getSuccessfulRepositories(statuses), getSkippedCommits(statuses)); } @@ -165,6 +164,28 @@ public class GitRebaseProcess { } } + @Nullable + private Collection collectFutureChanges(@NotNull GitRepository repository) { + GitRebaseParams params = myRebaseSpec.getParams(); + if (params == null) return null; + + Collection changes = new ArrayList<>(); + String branch = params.getBranch(); + if (branch != null) { + Collection changesFromCheckout = GitChangeUtils.getDiff(repository, HEAD, branch); + if (changesFromCheckout == null) return null; + changes.addAll(changesFromCheckout); + } + + String rev1 = coalesce(params.getNewBase(), branch, HEAD); + String rev2 = params.getUpstream(); + Collection changesFromRebase = GitChangeUtils.getDiff(repository, rev1, rev2); + if (changesFromRebase == null) return null; + + changes.addAll(changesFromRebase); + return changes; + } + private void saveUpdatedSpec(@NotNull Map statuses) { if (myRebaseSpec.shouldBeSaved()) { GitRebaseSpec newRebaseInfo = myRebaseSpec.cloneWithNewStatuses(statuses); diff --git a/plugins/git4idea/src/git4idea/reset/GitResetOperation.java b/plugins/git4idea/src/git4idea/reset/GitResetOperation.java index d88ce6c88149..1e90db62dc0a 100644 --- a/plugins/git4idea/src/git4idea/reset/GitResetOperation.java +++ b/plugins/git4idea/src/git4idea/reset/GitResetOperation.java @@ -26,7 +26,6 @@ import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsNotifier; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager; -import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; @@ -34,6 +33,7 @@ import com.intellij.vcs.log.Hash; import git4idea.GitUtil; import git4idea.branch.GitBranchUiHandlerImpl; import git4idea.branch.GitSmartOperationDialog; +import git4idea.changes.GitChangeUtils; import git4idea.commands.Git; import git4idea.commands.GitCommandResult; import git4idea.commands.GitLocalChangesWouldBeOverwrittenDetector; @@ -47,10 +47,12 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import static git4idea.GitUtil.updateAndRefreshVfs; import static git4idea.commands.GitLocalChangesWouldBeOverwrittenDetector.Operation.RESET; public class GitResetOperation { + @NotNull private final Project myProject; @NotNull private final Map myCommits; @NotNull private final GitResetMode myMode; @@ -83,6 +85,8 @@ public class GitResetOperation { String target = entry.getValue().asString(); GitLocalChangesWouldBeOverwrittenDetector detector = new GitLocalChangesWouldBeOverwrittenDetector(root, RESET); + Collection changes = GitChangeUtils.getDiffWithWorkingTree(repository, target); + GitCommandResult result = myGit.reset(repository, myMode, target, detector); if (!result.success() && detector.wasMessageDetected()) { GitCommandResult smartResult = proposeSmartReset(detector, repository, target); @@ -91,8 +95,8 @@ public class GitResetOperation { } } results.put(repository, result); - repository.update(); - VfsUtil.markDirtyAndRefresh(false, true, false, root); + + updateAndRefreshVfs(repository, changes); VcsDirtyScopeManager.getInstance(myProject).dirDirtyRecursively(root); } } diff --git a/plugins/git4idea/tests/git4idea/reset/GitResetTest.kt b/plugins/git4idea/tests/git4idea/reset/GitResetTest.kt new file mode 100644 index 000000000000..ff8ff9689733 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/reset/GitResetTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2017 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.reset + +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.vcs.FileStatus +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.vcs.log.Hash +import com.intellij.vcs.log.impl.HashImpl +import git4idea.test.GitSingleRepoTest +import git4idea.test.file +import git4idea.test.last + +class GitResetTest : GitSingleRepoTest() { + + fun `test file is refreshed on hard reset`() { + val (oldHash, vf) = prepare() + + GitResetOperation(myProject, mapOf(myRepo to oldHash), GitResetMode.HARD, EmptyProgressIndicator()).execute() + + assertSuccessfulNotification("Reset successful") + assertEquals("Branch is on incorrect point", oldHash.asString(), last()) + assertEquals("VirtualFile wasn't refreshed", "initial\n", String(vf.contentsToByteArray())) + } + + fun `test file status is refreshed on soft reset`() { + val (oldHash, vf) = prepare() + + GitResetOperation(myProject, mapOf(myRepo to oldHash), GitResetMode.SOFT, EmptyProgressIndicator()).execute() + + assertSuccessfulNotification("Reset successful") + assertEquals("Branch is on incorrect point", oldHash.asString(), last()) + changeListManager.ensureUpToDate(false) + assertEquals("File status wasn't refreshed", FileStatus.MODIFIED, changeListManager.getChange(vf)!!.fileStatus) + } + + private fun prepare(): Pair { + val file = file("f.txt").create().write("initial\n") + val prevHash = HashImpl.build(file.addCommit("created").hash()) + file.append("more\n") + file.addCommit("Added more") + val vf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file.file)!! + return Pair(prevHash, vf) + } +} \ No newline at end of file diff --git a/plugins/git4idea/tests/git4idea/test/GitSingleRepoTest.kt b/plugins/git4idea/tests/git4idea/test/GitSingleRepoTest.kt index 4030928aaaaf..b378075eada9 100644 --- a/plugins/git4idea/tests/git4idea/test/GitSingleRepoTest.kt +++ b/plugins/git4idea/tests/git4idea/test/GitSingleRepoTest.kt @@ -41,9 +41,7 @@ abstract class GitSingleRepoTest : GitPlatformTest() { cd(myProjectPath) } - protected open fun makeInitialCommit(): Boolean { - return true - } + protected open fun makeInitialCommit() = true protected fun VcsConfiguration.StandardConfirmation.doSilently() = AbstractVcsTestCase.setStandardConfirmation(myProject, GitVcs.NAME, this, DO_ACTION_SILENTLY) @@ -74,9 +72,7 @@ abstract class GitSingleRepoTest : GitPlatformTest() { updateChangeListManager() } - protected fun build(f: RepoBuilder.() -> Unit) { - build(myRepo, f) - } + protected fun build(f: RepoBuilder.() -> Unit) = build(myRepo, f) protected fun assertUnversioned(file: VirtualFile) { assertTrue("File should be unversioned! All changes: " + GitUtil.getLogString(myProjectPath, changeListManager.allChanges),