IDEA-118140 don't add unversioned file to VCS on file move

The bug was introduced in 783963b while fixing IDEA-153272.
Originally the code used to avoid registering unversioned and ignored
moved files in performMoveRename(), thus they were not passed to
`git add/rm` in executeMoveRename(), thus they were correctly not added
to the VCS.

But that behavior led to IDEA-153272 (both files are lost
when unversioned file is moved to overwrite existing file of the same name)
Here is what actually happened:
1. Delete original file to let it be overwritten.
2. beforeFileDeleted: remember to git rm this file later.
3. Move unversioned file to the place of original.
4. Don't remember the file because it is unversioned.
5. git rm the original file from executeDelete() after the command finished.
=> both unversioned (during the move) and original (during git rm)
were deleted.

The fix in 783963b was to avoid #4 in this sequence. However,
moved unversioned files were not filtered anymore, and thus were later
passed to executeMoveRename and further to `git add/rm/mv`.

This fix is actually a partial revert of 783963b: it returns filtering
by UNKNOWN/INGORED file status back (with a special handle for Perforce),
but in order to keep IDEA-153272 fixed it moves the unversioned & ignored check
from performMoveRename to executeMoveRename. The latter is called after
files are checked for doNotDeleteAddedCopiedOrMovedFiles => file is
not scheduled for deletion anymore even if it was unversioned moved file.

Tests were added for both IDEA-153272 and IDEA-118140.
This commit is contained in:
Kirill Likhodedov
2016-07-23 13:45:38 +03:00
parent 73a89dc0c6
commit a30e7bbd67
3 changed files with 144 additions and 3 deletions
@@ -266,6 +266,10 @@ public abstract class VcsVFSListener implements Disposable {
}
}
protected boolean filterOutUnknownFiles() {
return true;
}
protected void processMovedFile(VirtualFile file, String newParentPath, String newName) {
final FileStatus status = FileStatusManager.getInstance(myProject).getStatus(file);
LOG.debug("Checking moved file " + file + "; status=" + status);
@@ -292,7 +296,10 @@ public abstract class VcsVFSListener implements Disposable {
final List<MovedFileInfo> movedFiles = new ArrayList<MovedFileInfo>(myMovedFiles);
LOG.debug("executeMoveRename " + movedFiles);
myMovedFiles.clear();
performMoveRename(movedFiles);
performMoveRename(ContainerUtil.filter(movedFiles, info -> {
FileStatus status = FileStatusManager.getInstance(myProject).getStatus(info.myFile);
return !(status == FileStatus.UNKNOWN && filterOutUnknownFiles()) && status != FileStatus.IGNORED;
}));
}
protected VcsDeleteType needConfirmDeletion(final VirtualFile file) {
@@ -19,6 +19,7 @@ import com.intellij.ide.highlighter.ProjectFileType
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vcs.changes.ChangeListManager
import com.intellij.openapi.vcs.changes.ChangeListManagerImpl
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
@@ -39,7 +40,7 @@ abstract class VcsPlatformTest : PlatformTestCase() {
private lateinit var myTestStartedIndicator: String
protected lateinit var changeListManager: ChangeListManager
protected lateinit var changeListManager: ChangeListManagerImpl
@Throws(Exception::class)
override fun setUp() {
@@ -56,7 +57,7 @@ abstract class VcsPlatformTest : PlatformTestCase() {
myProjectRoot = myProject.baseDir
myProjectPath = myProjectRoot.path
changeListManager = ChangeListManager.getInstance(myProject)
changeListManager = ChangeListManager.getInstance(myProject) as ChangeListManagerImpl
}
@Throws(Exception::class)
@@ -0,0 +1,133 @@
/*
* Copyright 2000-2016 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.checkin
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.vcs.VcsConfiguration
import com.intellij.openapi.vcs.VcsConfiguration.StandardConfirmation.ADD
import com.intellij.openapi.vcs.VcsShowConfirmationOption.Value.DO_ACTION_SILENTLY
import com.intellij.openapi.vcs.VcsShowConfirmationOption.Value.DO_NOTHING_SILENTLY
import com.intellij.openapi.vcs.VcsTestUtil
import com.intellij.openapi.vcs.VcsTestUtil.renameFileInCommand
import com.intellij.openapi.vcs.VcsVFSListener
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.testFramework.vcs.AbstractVcsTestCase.setStandardConfirmation
import com.intellij.vcsUtil.VcsUtil
import git4idea.GitUtil.getLogString
import git4idea.GitVcs
import git4idea.test.GitExecutor.*
import git4idea.test.GitSingleRepoTest
import java.io.File
class GitMoveTest : GitSingleRepoTest() {
override fun getDebugLogCategories() = super.getDebugLogCategories().plus("#" + VcsVFSListener::class.java.name)
fun `test unchanged file should be added to Git on move`() {
ADD.doNothing()
val file = "before.txt"
echo(file, "some\ncontent\nere")
addCommit("created $file")
val vf = LocalFileSystem.getInstance().refreshAndFindFileByPath(myProjectPath + "/$file")!!
renameFile(vf, "ver-ren.txt")
assertTrue("File should versioned! All changes: " + getLogString(myProjectPath, changeListManager.allChanges),
!changeListManager.isUnversioned(vf))
val change = changeListManager.getChange(vf)!!
assertTrue("Change should be rename: " + change, change.isRenamed)
}
// IDEA-153272
fun `test move unversioned file over existing file should keep the file`() {
ADD.doNothing()
val content = "original content"
val fileName = "file.txt"
val originalDir = myProjectRoot.createDir("original")
val unversionedDir = myProjectRoot.createDir("unv")
val original = originalDir.createFile(fileName, content)
val unversioned = unversionedDir.createFile(fileName, content)
val originalFile = File(original.path)
val unversionedFile = File(unversioned.path)
git("add original/$fileName")
git("commit -m msg")
updateChangeListManager()
runInEdtAndWait {
CommandProcessor.getInstance().executeCommand(myProject, {
runWriteAction {
original.delete(this)
}
runWriteAction {
unversioned.move(this, original.parent)
}
}, null, null)
}
updateChangeListManager()
assertTrue("Original file should exist", originalFile.exists()) // IDEA-153272 failed here: both files were deleted.
assertFalse("Unversioned file shouldn't exist", unversionedFile.exists())
updateChangeListManager()
val change = changeListManager.getChange(VcsUtil.getFilePath(originalFile))
assertNull("There should be no change for $originalFile. Changes: ${getLogString(myProjectPath, changeListManager.allChanges)}", change)
}
// IDEA-118140
fun `test unversioned file should not be added to Git on move`() {
ADD.doNothing()
val file = prepareUnversionedFile("unv.txt")
renameFile(file, "unv-ren.txt")
assertUnversioned(file)
}
fun `test unversioned file should not be added to Git on move even if add silently`() {
ADD.doNothing()
val file = prepareUnversionedFile("unv.txt")
ADD.doSilently()
renameFile(file, "unv-ren.txt")
assertUnversioned(file)
}
private fun VcsConfiguration.StandardConfirmation.doSilently() = setStandardConfirmation(myProject, GitVcs.NAME, this, DO_ACTION_SILENTLY)
private fun VcsConfiguration.StandardConfirmation.doNothing() = setStandardConfirmation(myProject, GitVcs.NAME, this, DO_NOTHING_SILENTLY)
private fun prepareUnversionedFile(fileName: String): VirtualFile {
val file = myProjectRoot.createFile(fileName, "initial\ncontent\n")
updateChangeListManager()
assertUnversioned(file)
return file
}
private fun VirtualFile.createDir(dir: String) = VcsTestUtil.findOrCreateDir(myProject, this, dir)
private fun VirtualFile.createFile(fileName: String, content: String) = VcsTestUtil.createFile(myProject, this, fileName, content)
private fun renameFile(file: VirtualFile, newName: String) {
renameFileInCommand(myProject, file, newName)
updateChangeListManager()
}
private fun assertUnversioned(file: VirtualFile) {
assertTrue("File should be unversioned! All changes: " + getLogString(myProjectPath, changeListManager.allChanges),
changeListManager.isUnversioned(file))
}
}